# Django 4.2.11 documentation

# Source code for django.views.generic.dates

    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()