|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- import datetime
-
- from django.conf import settings
- from django.core.exceptions import ImproperlyConfigured
- from django.db import models
- from django.http import Http404
- from django.utils import timezone
- from django.utils.functional import cached_property
- from django.utils.translation import gettext as _
- from django.views.generic.base import View
- from django.views.generic.detail import (
- BaseDetailView,
- SingleObjectTemplateResponseMixin,
- )
- from django.views.generic.list import (
- MultipleObjectMixin,
- MultipleObjectTemplateResponseMixin,
- )
-
-
- class YearMixin:
- """Mixin for views manipulating year-based data."""
-
- year_format = "%Y"
- year = None
-
- def get_year_format(self):
- """
- Get a year format string in strptime syntax to be used to parse the
- year from url variables.
- """
- return self.year_format
-
- def get_year(self):
- """Return the year for which this view should display data."""
- year = self.year
- if year is None:
- try:
- year = self.kwargs["year"]
- except KeyError:
- try:
- year = self.request.GET["year"]
- except KeyError:
- raise Http404(_("No year specified"))
- return year
-
- def get_next_year(self, date):
- """Get the next valid year."""
- return _get_next_prev(self, date, is_previous=False, period="year")
-
- def get_previous_year(self, date):
- """Get the previous valid year."""
- return _get_next_prev(self, date, is_previous=True, period="year")
-
- def _get_next_year(self, date):
- """
- Return the start date of the next interval.
-
- The interval is defined by start date <= item date < next start date.
- """
- try:
- return date.replace(year=date.year + 1, month=1, day=1)
- except ValueError:
- raise Http404(_("Date out of range"))
-
- def _get_current_year(self, date):
- """Return the start date of the current interval."""
- return date.replace(month=1, day=1)
-
-
- class MonthMixin:
- """Mixin for views manipulating month-based data."""
-
- month_format = "%b"
- month = None
-
- def get_month_format(self):
- """
- Get a month format string in strptime syntax to be used to parse the
- month from url variables.
- """
- return self.month_format
-
- def get_month(self):
- """Return the month for which this view should display data."""
- month = self.month
- if month is None:
- try:
- month = self.kwargs["month"]
- except KeyError:
- try:
- month = self.request.GET["month"]
- except KeyError:
- raise Http404(_("No month specified"))
- return month
-
- def get_next_month(self, date):
- """Get the next valid month."""
- return _get_next_prev(self, date, is_previous=False, period="month")
-
- def get_previous_month(self, date):
- """Get the previous valid month."""
- return _get_next_prev(self, date, is_previous=True, period="month")
-
- def _get_next_month(self, date):
- """
- Return the start date of the next interval.
-
- The interval is defined by start date <= item date < next start date.
- """
- if date.month == 12:
- try:
- return date.replace(year=date.year + 1, month=1, day=1)
- except ValueError:
- raise Http404(_("Date out of range"))
- else:
- return date.replace(month=date.month + 1, day=1)
-
- def _get_current_month(self, date):
- """Return the start date of the previous interval."""
- return date.replace(day=1)
-
-
- class DayMixin:
- """Mixin for views manipulating day-based data."""
-
- day_format = "%d"
- day = None
-
- def get_day_format(self):
- """
- Get a day format string in strptime syntax to be used to parse the day
- from url variables.
- """
- return self.day_format
-
- def get_day(self):
- """Return the day for which this view should display data."""
- day = self.day
- if day is None:
- try:
- day = self.kwargs["day"]
- except KeyError:
- try:
- day = self.request.GET["day"]
- except KeyError:
- raise Http404(_("No day specified"))
- return day
-
- def get_next_day(self, date):
- """Get the next valid day."""
- return _get_next_prev(self, date, is_previous=False, period="day")
-
- def get_previous_day(self, date):
- """Get the previous valid day."""
- return _get_next_prev(self, date, is_previous=True, period="day")
-
- def _get_next_day(self, date):
- """
- Return the start date of the next interval.
-
- The interval is defined by start date <= item date < next start date.
- """
- return date + datetime.timedelta(days=1)
-
- def _get_current_day(self, date):
- """Return the start date of the current interval."""
- return date
-
-
- class WeekMixin:
- """Mixin for views manipulating week-based data."""
-
- week_format = "%U"
- week = None
-
- def get_week_format(self):
- """
- Get a week format string in strptime syntax to be used to parse the
- week from url variables.
- """
- return self.week_format
-
- def get_week(self):
- """Return the week for which this view should display data."""
- week = self.week
- if week is None:
- try:
- week = self.kwargs["week"]
- except KeyError:
- try:
- week = self.request.GET["week"]
- except KeyError:
- raise Http404(_("No week specified"))
- return week
-
- def get_next_week(self, date):
- """Get the next valid week."""
- return _get_next_prev(self, date, is_previous=False, period="week")
-
- def get_previous_week(self, date):
- """Get the previous valid week."""
- return _get_next_prev(self, date, is_previous=True, period="week")
-
- def _get_next_week(self, date):
- """
- Return the start date of the next interval.
-
- The interval is defined by start date <= item date < next start date.
- """
- try:
- return date + datetime.timedelta(days=7 - self._get_weekday(date))
- except OverflowError:
- raise Http404(_("Date out of range"))
-
- def _get_current_week(self, date):
- """Return the start date of the current interval."""
- return date - datetime.timedelta(self._get_weekday(date))
-
- def _get_weekday(self, date):
- """
- Return the weekday for a given date.
-
- The first day according to the week format is 0 and the last day is 6.
- """
- week_format = self.get_week_format()
- if week_format in {"%W", "%V"}: # week starts on Monday
- return date.weekday()
- elif week_format == "%U": # week starts on Sunday
- return (date.weekday() + 1) % 7
- else:
- raise ValueError("unknown week format: %s" % week_format)
-
-
- class DateMixin:
- """Mixin class for views manipulating date-based data."""
-
- date_field = None
- allow_future = False
-
- def get_date_field(self):
- """Get the name of the date field to be used to filter by."""
- if self.date_field is None:
- raise ImproperlyConfigured(
- "%s.date_field is required." % self.__class__.__name__
- )
- return self.date_field
-
- def get_allow_future(self):
- """
- Return `True` if the view should be allowed to display objects from
- the future.
- """
- return self.allow_future
-
- # Note: the following three methods only work in subclasses that also
- # inherit SingleObjectMixin or MultipleObjectMixin.
-
- @cached_property
- def uses_datetime_field(self):
- """
- Return `True` if the date field is a `DateTimeField` and `False`
- if it's a `DateField`.
- """
- model = self.get_queryset().model if self.model is None else self.model
- field = model._meta.get_field(self.get_date_field())
- return isinstance(field, models.DateTimeField)
-
- def _make_date_lookup_arg(self, value):
- """
- Convert a date into a datetime when the date field is a DateTimeField.
-
- When time zone support is enabled, `date` is assumed to be in the
- current time zone, so that displayed items are consistent with the URL.
- """
- if self.uses_datetime_field:
- value = datetime.datetime.combine(value, datetime.time.min)
- if settings.USE_TZ:
- value = timezone.make_aware(value)
- return value
-
- def _make_single_date_lookup(self, date):
- """
- Get the lookup kwargs for filtering on a single date.
-
- If the date field is a DateTimeField, we can't just filter on
- date_field=date because that doesn't take the time into account.
- """
- date_field = self.get_date_field()
- if self.uses_datetime_field:
- since = self._make_date_lookup_arg(date)
- until = self._make_date_lookup_arg(date + datetime.timedelta(days=1))
- return {
- "%s__gte" % date_field: since,
- "%s__lt" % date_field: until,
- }
- else:
- # Skip self._make_date_lookup_arg, it's a no-op in this branch.
- return {date_field: date}
-
-
- class BaseDateListView(MultipleObjectMixin, DateMixin, View):
- """Abstract base class for date-based views displaying a list of objects."""
-
- allow_empty = False
- date_list_period = "year"
-
- def get(self, request, *args, **kwargs):
- self.date_list, self.object_list, extra_context = self.get_dated_items()
- context = self.get_context_data(
- object_list=self.object_list, date_list=self.date_list, **extra_context
- )
- return self.render_to_response(context)
-
- def get_dated_items(self):
- """Obtain the list of dates and items."""
- raise NotImplementedError(
- "A DateView must provide an implementation of get_dated_items()"
- )
-
- def get_ordering(self):
- """
- Return the field or fields to use for ordering the queryset; use the
- date field by default.
- """
- return "-%s" % self.get_date_field() if self.ordering is None else self.ordering
-
- def get_dated_queryset(self, **lookup):
- """
- Get a queryset properly filtered according to `allow_future` and any
- extra lookup kwargs.
- """
- qs = self.get_queryset().filter(**lookup)
- date_field = self.get_date_field()
- allow_future = self.get_allow_future()
- allow_empty = self.get_allow_empty()
- paginate_by = self.get_paginate_by(qs)
-
- if not allow_future:
- now = timezone.now() if self.uses_datetime_field else timezone_today()
- qs = qs.filter(**{"%s__lte" % date_field: now})
-
- if not allow_empty:
- # When pagination is enabled, it's better to do a cheap query
- # than to load the unpaginated queryset in memory.
- is_empty = not qs if paginate_by is None else not qs.exists()
- if is_empty:
- raise Http404(
- _("No %(verbose_name_plural)s available")
- % {
- "verbose_name_plural": qs.model._meta.verbose_name_plural,
- }
- )
-
- return qs
-
- def get_date_list_period(self):
- """
- Get the aggregation period for the list of dates: 'year', 'month', or
- 'day'.
- """
- return self.date_list_period
-
- def get_date_list(self, queryset, date_type=None, ordering="ASC"):
- """
- Get a date list by calling `queryset.dates/datetimes()`, checking
- along the way for empty lists that aren't allowed.
- """
- date_field = self.get_date_field()
- allow_empty = self.get_allow_empty()
- if date_type is None:
- date_type = self.get_date_list_period()
-
- if self.uses_datetime_field:
- date_list = queryset.datetimes(date_field, date_type, ordering)
- else:
- date_list = queryset.dates(date_field, date_type, ordering)
- if date_list is not None and not date_list and not allow_empty:
- raise Http404(
- _("No %(verbose_name_plural)s available")
- % {
- "verbose_name_plural": queryset.model._meta.verbose_name_plural,
- }
- )
-
- return date_list
-
-
- class BaseArchiveIndexView(BaseDateListView):
- """
- Base class for archives of date-based items. Requires a response mixin.
- """
-
- context_object_name = "latest"
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- qs = self.get_dated_queryset()
- date_list = self.get_date_list(qs, ordering="DESC")
-
- if not date_list:
- qs = qs.none()
-
- return (date_list, qs, {})
-
-
- class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
- """Top-level archive of date-based items."""
-
- template_name_suffix = "_archive"
-
-
- class BaseYearArchiveView(YearMixin, BaseDateListView):
- """List of objects published in a given year."""
-
- date_list_period = "month"
- make_object_list = False
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- year = self.get_year()
-
- date_field = self.get_date_field()
- date = _date_from_string(year, self.get_year_format())
-
- since = self._make_date_lookup_arg(date)
- until = self._make_date_lookup_arg(self._get_next_year(date))
- lookup_kwargs = {
- "%s__gte" % date_field: since,
- "%s__lt" % date_field: until,
- }
-
- qs = self.get_dated_queryset(**lookup_kwargs)
- date_list = self.get_date_list(qs)
-
- if not self.get_make_object_list():
- # We need this to be a queryset since parent classes introspect it
- # to find information about the model.
- qs = qs.none()
-
- return (
- date_list,
- qs,
- {
- "year": date,
- "next_year": self.get_next_year(date),
- "previous_year": self.get_previous_year(date),
- },
- )
-
- def get_make_object_list(self):
- """
- Return `True` if this view should contain the full list of objects in
- the given year.
- """
- return self.make_object_list
-
-
- class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
- """List of objects published in a given year."""
-
- template_name_suffix = "_archive_year"
-
-
- class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
- """List of objects published in a given month."""
-
- date_list_period = "day"
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- year = self.get_year()
- month = self.get_month()
-
- date_field = self.get_date_field()
- date = _date_from_string(
- year, self.get_year_format(), month, self.get_month_format()
- )
-
- since = self._make_date_lookup_arg(date)
- until = self._make_date_lookup_arg(self._get_next_month(date))
- lookup_kwargs = {
- "%s__gte" % date_field: since,
- "%s__lt" % date_field: until,
- }
-
- qs = self.get_dated_queryset(**lookup_kwargs)
- date_list = self.get_date_list(qs)
-
- return (
- date_list,
- qs,
- {
- "month": date,
- "next_month": self.get_next_month(date),
- "previous_month": self.get_previous_month(date),
- },
- )
-
-
- class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
- """List of objects published in a given month."""
-
- template_name_suffix = "_archive_month"
-
-
- class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
- """List of objects published in a given week."""
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- year = self.get_year()
- week = self.get_week()
-
- date_field = self.get_date_field()
- week_format = self.get_week_format()
- week_choices = {"%W": "1", "%U": "0", "%V": "1"}
- try:
- week_start = week_choices[week_format]
- except KeyError:
- raise ValueError(
- "Unknown week format %r. Choices are: %s"
- % (
- week_format,
- ", ".join(sorted(week_choices)),
- )
- )
- year_format = self.get_year_format()
- if week_format == "%V" and year_format != "%G":
- raise ValueError(
- "ISO week directive '%s' is incompatible with the year "
- "directive '%s'. Use the ISO year '%%G' instead."
- % (
- week_format,
- year_format,
- )
- )
- date = _date_from_string(year, year_format, week_start, "%w", week, week_format)
- since = self._make_date_lookup_arg(date)
- until = self._make_date_lookup_arg(self._get_next_week(date))
- lookup_kwargs = {
- "%s__gte" % date_field: since,
- "%s__lt" % date_field: until,
- }
-
- qs = self.get_dated_queryset(**lookup_kwargs)
-
- return (
- None,
- qs,
- {
- "week": date,
- "next_week": self.get_next_week(date),
- "previous_week": self.get_previous_week(date),
- },
- )
-
-
- class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
- """List of objects published in a given week."""
-
- template_name_suffix = "_archive_week"
-
-
- class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
- """List of objects published on a given day."""
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- year = self.get_year()
- month = self.get_month()
- day = self.get_day()
-
- date = _date_from_string(
- year,
- self.get_year_format(),
- month,
- self.get_month_format(),
- day,
- self.get_day_format(),
- )
-
- return self._get_dated_items(date)
-
- def _get_dated_items(self, date):
- """
- Do the actual heavy lifting of getting the dated items; this accepts a
- date object so that TodayArchiveView can be trivial.
- """
- lookup_kwargs = self._make_single_date_lookup(date)
- qs = self.get_dated_queryset(**lookup_kwargs)
-
- return (
- None,
- qs,
- {
- "day": date,
- "previous_day": self.get_previous_day(date),
- "next_day": self.get_next_day(date),
- "previous_month": self.get_previous_month(date),
- "next_month": self.get_next_month(date),
- },
- )
-
-
- class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
- """List of objects published on a given day."""
-
- template_name_suffix = "_archive_day"
-
-
- class BaseTodayArchiveView(BaseDayArchiveView):
- """List of objects published today."""
-
- def get_dated_items(self):
- """Return (date_list, items, extra_context) for this request."""
- return self._get_dated_items(datetime.date.today())
-
-
- class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
- """List of objects published today."""
-
- template_name_suffix = "_archive_day"
-
-
- class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
- """
- Detail view of a single object on a single date; this differs from the
- standard DetailView by accepting a year/month/day in the URL.
- """
-
- def get_object(self, queryset=None):
- """Get the object this request displays."""
- year = self.get_year()
- month = self.get_month()
- day = self.get_day()
- date = _date_from_string(
- year,
- self.get_year_format(),
- month,
- self.get_month_format(),
- day,
- self.get_day_format(),
- )
-
- # Use a custom queryset if provided
- qs = self.get_queryset() if queryset is None else queryset
-
- if not self.get_allow_future() and date > datetime.date.today():
- raise Http404(
- _(
- "Future %(verbose_name_plural)s not available because "
- "%(class_name)s.allow_future is False."
- )
- % {
- "verbose_name_plural": qs.model._meta.verbose_name_plural,
- "class_name": self.__class__.__name__,
- }
- )
-
- # Filter down a queryset from self.queryset using the date from the
- # URL. This'll get passed as the queryset to DetailView.get_object,
- # which'll handle the 404
- lookup_kwargs = self._make_single_date_lookup(date)
- qs = qs.filter(**lookup_kwargs)
-
- return super().get_object(queryset=qs)
-
-
- class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
- """
- Detail view of a single object on a single date; this differs from the
- standard DetailView by accepting a year/month/day in the URL.
- """
-
- template_name_suffix = "_detail"
-
-
- def _date_from_string(
- year, year_format, month="", month_format="", day="", day_format="", delim="__"
- ):
- """
- Get a datetime.date object given a format string and a year, month, and day
- (only year is mandatory). Raise a 404 for an invalid date.
- """
- format = year_format + delim + month_format + delim + day_format
- datestr = str(year) + delim + str(month) + delim + str(day)
- try:
- return datetime.datetime.strptime(datestr, format).date()
- except ValueError:
- raise Http404(
- _("Invalid date string “%(datestr)s” given format “%(format)s”")
- % {
- "datestr": datestr,
- "format": format,
- }
- )
-
-
- def _get_next_prev(generic_view, date, is_previous, period):
- """
- Get the next or the previous valid date. The idea is to allow links on
- month/day views to never be 404s by never providing a date that'll be
- invalid for the given view.
-
- This is a bit complicated since it handles different intervals of time,
- hence the coupling to generic_view.
-
- However in essence the logic comes down to:
-
- * If allow_empty and allow_future are both true, this is easy: just
- return the naive result (just the next/previous day/week/month,
- regardless of object existence.)
-
- * If allow_empty is true, allow_future is false, and the naive result
- isn't in the future, then return it; otherwise return None.
-
- * If allow_empty is false and allow_future is true, return the next
- date *that contains a valid object*, even if it's in the future. If
- there are no next objects, return None.
-
- * If allow_empty is false and allow_future is false, return the next
- date that contains a valid object. If that date is in the future, or
- if there are no next objects, return None.
- """
- date_field = generic_view.get_date_field()
- allow_empty = generic_view.get_allow_empty()
- allow_future = generic_view.get_allow_future()
-
- get_current = getattr(generic_view, "_get_current_%s" % period)
- get_next = getattr(generic_view, "_get_next_%s" % period)
-
- # Bounds of the current interval
- start, end = get_current(date), get_next(date)
-
- # If allow_empty is True, the naive result will be valid
- if allow_empty:
- if is_previous:
- result = get_current(start - datetime.timedelta(days=1))
- else:
- result = end
-
- if allow_future or result <= timezone_today():
- return result
- else:
- return None
-
- # Otherwise, we'll need to go to the database to look for an object
- # whose date_field is at least (greater than/less than) the given
- # naive result
- else:
- # Construct a lookup and an ordering depending on whether we're doing
- # a previous date or a next date lookup.
- if is_previous:
- lookup = {"%s__lt" % date_field: generic_view._make_date_lookup_arg(start)}
- ordering = "-%s" % date_field
- else:
- lookup = {"%s__gte" % date_field: generic_view._make_date_lookup_arg(end)}
- ordering = date_field
-
- # Filter out objects in the future if appropriate.
- if not allow_future:
- # Fortunately, to match the implementation of allow_future,
- # we need __lte, which doesn't conflict with __lt above.
- if generic_view.uses_datetime_field:
- now = timezone.now()
- else:
- now = timezone_today()
- lookup["%s__lte" % date_field] = now
-
- qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
-
- # Snag the first object from the queryset; if it doesn't exist that
- # means there's no next/previous link available.
- try:
- result = getattr(qs[0], date_field)
- except IndexError:
- return None
-
- # Convert datetimes to dates in the current time zone.
- if generic_view.uses_datetime_field:
- if settings.USE_TZ:
- result = timezone.localtime(result)
- result = result.date()
-
- # Return the first day of the period.
- return get_current(result)
-
-
- def timezone_today():
- """Return the current date in the current time zone."""
- if settings.USE_TZ:
- return timezone.localdate()
- else:
- return datetime.date.today()
|