Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/views/generic/dates.py: 33%
333 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import datetime
3from django.conf import settings
4from django.core.exceptions import ImproperlyConfigured
5from django.db import models
6from django.http import Http404
7from django.utils import timezone
8from django.utils.functional import cached_property
9from django.utils.translation import gettext as _
10from django.views.generic.base import View
11from django.views.generic.detail import (
12 BaseDetailView,
13 SingleObjectTemplateResponseMixin,
14)
15from django.views.generic.list import (
16 MultipleObjectMixin,
17 MultipleObjectTemplateResponseMixin,
18)
21class YearMixin:
22 """Mixin for views manipulating year-based data."""
24 year_format = "%Y"
25 year = None
27 def get_year_format(self):
28 """
29 Get a year format string in strptime syntax to be used to parse the
30 year from url variables.
31 """
32 return self.year_format
34 def get_year(self):
35 """Return the year for which this view should display data."""
36 year = self.year
37 if year is None:
38 try:
39 year = self.kwargs["year"]
40 except KeyError:
41 try:
42 year = self.request.GET["year"]
43 except KeyError:
44 raise Http404(_("No year specified"))
45 return year
47 def get_next_year(self, date):
48 """Get the next valid year."""
49 return _get_next_prev(self, date, is_previous=False, period="year")
51 def get_previous_year(self, date):
52 """Get the previous valid year."""
53 return _get_next_prev(self, date, is_previous=True, period="year")
55 def _get_next_year(self, date):
56 """
57 Return the start date of the next interval.
59 The interval is defined by start date <= item date < next start date.
60 """
61 try:
62 return date.replace(year=date.year + 1, month=1, day=1)
63 except ValueError:
64 raise Http404(_("Date out of range"))
66 def _get_current_year(self, date):
67 """Return the start date of the current interval."""
68 return date.replace(month=1, day=1)
71class MonthMixin:
72 """Mixin for views manipulating month-based data."""
74 month_format = "%b"
75 month = None
77 def get_month_format(self):
78 """
79 Get a month format string in strptime syntax to be used to parse the
80 month from url variables.
81 """
82 return self.month_format
84 def get_month(self):
85 """Return the month for which this view should display data."""
86 month = self.month
87 if month is None:
88 try:
89 month = self.kwargs["month"]
90 except KeyError:
91 try:
92 month = self.request.GET["month"]
93 except KeyError:
94 raise Http404(_("No month specified"))
95 return month
97 def get_next_month(self, date):
98 """Get the next valid month."""
99 return _get_next_prev(self, date, is_previous=False, period="month")
101 def get_previous_month(self, date):
102 """Get the previous valid month."""
103 return _get_next_prev(self, date, is_previous=True, period="month")
105 def _get_next_month(self, date):
106 """
107 Return the start date of the next interval.
109 The interval is defined by start date <= item date < next start date.
110 """
111 if date.month == 12:
112 try:
113 return date.replace(year=date.year + 1, month=1, day=1)
114 except ValueError:
115 raise Http404(_("Date out of range"))
116 else:
117 return date.replace(month=date.month + 1, day=1)
119 def _get_current_month(self, date):
120 """Return the start date of the previous interval."""
121 return date.replace(day=1)
124class DayMixin:
125 """Mixin for views manipulating day-based data."""
127 day_format = "%d"
128 day = None
130 def get_day_format(self):
131 """
132 Get a day format string in strptime syntax to be used to parse the day
133 from url variables.
134 """
135 return self.day_format
137 def get_day(self):
138 """Return the day for which this view should display data."""
139 day = self.day
140 if day is None:
141 try:
142 day = self.kwargs["day"]
143 except KeyError:
144 try:
145 day = self.request.GET["day"]
146 except KeyError:
147 raise Http404(_("No day specified"))
148 return day
150 def get_next_day(self, date):
151 """Get the next valid day."""
152 return _get_next_prev(self, date, is_previous=False, period="day")
154 def get_previous_day(self, date):
155 """Get the previous valid day."""
156 return _get_next_prev(self, date, is_previous=True, period="day")
158 def _get_next_day(self, date):
159 """
160 Return the start date of the next interval.
162 The interval is defined by start date <= item date < next start date.
163 """
164 return date + datetime.timedelta(days=1)
166 def _get_current_day(self, date):
167 """Return the start date of the current interval."""
168 return date
171class WeekMixin:
172 """Mixin for views manipulating week-based data."""
174 week_format = "%U"
175 week = None
177 def get_week_format(self):
178 """
179 Get a week format string in strptime syntax to be used to parse the
180 week from url variables.
181 """
182 return self.week_format
184 def get_week(self):
185 """Return the week for which this view should display data."""
186 week = self.week
187 if week is None:
188 try:
189 week = self.kwargs["week"]
190 except KeyError:
191 try:
192 week = self.request.GET["week"]
193 except KeyError:
194 raise Http404(_("No week specified"))
195 return week
197 def get_next_week(self, date):
198 """Get the next valid week."""
199 return _get_next_prev(self, date, is_previous=False, period="week")
201 def get_previous_week(self, date):
202 """Get the previous valid week."""
203 return _get_next_prev(self, date, is_previous=True, period="week")
205 def _get_next_week(self, date):
206 """
207 Return the start date of the next interval.
209 The interval is defined by start date <= item date < next start date.
210 """
211 try:
212 return date + datetime.timedelta(days=7 - self._get_weekday(date))
213 except OverflowError:
214 raise Http404(_("Date out of range"))
216 def _get_current_week(self, date):
217 """Return the start date of the current interval."""
218 return date - datetime.timedelta(self._get_weekday(date))
220 def _get_weekday(self, date):
221 """
222 Return the weekday for a given date.
224 The first day according to the week format is 0 and the last day is 6.
225 """
226 week_format = self.get_week_format()
227 if week_format in {"%W", "%V"}: # week starts on Monday
228 return date.weekday()
229 elif week_format == "%U": # week starts on Sunday
230 return (date.weekday() + 1) % 7
231 else:
232 raise ValueError("unknown week format: %s" % week_format)
235class DateMixin:
236 """Mixin class for views manipulating date-based data."""
238 date_field = None
239 allow_future = False
241 def get_date_field(self):
242 """Get the name of the date field to be used to filter by."""
243 if self.date_field is None:
244 raise ImproperlyConfigured(
245 "%s.date_field is required." % self.__class__.__name__
246 )
247 return self.date_field
249 def get_allow_future(self):
250 """
251 Return `True` if the view should be allowed to display objects from
252 the future.
253 """
254 return self.allow_future
256 # Note: the following three methods only work in subclasses that also
257 # inherit SingleObjectMixin or MultipleObjectMixin.
259 @cached_property
260 def uses_datetime_field(self):
261 """
262 Return `True` if the date field is a `DateTimeField` and `False`
263 if it's a `DateField`.
264 """
265 model = self.get_queryset().model if self.model is None else self.model
266 field = model._meta.get_field(self.get_date_field())
267 return isinstance(field, models.DateTimeField)
269 def _make_date_lookup_arg(self, value):
270 """
271 Convert a date into a datetime when the date field is a DateTimeField.
273 When time zone support is enabled, `date` is assumed to be in the
274 current time zone, so that displayed items are consistent with the URL.
275 """
276 if self.uses_datetime_field:
277 value = datetime.datetime.combine(value, datetime.time.min)
278 if settings.USE_TZ:
279 value = timezone.make_aware(value)
280 return value
282 def _make_single_date_lookup(self, date):
283 """
284 Get the lookup kwargs for filtering on a single date.
286 If the date field is a DateTimeField, we can't just filter on
287 date_field=date because that doesn't take the time into account.
288 """
289 date_field = self.get_date_field()
290 if self.uses_datetime_field:
291 since = self._make_date_lookup_arg(date)
292 until = self._make_date_lookup_arg(date + datetime.timedelta(days=1))
293 return {
294 "%s__gte" % date_field: since,
295 "%s__lt" % date_field: until,
296 }
297 else:
298 # Skip self._make_date_lookup_arg, it's a no-op in this branch.
299 return {date_field: date}
302class BaseDateListView(MultipleObjectMixin, DateMixin, View):
303 """Abstract base class for date-based views displaying a list of objects."""
305 allow_empty = False
306 date_list_period = "year"
308 def get(self, request, *args, **kwargs):
309 self.date_list, self.object_list, extra_context = self.get_dated_items()
310 context = self.get_context_data(
311 object_list=self.object_list, date_list=self.date_list, **extra_context
312 )
313 return self.render_to_response(context)
315 def get_dated_items(self):
316 """Obtain the list of dates and items."""
317 raise NotImplementedError(
318 "A DateView must provide an implementation of get_dated_items()"
319 )
321 def get_ordering(self):
322 """
323 Return the field or fields to use for ordering the queryset; use the
324 date field by default.
325 """
326 return "-%s" % self.get_date_field() if self.ordering is None else self.ordering
328 def get_dated_queryset(self, **lookup):
329 """
330 Get a queryset properly filtered according to `allow_future` and any
331 extra lookup kwargs.
332 """
333 qs = self.get_queryset().filter(**lookup)
334 date_field = self.get_date_field()
335 allow_future = self.get_allow_future()
336 allow_empty = self.get_allow_empty()
337 paginate_by = self.get_paginate_by(qs)
339 if not allow_future:
340 now = timezone.now() if self.uses_datetime_field else timezone_today()
341 qs = qs.filter(**{"%s__lte" % date_field: now})
343 if not allow_empty:
344 # When pagination is enabled, it's better to do a cheap query
345 # than to load the unpaginated queryset in memory.
346 is_empty = not qs if paginate_by is None else not qs.exists()
347 if is_empty:
348 raise Http404(
349 _("No %(verbose_name_plural)s available")
350 % {
351 "verbose_name_plural": qs.model._meta.verbose_name_plural,
352 }
353 )
355 return qs
357 def get_date_list_period(self):
358 """
359 Get the aggregation period for the list of dates: 'year', 'month', or
360 'day'.
361 """
362 return self.date_list_period
364 def get_date_list(self, queryset, date_type=None, ordering="ASC"):
365 """
366 Get a date list by calling `queryset.dates/datetimes()`, checking
367 along the way for empty lists that aren't allowed.
368 """
369 date_field = self.get_date_field()
370 allow_empty = self.get_allow_empty()
371 if date_type is None:
372 date_type = self.get_date_list_period()
374 if self.uses_datetime_field:
375 date_list = queryset.datetimes(date_field, date_type, ordering)
376 else:
377 date_list = queryset.dates(date_field, date_type, ordering)
378 if date_list is not None and not date_list and not allow_empty:
379 raise Http404(
380 _("No %(verbose_name_plural)s available")
381 % {
382 "verbose_name_plural": queryset.model._meta.verbose_name_plural,
383 }
384 )
386 return date_list
389class BaseArchiveIndexView(BaseDateListView):
390 """
391 Base class for archives of date-based items. Requires a response mixin.
392 """
394 context_object_name = "latest"
396 def get_dated_items(self):
397 """Return (date_list, items, extra_context) for this request."""
398 qs = self.get_dated_queryset()
399 date_list = self.get_date_list(qs, ordering="DESC")
401 if not date_list:
402 qs = qs.none()
404 return (date_list, qs, {})
407class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
408 """Top-level archive of date-based items."""
410 template_name_suffix = "_archive"
413class BaseYearArchiveView(YearMixin, BaseDateListView):
414 """List of objects published in a given year."""
416 date_list_period = "month"
417 make_object_list = False
419 def get_dated_items(self):
420 """Return (date_list, items, extra_context) for this request."""
421 year = self.get_year()
423 date_field = self.get_date_field()
424 date = _date_from_string(year, self.get_year_format())
426 since = self._make_date_lookup_arg(date)
427 until = self._make_date_lookup_arg(self._get_next_year(date))
428 lookup_kwargs = {
429 "%s__gte" % date_field: since,
430 "%s__lt" % date_field: until,
431 }
433 qs = self.get_dated_queryset(**lookup_kwargs)
434 date_list = self.get_date_list(qs)
436 if not self.get_make_object_list():
437 # We need this to be a queryset since parent classes introspect it
438 # to find information about the model.
439 qs = qs.none()
441 return (
442 date_list,
443 qs,
444 {
445 "year": date,
446 "next_year": self.get_next_year(date),
447 "previous_year": self.get_previous_year(date),
448 },
449 )
451 def get_make_object_list(self):
452 """
453 Return `True` if this view should contain the full list of objects in
454 the given year.
455 """
456 return self.make_object_list
459class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
460 """List of objects published in a given year."""
462 template_name_suffix = "_archive_year"
465class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
466 """List of objects published in a given month."""
468 date_list_period = "day"
470 def get_dated_items(self):
471 """Return (date_list, items, extra_context) for this request."""
472 year = self.get_year()
473 month = self.get_month()
475 date_field = self.get_date_field()
476 date = _date_from_string(
477 year, self.get_year_format(), month, self.get_month_format()
478 )
480 since = self._make_date_lookup_arg(date)
481 until = self._make_date_lookup_arg(self._get_next_month(date))
482 lookup_kwargs = {
483 "%s__gte" % date_field: since,
484 "%s__lt" % date_field: until,
485 }
487 qs = self.get_dated_queryset(**lookup_kwargs)
488 date_list = self.get_date_list(qs)
490 return (
491 date_list,
492 qs,
493 {
494 "month": date,
495 "next_month": self.get_next_month(date),
496 "previous_month": self.get_previous_month(date),
497 },
498 )
501class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
502 """List of objects published in a given month."""
504 template_name_suffix = "_archive_month"
507class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
508 """List of objects published in a given week."""
510 def get_dated_items(self):
511 """Return (date_list, items, extra_context) for this request."""
512 year = self.get_year()
513 week = self.get_week()
515 date_field = self.get_date_field()
516 week_format = self.get_week_format()
517 week_choices = {"%W": "1", "%U": "0", "%V": "1"}
518 try:
519 week_start = week_choices[week_format]
520 except KeyError:
521 raise ValueError(
522 "Unknown week format %r. Choices are: %s"
523 % (
524 week_format,
525 ", ".join(sorted(week_choices)),
526 )
527 )
528 year_format = self.get_year_format()
529 if week_format == "%V" and year_format != "%G":
530 raise ValueError(
531 "ISO week directive '%s' is incompatible with the year "
532 "directive '%s'. Use the ISO year '%%G' instead."
533 % (
534 week_format,
535 year_format,
536 )
537 )
538 date = _date_from_string(year, year_format, week_start, "%w", week, week_format)
539 since = self._make_date_lookup_arg(date)
540 until = self._make_date_lookup_arg(self._get_next_week(date))
541 lookup_kwargs = {
542 "%s__gte" % date_field: since,
543 "%s__lt" % date_field: until,
544 }
546 qs = self.get_dated_queryset(**lookup_kwargs)
548 return (
549 None,
550 qs,
551 {
552 "week": date,
553 "next_week": self.get_next_week(date),
554 "previous_week": self.get_previous_week(date),
555 },
556 )
559class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
560 """List of objects published in a given week."""
562 template_name_suffix = "_archive_week"
565class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
566 """List of objects published on a given day."""
568 def get_dated_items(self):
569 """Return (date_list, items, extra_context) for this request."""
570 year = self.get_year()
571 month = self.get_month()
572 day = self.get_day()
574 date = _date_from_string(
575 year,
576 self.get_year_format(),
577 month,
578 self.get_month_format(),
579 day,
580 self.get_day_format(),
581 )
583 return self._get_dated_items(date)
585 def _get_dated_items(self, date):
586 """
587 Do the actual heavy lifting of getting the dated items; this accepts a
588 date object so that TodayArchiveView can be trivial.
589 """
590 lookup_kwargs = self._make_single_date_lookup(date)
591 qs = self.get_dated_queryset(**lookup_kwargs)
593 return (
594 None,
595 qs,
596 {
597 "day": date,
598 "previous_day": self.get_previous_day(date),
599 "next_day": self.get_next_day(date),
600 "previous_month": self.get_previous_month(date),
601 "next_month": self.get_next_month(date),
602 },
603 )
606class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
607 """List of objects published on a given day."""
609 template_name_suffix = "_archive_day"
612class BaseTodayArchiveView(BaseDayArchiveView):
613 """List of objects published today."""
615 def get_dated_items(self):
616 """Return (date_list, items, extra_context) for this request."""
617 return self._get_dated_items(datetime.date.today())
620class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
621 """List of objects published today."""
623 template_name_suffix = "_archive_day"
626class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
627 """
628 Detail view of a single object on a single date; this differs from the
629 standard DetailView by accepting a year/month/day in the URL.
630 """
632 def get_object(self, queryset=None):
633 """Get the object this request displays."""
634 year = self.get_year()
635 month = self.get_month()
636 day = self.get_day()
637 date = _date_from_string(
638 year,
639 self.get_year_format(),
640 month,
641 self.get_month_format(),
642 day,
643 self.get_day_format(),
644 )
646 # Use a custom queryset if provided
647 qs = self.get_queryset() if queryset is None else queryset
649 if not self.get_allow_future() and date > datetime.date.today():
650 raise Http404(
651 _(
652 "Future %(verbose_name_plural)s not available because "
653 "%(class_name)s.allow_future is False."
654 )
655 % {
656 "verbose_name_plural": qs.model._meta.verbose_name_plural,
657 "class_name": self.__class__.__name__,
658 }
659 )
661 # Filter down a queryset from self.queryset using the date from the
662 # URL. This'll get passed as the queryset to DetailView.get_object,
663 # which'll handle the 404
664 lookup_kwargs = self._make_single_date_lookup(date)
665 qs = qs.filter(**lookup_kwargs)
667 return super().get_object(queryset=qs)
670class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
671 """
672 Detail view of a single object on a single date; this differs from the
673 standard DetailView by accepting a year/month/day in the URL.
674 """
676 template_name_suffix = "_detail"
679def _date_from_string(
680 year, year_format, month="", month_format="", day="", day_format="", delim="__"
681):
682 """
683 Get a datetime.date object given a format string and a year, month, and day
684 (only year is mandatory). Raise a 404 for an invalid date.
685 """
686 format = year_format + delim + month_format + delim + day_format
687 datestr = str(year) + delim + str(month) + delim + str(day)
688 try:
689 return datetime.datetime.strptime(datestr, format).date()
690 except ValueError:
691 raise Http404(
692 _("Invalid date string “%(datestr)s” given format “%(format)s”")
693 % {
694 "datestr": datestr,
695 "format": format,
696 }
697 )
700def _get_next_prev(generic_view, date, is_previous, period):
701 """
702 Get the next or the previous valid date. The idea is to allow links on
703 month/day views to never be 404s by never providing a date that'll be
704 invalid for the given view.
706 This is a bit complicated since it handles different intervals of time,
707 hence the coupling to generic_view.
709 However in essence the logic comes down to:
711 * If allow_empty and allow_future are both true, this is easy: just
712 return the naive result (just the next/previous day/week/month,
713 regardless of object existence.)
715 * If allow_empty is true, allow_future is false, and the naive result
716 isn't in the future, then return it; otherwise return None.
718 * If allow_empty is false and allow_future is true, return the next
719 date *that contains a valid object*, even if it's in the future. If
720 there are no next objects, return None.
722 * If allow_empty is false and allow_future is false, return the next
723 date that contains a valid object. If that date is in the future, or
724 if there are no next objects, return None.
725 """
726 date_field = generic_view.get_date_field()
727 allow_empty = generic_view.get_allow_empty()
728 allow_future = generic_view.get_allow_future()
730 get_current = getattr(generic_view, "_get_current_%s" % period)
731 get_next = getattr(generic_view, "_get_next_%s" % period)
733 # Bounds of the current interval
734 start, end = get_current(date), get_next(date)
736 # If allow_empty is True, the naive result will be valid
737 if allow_empty:
738 if is_previous:
739 result = get_current(start - datetime.timedelta(days=1))
740 else:
741 result = end
743 if allow_future or result <= timezone_today():
744 return result
745 else:
746 return None
748 # Otherwise, we'll need to go to the database to look for an object
749 # whose date_field is at least (greater than/less than) the given
750 # naive result
751 else:
752 # Construct a lookup and an ordering depending on whether we're doing
753 # a previous date or a next date lookup.
754 if is_previous:
755 lookup = {"%s__lt" % date_field: generic_view._make_date_lookup_arg(start)}
756 ordering = "-%s" % date_field
757 else:
758 lookup = {"%s__gte" % date_field: generic_view._make_date_lookup_arg(end)}
759 ordering = date_field
761 # Filter out objects in the future if appropriate.
762 if not allow_future:
763 # Fortunately, to match the implementation of allow_future,
764 # we need __lte, which doesn't conflict with __lt above.
765 if generic_view.uses_datetime_field:
766 now = timezone.now()
767 else:
768 now = timezone_today()
769 lookup["%s__lte" % date_field] = now
771 qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
773 # Snag the first object from the queryset; if it doesn't exist that
774 # means there's no next/previous link available.
775 try:
776 result = getattr(qs[0], date_field)
777 except IndexError:
778 return None
780 # Convert datetimes to dates in the current time zone.
781 if generic_view.uses_datetime_field:
782 if settings.USE_TZ:
783 result = timezone.localtime(result)
784 result = result.date()
786 # Return the first day of the period.
787 return get_current(result)
790def timezone_today():
791 """Return the current date in the current time zone."""
792 if settings.USE_TZ:
793 return timezone.localdate()
794 else:
795 return datetime.date.today()