Loading django/views/generic/dates.py +83 −47 Original line number Diff line number Diff line import datetime from django.conf import settings 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.functional import cached_property from django.utils.translation import ugettext as _ from django.utils import timezone from django.views.generic.base import View Loading Loading @@ -164,6 +166,51 @@ class DateMixin(object): """ 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, timezone.get_current_timezone()) 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): """ Loading @@ -180,7 +227,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): def get_dated_items(self): """ Obtain the list of dates and itesm Obtain the list of dates and items. """ raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') Loading @@ -196,7 +243,8 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): paginate_by = self.get_paginate_by(qs) if not allow_future: qs = qs.filter(**{'%s__lte' % date_field: timezone.now()}) now = timezone.now() if self.uses_datetime_field else datetime.date.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 Loading Loading @@ -225,6 +273,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): return date_list class BaseArchiveIndexView(BaseDateListView): """ Base class for archives of date-based items. Loading Loading @@ -265,11 +314,19 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): """ 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 = _date_from_string(year, self.get_year_format()) since = self._make_date_lookup_arg(date) until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) 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, 'month') if self.get_make_object_list(): Loading Loading @@ -312,14 +369,14 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): month, self.get_month_format()) # Construct a date-range lookup. first_day = date.replace(day=1) if first_day.month == 12: last_day = first_day.replace(year=first_day.year + 1, month=1) since = self._make_date_lookup_arg(date) if date.month == 12: until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) else: last_day = first_day.replace(month=first_day.month + 1) until = self._make_date_lookup_arg(datetime.date(date.year, date.month + 1, 1)) lookup_kwargs = { '%s__gte' % date_field: first_day, '%s__lt' % date_field: last_day, '%s__gte' % date_field: since, '%s__lt' % date_field: until, } qs = self.get_dated_queryset(**lookup_kwargs) Loading Loading @@ -362,11 +419,11 @@ class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView): week, week_format) # Construct a date-range lookup. first_day = date last_day = date + datetime.timedelta(days=7) since = self._make_date_lookup_arg(date) until = self._make_date_lookup_arg(date + datetime.timedelta(days=7)) lookup_kwargs = { '%s__gte' % date_field: first_day, '%s__lt' % date_field: last_day, '%s__gte' % date_field: since, '%s__lt' % date_field: until, } qs = self.get_dated_queryset(**lookup_kwargs) Loading Loading @@ -404,11 +461,7 @@ class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView): 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) lookup_kwargs = self._make_single_date_lookup(date) qs = self.get_dated_queryset(**lookup_kwargs) return (None, qs, { Loading Loading @@ -474,10 +527,8 @@ class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailV # 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) lookup_kwargs = self._make_single_date_lookup(date) qs = qs.filter(**lookup_kwargs) return super(BaseDetailView, self).get_object(queryset=qs) Loading @@ -490,10 +541,10 @@ class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView): template_name_suffix = '_detail' def _date_from_string(year, year_format, month, month_format, day='', day_format='', delim='__'): 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. month, and day (only year is mandatory). Raise a 404 for an invalid date. """ format = delim.join((year_format, month_format, day_format)) datestr = delim.join((year, month, day)) Loading Loading @@ -548,10 +599,10 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) # 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} lookup = {'%s__lte' % date_field: generic_view._make_date_lookup_arg(naive_result)} ordering = '-%s' % date_field else: lookup = {'%s__gte' % date_field: naive_result} lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(naive_result)} ordering = date_field qs = generic_view.get_queryset().filter(**lookup).order_by(ordering) Loading @@ -564,7 +615,9 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) result = None # Convert datetimes to a dates if hasattr(result, 'date'): if result and generic_view.uses_datetime_field: if settings.USE_TZ: result = timezone.localtime(result) result = result.date() # For month views, we always want to have a date that's the first of the Loading @@ -577,20 +630,3 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) 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} docs/ref/class-based-views.txt +6 −0 Original line number Diff line number Diff line Loading @@ -748,6 +748,12 @@ DateMixin ``QuerySet``'s model that the date-based archive should use to determine the objects on the page. When :doc:`time zone support </topics/i18n/timezones>` is enabled and ``date_field`` is a ``DateTimeField``, dates are assumed to be in the current time zone. As a consequence, if you have implemented per-user time zone selection, users living in different time zones may view a different set of objects at the same URL. .. attribute:: allow_future A boolean specifying whether to include "future" objects on this page, Loading tests/regressiontests/generic_views/dates.py +101 −1 Original line number Diff line number Diff line Loading @@ -4,8 +4,18 @@ import datetime from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from .models import Book, BookSigning import warnings warnings.filterwarnings( 'error', r"DateTimeField received a naive datetime", RuntimeWarning, r'django\.db\.models\.fields') from .models import Book class ArchiveIndexViewTests(TestCase): Loading Loading @@ -88,6 +98,18 @@ class ArchiveIndexViewTests(TestCase): with self.assertNumQueries(3): self.client.get('/dates/books/paginated/') def test_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) class YearArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -141,6 +163,18 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/books/no_year/') self.assertEqual(res.status_code, 404) def test_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) class MonthArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -245,6 +279,21 @@ class MonthArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['previous_month'], datetime.date(2010,9,1)) def test_datetime_month_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 2, 1, 12, 0)) BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) BookSigning.objects.create(event_date=datetime.datetime(2008, 6, 3, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_month_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 2, 1, 12, 0, tzinfo=timezone.utc)) BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) BookSigning.objects.create(event_date=datetime.datetime(2008, 6, 3, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) class WeekArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] Loading Loading @@ -300,6 +349,18 @@ class WeekArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['week'], datetime.date(2008, 9, 29)) def test_datetime_week_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/week/13/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_week_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/week/13/') self.assertEqual(res.status_code, 200) class DayArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -388,6 +449,26 @@ class DayArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['day'], datetime.date.today()) def test_datetime_day_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_day_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) # 2008-04-02T00:00:00+03:00 (beginning of day) > 2008-04-01T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 1, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) # 2008-04-03T00:00:00+03:00 (end of day) > 2008-04-02T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 2, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 404) class DateDetailViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -441,3 +522,22 @@ class DateDetailViewTests(TestCase): res = self.client.get( '/dates/books/get_object_custom_queryset/2008/oct/01/1/') self.assertEqual(res.status_code, 404) def test_datetime_date_detail(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_date_detail(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) # 2008-04-02T00:00:00+03:00 (beginning of day) > 2008-04-01T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 1, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) # 2008-04-03T00:00:00+03:00 (end of day) > 2008-04-02T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 2, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 404) tests/regressiontests/generic_views/models.py +3 −0 Original line number Diff line number Diff line Loading @@ -42,3 +42,6 @@ class Book(models.Model): class Page(models.Model): content = models.TextField() template = models.CharField(max_length=300) class BookSigning(models.Model): event_date = models.DateTimeField() tests/regressiontests/generic_views/urls.py +17 −2 Original line number Diff line number Diff line Loading @@ -108,6 +108,8 @@ urlpatterns = patterns('', views.BookArchive.as_view(queryset=None)), (r'^dates/books/paginated/$', views.BookArchive.as_view(paginate_by=10)), (r'^dates/booksignings/$', views.BookSigningArchive.as_view()), # ListView (r'^list/dict/$', Loading Loading @@ -156,6 +158,8 @@ urlpatterns = patterns('', views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)), (r'^dates/books/no_year/$', views.BookYearArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/$', views.BookSigningYearArchive.as_view()), # MonthArchiveView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', Loading @@ -170,6 +174,8 @@ urlpatterns = patterns('', views.BookMonthArchive.as_view(paginate_by=30)), (r'^dates/books/(?P<year>\d{4})/no_month/$', views.BookMonthArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', views.BookSigningMonthArchive.as_view()), # WeekArchiveView (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/$', Loading @@ -184,6 +190,8 @@ urlpatterns = patterns('', views.BookWeekArchive.as_view()), (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/monday/$', views.BookWeekArchive.as_view(week_format='%W')), (r'^dates/booksignings/(?P<year>\d{4})/week/(?P<week>\d{1,2})/$', views.BookSigningWeekArchive.as_view()), # DayArchiveView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$', Loading @@ -198,12 +206,16 @@ urlpatterns = patterns('', views.BookDayArchive.as_view(paginate_by=True)), (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/no_day/$', views.BookDayArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$', views.BookSigningDayArchive.as_view()), # TodayArchiveView (r'dates/books/today/$', (r'^dates/books/today/$', views.BookTodayArchive.as_view()), (r'dates/books/today/allow_empty/$', (r'^dates/books/today/allow_empty/$', views.BookTodayArchive.as_view(allow_empty=True)), (r'^dates/booksignings/today/$', views.BookSigningTodayArchive.as_view()), # DateDetailView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', Loading @@ -221,6 +233,9 @@ urlpatterns = patterns('', (r'^dates/books/get_object_custom_queryset/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', views.BookDetailGetObjectCustomQueryset.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', views.BookSigningDetail.as_view()), # Useful for testing redirects (r'^accounts/login/$', 'django.contrib.auth.views.login') ) Loading
django/views/generic/dates.py +83 −47 Original line number Diff line number Diff line import datetime from django.conf import settings 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.functional import cached_property from django.utils.translation import ugettext as _ from django.utils import timezone from django.views.generic.base import View Loading Loading @@ -164,6 +166,51 @@ class DateMixin(object): """ 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, timezone.get_current_timezone()) 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): """ Loading @@ -180,7 +227,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): def get_dated_items(self): """ Obtain the list of dates and itesm Obtain the list of dates and items. """ raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') Loading @@ -196,7 +243,8 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): paginate_by = self.get_paginate_by(qs) if not allow_future: qs = qs.filter(**{'%s__lte' % date_field: timezone.now()}) now = timezone.now() if self.uses_datetime_field else datetime.date.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 Loading Loading @@ -225,6 +273,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): return date_list class BaseArchiveIndexView(BaseDateListView): """ Base class for archives of date-based items. Loading Loading @@ -265,11 +314,19 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): """ 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 = _date_from_string(year, self.get_year_format()) since = self._make_date_lookup_arg(date) until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) 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, 'month') if self.get_make_object_list(): Loading Loading @@ -312,14 +369,14 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): month, self.get_month_format()) # Construct a date-range lookup. first_day = date.replace(day=1) if first_day.month == 12: last_day = first_day.replace(year=first_day.year + 1, month=1) since = self._make_date_lookup_arg(date) if date.month == 12: until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) else: last_day = first_day.replace(month=first_day.month + 1) until = self._make_date_lookup_arg(datetime.date(date.year, date.month + 1, 1)) lookup_kwargs = { '%s__gte' % date_field: first_day, '%s__lt' % date_field: last_day, '%s__gte' % date_field: since, '%s__lt' % date_field: until, } qs = self.get_dated_queryset(**lookup_kwargs) Loading Loading @@ -362,11 +419,11 @@ class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView): week, week_format) # Construct a date-range lookup. first_day = date last_day = date + datetime.timedelta(days=7) since = self._make_date_lookup_arg(date) until = self._make_date_lookup_arg(date + datetime.timedelta(days=7)) lookup_kwargs = { '%s__gte' % date_field: first_day, '%s__lt' % date_field: last_day, '%s__gte' % date_field: since, '%s__lt' % date_field: until, } qs = self.get_dated_queryset(**lookup_kwargs) Loading Loading @@ -404,11 +461,7 @@ class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView): 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) lookup_kwargs = self._make_single_date_lookup(date) qs = self.get_dated_queryset(**lookup_kwargs) return (None, qs, { Loading Loading @@ -474,10 +527,8 @@ class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailV # 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) lookup_kwargs = self._make_single_date_lookup(date) qs = qs.filter(**lookup_kwargs) return super(BaseDetailView, self).get_object(queryset=qs) Loading @@ -490,10 +541,10 @@ class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView): template_name_suffix = '_detail' def _date_from_string(year, year_format, month, month_format, day='', day_format='', delim='__'): 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. month, and day (only year is mandatory). Raise a 404 for an invalid date. """ format = delim.join((year_format, month_format, day_format)) datestr = delim.join((year, month, day)) Loading Loading @@ -548,10 +599,10 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) # 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} lookup = {'%s__lte' % date_field: generic_view._make_date_lookup_arg(naive_result)} ordering = '-%s' % date_field else: lookup = {'%s__gte' % date_field: naive_result} lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(naive_result)} ordering = date_field qs = generic_view.get_queryset().filter(**lookup).order_by(ordering) Loading @@ -564,7 +615,9 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) result = None # Convert datetimes to a dates if hasattr(result, 'date'): if result and generic_view.uses_datetime_field: if settings.USE_TZ: result = timezone.localtime(result) result = result.date() # For month views, we always want to have a date that's the first of the Loading @@ -577,20 +630,3 @@ def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day) 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}
docs/ref/class-based-views.txt +6 −0 Original line number Diff line number Diff line Loading @@ -748,6 +748,12 @@ DateMixin ``QuerySet``'s model that the date-based archive should use to determine the objects on the page. When :doc:`time zone support </topics/i18n/timezones>` is enabled and ``date_field`` is a ``DateTimeField``, dates are assumed to be in the current time zone. As a consequence, if you have implemented per-user time zone selection, users living in different time zones may view a different set of objects at the same URL. .. attribute:: allow_future A boolean specifying whether to include "future" objects on this page, Loading
tests/regressiontests/generic_views/dates.py +101 −1 Original line number Diff line number Diff line Loading @@ -4,8 +4,18 @@ import datetime from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from .models import Book, BookSigning import warnings warnings.filterwarnings( 'error', r"DateTimeField received a naive datetime", RuntimeWarning, r'django\.db\.models\.fields') from .models import Book class ArchiveIndexViewTests(TestCase): Loading Loading @@ -88,6 +98,18 @@ class ArchiveIndexViewTests(TestCase): with self.assertNumQueries(3): self.client.get('/dates/books/paginated/') def test_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) class YearArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -141,6 +163,18 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/books/no_year/') self.assertEqual(res.status_code, 404) def test_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) class MonthArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -245,6 +279,21 @@ class MonthArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['previous_month'], datetime.date(2010,9,1)) def test_datetime_month_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 2, 1, 12, 0)) BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) BookSigning.objects.create(event_date=datetime.datetime(2008, 6, 3, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_month_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 2, 1, 12, 0, tzinfo=timezone.utc)) BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) BookSigning.objects.create(event_date=datetime.datetime(2008, 6, 3, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) class WeekArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] Loading Loading @@ -300,6 +349,18 @@ class WeekArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['week'], datetime.date(2008, 9, 29)) def test_datetime_week_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/week/13/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_week_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/week/13/') self.assertEqual(res.status_code, 200) class DayArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -388,6 +449,26 @@ class DayArchiveViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(res.context['day'], datetime.date.today()) def test_datetime_day_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_day_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) # 2008-04-02T00:00:00+03:00 (beginning of day) > 2008-04-01T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 1, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) # 2008-04-03T00:00:00+03:00 (end of day) > 2008-04-02T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 2, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 404) class DateDetailViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' Loading Loading @@ -441,3 +522,22 @@ class DateDetailViewTests(TestCase): res = self.client.get( '/dates/books/get_object_custom_queryset/2008/oct/01/1/') self.assertEqual(res.status_code, 404) def test_datetime_date_detail(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_date_detail(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) # 2008-04-02T00:00:00+03:00 (beginning of day) > 2008-04-01T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 1, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 200) # 2008-04-03T00:00:00+03:00 (end of day) > 2008-04-02T22:00:00+00:00 (book signing event date) BookSigning.objects.filter(pk=1).update(event_date=datetime.datetime(2008, 4, 2, 22, 0, tzinfo=timezone.utc)) res = self.client.get('/dates/booksignings/2008/apr/2/1/') self.assertEqual(res.status_code, 404)
tests/regressiontests/generic_views/models.py +3 −0 Original line number Diff line number Diff line Loading @@ -42,3 +42,6 @@ class Book(models.Model): class Page(models.Model): content = models.TextField() template = models.CharField(max_length=300) class BookSigning(models.Model): event_date = models.DateTimeField()
tests/regressiontests/generic_views/urls.py +17 −2 Original line number Diff line number Diff line Loading @@ -108,6 +108,8 @@ urlpatterns = patterns('', views.BookArchive.as_view(queryset=None)), (r'^dates/books/paginated/$', views.BookArchive.as_view(paginate_by=10)), (r'^dates/booksignings/$', views.BookSigningArchive.as_view()), # ListView (r'^list/dict/$', Loading Loading @@ -156,6 +158,8 @@ urlpatterns = patterns('', views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)), (r'^dates/books/no_year/$', views.BookYearArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/$', views.BookSigningYearArchive.as_view()), # MonthArchiveView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', Loading @@ -170,6 +174,8 @@ urlpatterns = patterns('', views.BookMonthArchive.as_view(paginate_by=30)), (r'^dates/books/(?P<year>\d{4})/no_month/$', views.BookMonthArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/$', views.BookSigningMonthArchive.as_view()), # WeekArchiveView (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/$', Loading @@ -184,6 +190,8 @@ urlpatterns = patterns('', views.BookWeekArchive.as_view()), (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/monday/$', views.BookWeekArchive.as_view(week_format='%W')), (r'^dates/booksignings/(?P<year>\d{4})/week/(?P<week>\d{1,2})/$', views.BookSigningWeekArchive.as_view()), # DayArchiveView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$', Loading @@ -198,12 +206,16 @@ urlpatterns = patterns('', views.BookDayArchive.as_view(paginate_by=True)), (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/no_day/$', views.BookDayArchive.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$', views.BookSigningDayArchive.as_view()), # TodayArchiveView (r'dates/books/today/$', (r'^dates/books/today/$', views.BookTodayArchive.as_view()), (r'dates/books/today/allow_empty/$', (r'^dates/books/today/allow_empty/$', views.BookTodayArchive.as_view(allow_empty=True)), (r'^dates/booksignings/today/$', views.BookSigningTodayArchive.as_view()), # DateDetailView (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', Loading @@ -221,6 +233,9 @@ urlpatterns = patterns('', (r'^dates/books/get_object_custom_queryset/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', views.BookDetailGetObjectCustomQueryset.as_view()), (r'^dates/booksignings/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$', views.BookSigningDetail.as_view()), # Useful for testing redirects (r'^accounts/login/$', 'django.contrib.auth.views.login') )