| import time | |
| import datetime | |
| from django.db import models | |
| from django.core.exceptions import ImproperlyConfigured | |
| from django.http import Http404 | |
| from django.utils.encoding import force_unicode | |
| from django.utils.translation import ugettext 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(object): | |
| 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(_(u"No year specified")) | |
| return year | |
| class MonthMixin(object): | |
| 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(_(u"No month specified")) | |
| return month | |
| def get_next_month(self, date): | |
| """ | |
| Get the next valid month. | |
| """ | |
| first_day, last_day = _month_bounds(date) | |
| next = (last_day + datetime.timedelta(days=1)).replace(day=1) | |
| return _get_next_prev_month(self, next, is_previous=False, use_first_day=True) | |
| def get_previous_month(self, date): | |
| """ | |
| Get the previous valid month. | |
| """ | |
| first_day, last_day = _month_bounds(date) | |
| prev = (first_day - datetime.timedelta(days=1)) | |
| return _get_next_prev_month(self, prev, is_previous=True, use_first_day=True) | |
| class DayMixin(object): | |
| 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(_(u"No day specified")) | |
| return day | |
| def get_next_day(self, date): | |
| """ | |
| Get the next valid day. | |
| """ | |
| next = date + datetime.timedelta(days=1) | |
| return _get_next_prev_month(self, next, is_previous=False, use_first_day=False) | |
| def get_previous_day(self, date): | |
| """ | |
| Get the previous valid day. | |
| """ | |
| prev = date - datetime.timedelta(days=1) | |
| return _get_next_prev_month(self, prev, is_previous=True, use_first_day=False) | |
| class WeekMixin(object): | |
| 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(_(u"No week specified")) | |
| return week | |
| class DateMixin(object): | |
| """ | |
| 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(u"%s.date_field is required." % self.__class__.__name__) | |
| return self.date_field | |
| def get_allow_future(self): | |
| """ | |
| Returns `True` if the view should be allowed to display objects from | |
| the future. | |
| """ | |
| return self.allow_future | |
| class BaseDateListView(MultipleObjectMixin, DateMixin, View): | |
| """ | |
| Abstract base class for date-based views display a list of objects. | |
| """ | |
| allow_empty = False | |
| 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) | |
| context.update(extra_context) | |
| return self.render_to_response(context) | |
| def get_dated_items(self): | |
| """ | |
| Obtain the list of dates and itesm | |
| """ | |
| raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') | |
| 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() | |
| if not allow_future: | |
| qs = qs.filter(**{'%s__lte' % date_field: datetime.datetime.now()}) | |
| if not allow_empty and not qs: | |
| raise Http404(_(u"No %(verbose_name_plural)s available") % { | |
| 'verbose_name_plural': force_unicode(qs.model._meta.verbose_name_plural) | |
| }) | |
| return qs | |
| def get_date_list(self, queryset, date_type): | |
| """ | |
| Get a date list by calling `queryset.dates()`, checking along the way | |
| for empty lists that aren't allowed. | |
| """ | |
| date_field = self.get_date_field() | |
| allow_empty = self.get_allow_empty() | |
| date_list = queryset.dates(date_field, date_type)[::-1] | |
| if date_list is not None and not date_list and not allow_empty: | |
| name = force_unicode(queryset.model._meta.verbose_name_plural) | |
| raise Http404(_(u"No %(verbose_name_plural)s available") % | |
| {'verbose_name_plural': name}) | |
| return date_list | |
| def get_context_data(self, **kwargs): | |
| """ | |
| Get the context. Must return a Context (or subclass) instance. | |
| """ | |
| items = kwargs.pop('object_list') | |
| context = super(BaseDateListView, self).get_context_data(object_list=items) | |
| context.update(kwargs) | |
| return context | |
| 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, 'year') | |
| if date_list: | |
| object_list = qs.order_by('-' + self.get_date_field()) | |
| else: | |
| object_list = qs.none() | |
| return (date_list, object_list, {}) | |
| 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. | |
| """ | |
| make_object_list = False | |
| def get_dated_items(self): | |
| """ | |
| Return (date_list, items, extra_context) for this request. | |
| """ | |
| # Yes, no error checking: the URLpattern ought to validate this; it's | |
| # an error if it doesn't. | |
| year = self.get_year() | |
| date_field = self.get_date_field() | |
| qs = self.get_dated_queryset(**{date_field+'__year': year}) | |
| date_list = self.get_date_list(qs, 'month') | |
| if self.get_make_object_list(): | |
| object_list = qs.order_by('-'+date_field) | |
| else: | |
| # We need this to be a queryset since parent classes introspect it | |
| # to find information about the model. | |
| object_list = qs.none() | |
| return (date_list, object_list, {'year': year}) | |
| 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 year. | |
| """ | |
| 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()) | |
| # Construct a date-range lookup. | |
| first_day, last_day = _month_bounds(date) | |
| lookup_kwargs = { | |
| '%s__gte' % date_field: first_day, | |
| '%s__lt' % date_field: last_day, | |
| } | |
| qs = self.get_dated_queryset(**lookup_kwargs) | |
| date_list = self.get_date_list(qs, 'day') | |
| 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 year. | |
| """ | |
| 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) | |
| # Construct a date-range lookup. | |
| first_day = date | |
| last_day = date + datetime.timedelta(days=7) | |
| lookup_kwargs = { | |
| '%s__gte' % date_field: first_day, | |
| '%s__lt' % date_field: last_day, | |
| } | |
| qs = self.get_dated_queryset(**lookup_kwargs) | |
| return (None, qs, {'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. | |
| """ | |
| date_field = self.get_date_field() | |
| field = self.get_queryset().model._meta.get_field(date_field) | |
| lookup_kwargs = _date_lookup_for_field(field, 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()) | |
| qs = self.get_queryset() | |
| if not self.get_allow_future() and date > datetime.date.today(): | |
| raise Http404(_(u"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 | |
| date_field = self.get_date_field() | |
| field = qs.model._meta.get_field(date_field) | |
| lookup = _date_lookup_for_field(field, date) | |
| qs = qs.filter(**lookup) | |
| return super(BaseDetailView, self).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='__'): | |
| """ | |
| Helper: get a datetime.date object given a format string and a year, | |
| month, and possibly day; raise a 404 for an invalid date. | |
| """ | |
| format = delim.join((year_format, month_format, day_format)) | |
| datestr = delim.join((year, month, day)) | |
| try: | |
| return datetime.date(*time.strptime(datestr, format)[:3]) | |
| except ValueError: | |
| raise Http404(_(u"Invalid date string '%(datestr)s' given format '%(format)s'") % { | |
| 'datestr': datestr, | |
| 'format': format, | |
| }) | |
| def _month_bounds(date): | |
| """ | |
| Helper: return the first and last days of the month for the given date. | |
| """ | |
| first_day = date.replace(day=1) | |
| if first_day.month == 12: | |
| last_day = first_day.replace(year=first_day.year + 1, month=1) | |
| else: | |
| last_day = first_day.replace(month=first_day.month + 1) | |
| return first_day, last_day | |
| def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day): | |
| """ | |
| Helper: 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 both next and previous months | |
| and days (for MonthArchiveView and DayArchiveView); 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 or month, | |
| reguardless of object existence.) | |
| * If allow_empty is true, allow_future is false, and the naive month | |
| 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() | |
| # If allow_empty is True the naive value will be valid | |
| if allow_empty: | |
| result = naive_result | |
| # 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__lte' % date_field: naive_result} | |
| ordering = '-%s' % date_field | |
| else: | |
| lookup = {'%s__gte' % date_field: naive_result} | |
| ordering = date_field | |
| 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: | |
| result = None | |
| # Convert datetimes to a dates | |
| if hasattr(result, 'date'): | |
| result = result.date() | |
| # For month views, we always want to have a date that's the first of the | |
| # month for consistency's sake. | |
| if result and use_first_day: | |
| result = result.replace(day=1) | |
| # Check against future dates. | |
| if result and (allow_future or result < datetime.date.today()): | |
| return result | |
| else: | |
| return None | |
| def _date_lookup_for_field(field, date): | |
| """ | |
| Get the lookup kwargs for looking up a date against a given Field. If the | |
| date field is a DateTimeField, we can't just do filter(df=date) because | |
| that doesn't take the time into account. So we need to make a range lookup | |
| in those cases. | |
| """ | |
| if isinstance(field, models.DateTimeField): | |
| date_range = ( | |
| datetime.datetime.combine(date, datetime.time.min), | |
| datetime.datetime.combine(date, datetime.time.max) | |
| ) | |
| return {'%s__range' % field.name: date_range} | |
| else: | |
| return {field.name: date} |