diff --git a/django/db/models/query.py b/django/db/models/query.py index a7c16c4bd8..5fdee3ff37 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -875,7 +875,7 @@ class QuerySet: 'datefield', flat=True ).distinct().filter(plain_field__isnull=False).order_by(('-' if order == 'DESC' else '') + 'datefield') - def datetimes(self, field_name, kind, order='ASC', tzinfo=None): + def datetimes(self, field_name, kind, order='ASC', tzinfo=None, is_dst=None): """ Return a list of datetime objects representing all available datetimes for the given field_name, scoped to 'kind'. @@ -890,7 +890,13 @@ class QuerySet: else: tzinfo = None return self.annotate( - datetimefield=Trunc(field_name, kind, output_field=DateTimeField(), tzinfo=tzinfo), + datetimefield=Trunc( + field_name, + kind, + output_field=DateTimeField(), + tzinfo=tzinfo, + is_dst=is_dst, + ), plain_field=F(field_name) ).values_list( 'datetimefield', flat=True diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 6b042d305e..9647b1a8ff 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -759,7 +759,7 @@ Examples:: ``datetimes()`` ~~~~~~~~~~~~~~~ -.. method:: datetimes(field_name, kind, order='ASC', tzinfo=None) +.. method:: datetimes(field_name, kind, order='ASC', tzinfo=None, is_dst=None) Returns a ``QuerySet`` that evaluates to a list of :class:`datetime.datetime` objects representing all available dates of a particular kind within the @@ -781,6 +781,14 @@ object. If it's ``None``, Django uses the :ref:`current time zone `. It has no effect when :setting:`USE_TZ` is ``False``. +``is_dst`` indicates whether or not ``pytz`` should interpret nonexistent and +ambiguous datetimes in daylight saving time. By default (when ``is_dst=None``), +``pytz`` raises an exception for such datetimes. + +.. versionadded:: 3.1 + + The ``is_dst`` parameter was added. + .. _database-time-zone-definitions: .. note:: diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 77a71bbc43..44ba489a44 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -324,6 +324,9 @@ Models :meth:`~.RelatedManager.set` methods now accept callables as values in the ``through_defaults`` argument. +* The new ``is_dst`` parameter of the :meth:`.QuerySet.datetimes` determines + the treatment of nonexistent and ambiguous datetimes. + Pagination ~~~~~~~~~~ diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py index a9f1199f06..857293acb6 100644 --- a/tests/datetimes/tests.py +++ b/tests/datetimes/tests.py @@ -1,5 +1,7 @@ import datetime +import pytz + from django.test import TestCase, override_settings from django.utils import timezone @@ -89,6 +91,40 @@ class DateTimesTests(TestCase): qs = Article.objects.datetimes('pub_date', 'second') self.assertEqual(qs[0], now) + @override_settings(USE_TZ=True, TIME_ZONE='UTC') + def test_datetimes_ambiguous_and_invalid_times(self): + sao = pytz.timezone('America/Sao_Paulo') + utc = pytz.UTC + article = Article.objects.create( + title='Article 1', + pub_date=utc.localize(datetime.datetime(2016, 2, 21, 1)), + ) + Comment.objects.create( + article=article, + pub_date=utc.localize(datetime.datetime(2016, 10, 16, 13)), + ) + with timezone.override(sao): + with self.assertRaisesMessage(pytz.AmbiguousTimeError, '2016-02-20 23:00:00'): + Article.objects.datetimes('pub_date', 'hour').get() + with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'): + Comment.objects.datetimes('pub_date', 'day').get() + self.assertEqual( + Article.objects.datetimes('pub_date', 'hour', is_dst=False).get().dst(), + datetime.timedelta(0), + ) + self.assertEqual( + Comment.objects.datetimes('pub_date', 'day', is_dst=False).get().dst(), + datetime.timedelta(0), + ) + self.assertEqual( + Article.objects.datetimes('pub_date', 'hour', is_dst=True).get().dst(), + datetime.timedelta(0, 3600), + ) + self.assertEqual( + Comment.objects.datetimes('pub_date', 'hour', is_dst=True).get().dst(), + datetime.timedelta(0, 3600), + ) + def test_datetimes_returns_available_dates_for_given_scope_and_given_field(self): pub_dates = [ datetime.datetime(2005, 7, 28, 12, 15),