Browse Source

Fixed #29754 -- Added is_dst parameter to Trunc database functions.

ahbk 6 years ago
parent
commit
d527639804

+ 8 - 4
django/db/models/functions/datetime.py

@@ -170,8 +170,9 @@ class TruncBase(TimezoneMixin, Transform):
     kind = None
     tzinfo = None
 
-    def __init__(self, expression, output_field=None, tzinfo=None, **extra):
+    def __init__(self, expression, output_field=None, tzinfo=None, is_dst=None, **extra):
         self.tzinfo = tzinfo
+        self.is_dst = is_dst
         super().__init__(expression, output_field=output_field, **extra)
 
     def as_sql(self, compiler, connection):
@@ -222,7 +223,7 @@ class TruncBase(TimezoneMixin, Transform):
                 pass
             elif value is not None:
                 value = value.replace(tzinfo=None)
-                value = timezone.make_aware(value, self.tzinfo)
+                value = timezone.make_aware(value, self.tzinfo, is_dst=self.is_dst)
             elif not connection.features.has_zoneinfo_database:
                 raise ValueError(
                     'Database returned an invalid datetime value. Are time '
@@ -240,9 +241,12 @@ class TruncBase(TimezoneMixin, Transform):
 
 class Trunc(TruncBase):
 
-    def __init__(self, expression, kind, output_field=None, tzinfo=None, **extra):
+    def __init__(self, expression, kind, output_field=None, tzinfo=None, is_dst=None, **extra):
         self.kind = kind
-        super().__init__(expression, output_field=output_field, tzinfo=tzinfo, **extra)
+        super().__init__(
+            expression, output_field=output_field, tzinfo=tzinfo,
+            is_dst=is_dst, **extra
+        )
 
 
 class TruncYear(TruncBase):

+ 20 - 12
docs/ref/models/database-functions.txt

@@ -442,7 +442,7 @@ Usage example::
 ``Trunc``
 ---------
 
-.. class:: Trunc(expression, kind, output_field=None, tzinfo=None, **extra)
+.. class:: Trunc(expression, kind, output_field=None, tzinfo=None, is_dst=None, **extra)
 
 Truncates a date up to a significant component.
 
@@ -460,6 +460,14 @@ value. If ``output_field`` is omitted, it will default to the ``output_field``
 of ``expression``. A ``tzinfo`` subclass, usually provided by ``pytz``, can be
 passed to truncate a value in a specific timezone.
 
+The ``is_dst`` parameter 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.0
+
+    The ``is_dst`` parameter was added.
+
 Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s
 return:
 
@@ -525,21 +533,21 @@ Usage example::
 ``DateField`` truncation
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. class:: TruncYear(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncYear(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'year'
 
-.. class:: TruncMonth(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncMonth(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'month'
 
-.. class:: TruncWeek(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncWeek(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     Truncates to midnight on the Monday of the week.
 
     .. attribute:: kind = 'week'
 
-.. class:: TruncQuarter(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncQuarter(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'quarter'
 
@@ -603,19 +611,19 @@ truncate function. It's also registered as a transform on  ``DateTimeField`` as
 truncate function. It's also registered as a transform on ``DateTimeField`` as
 ``__time``.
 
-.. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncDay(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'day'
 
-.. class:: TruncHour(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncHour(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'hour'
 
-.. class:: TruncMinute(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncMinute(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'minute'
 
-.. class:: TruncSecond(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncSecond(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'second'
 
@@ -653,15 +661,15 @@ Usage example::
 ``TimeField`` truncation
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. class:: TruncHour(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncHour(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'hour'
 
-.. class:: TruncMinute(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncMinute(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'minute'
 
-.. class:: TruncSecond(expression, output_field=None, tzinfo=None, **extra)
+.. class:: TruncSecond(expression, output_field=None, tzinfo=None, is_dst=None, **extra)
 
     .. attribute:: kind = 'second'
 

+ 4 - 0
docs/releases/3.0.txt

@@ -164,6 +164,10 @@ Models
 
 * Added the :class:`~django.db.models.functions.MD5` database function.
 
+* The new ``is_dst``  parameter of the
+  :class:`~django.db.models.functions.Trunc` database functions determines the
+  treatment of nonexistent and ambiguous datetimes.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 24 - 0
tests/db_functions/datetime/test_extract_trunc.py

@@ -1044,6 +1044,30 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         self.assertEqual(model.melb_year.year, 2016)
         self.assertEqual(model.pacific_year.year, 2015)
 
+    def test_trunc_ambiguous_and_invalid_times(self):
+        sao = pytz.timezone('America/Sao_Paulo')
+        utc = pytz.timezone('UTC')
+        start_datetime = utc.localize(datetime(2016, 10, 16, 13))
+        end_datetime = utc.localize(datetime(2016, 2, 21, 1))
+        self.create_model(start_datetime, end_datetime)
+        with timezone.override(sao):
+            with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'):
+                model = DTModel.objects.annotate(truncated_start=TruncDay('start_datetime')).get()
+            with self.assertRaisesMessage(pytz.AmbiguousTimeError, '2016-02-20 23:00:00'):
+                model = DTModel.objects.annotate(truncated_end=TruncHour('end_datetime')).get()
+            model = DTModel.objects.annotate(
+                truncated_start=TruncDay('start_datetime', is_dst=False),
+                truncated_end=TruncHour('end_datetime', is_dst=False),
+            ).get()
+            self.assertEqual(model.truncated_start.dst(), timedelta(0))
+            self.assertEqual(model.truncated_end.dst(), timedelta(0))
+            model = DTModel.objects.annotate(
+                truncated_start=TruncDay('start_datetime', is_dst=True),
+                truncated_end=TruncHour('end_datetime', is_dst=True),
+            ).get()
+            self.assertEqual(model.truncated_start.dst(), timedelta(0, 3600))
+            self.assertEqual(model.truncated_end.dst(), timedelta(0, 3600))
+
     def test_trunc_func_with_timezone(self):
         """
         If the truncated datetime transitions to a different offset (daylight