721 lines
25 KiB
Python
721 lines
25 KiB
Python
|
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 == '%W': # 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_start = {
|
||
|
'%W': '1',
|
||
|
'%U': '0',
|
||
|
}[week_format]
|
||
|
date = _date_from_string(year, self.get_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()
|