| import time |
| import datetime |
| from django.db import models |
| from django.core.exceptions import ImproperlyConfigured |
| from django.http import Http404 |
| 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("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("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)).replace(day=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 month format string in strptime syntax to be used to parse the |
| month 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. |
| """ |
| 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("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 %s available" % 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: |
| raise Http404(u"No %s available" % unicode(qs.model._meta.verbose_name_plural)) |
| |
| 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("Future %s not available because %s.allow_future is False." % ( |
| qs.model._meta.verbose_name_plural, 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 '%s' given format '%s'" % (datestr, 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 essance 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 weather 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 consistancy'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} |
| |