瀏覽代碼

Fixed #26348 -- Added TruncTime and exposed it through the __time lookup.

Thanks Tim for the review.
Simon Charette 8 年之前
父節點
當前提交
8a4f017f45

+ 6 - 0
django/db/backends/base/operations.py

@@ -96,6 +96,12 @@ class BaseDatabaseOperations(object):
         """
         raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_cast_date() method')
 
+    def datetime_cast_time_sql(self, field_name, tzname):
+        """
+        Returns the SQL necessary to cast a datetime value to time value.
+        """
+        raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_cast_time_sql() method')
+
     def datetime_extract_sql(self, lookup_type, field_name, tzname):
         """
         Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or

+ 5 - 0
django/db/backends/mysql/operations.py

@@ -51,6 +51,11 @@ class DatabaseOperations(BaseDatabaseOperations):
         sql = "DATE(%s)" % field_name
         return sql, params
 
+    def datetime_cast_time_sql(self, field_name, tzname):
+        field_name, params = self._convert_field_to_tz(field_name, tzname)
+        sql = "TIME(%s)" % field_name
+        return sql, params
+
     def datetime_extract_sql(self, lookup_type, field_name, tzname):
         field_name, params = self._convert_field_to_tz(field_name, tzname)
         sql = self.date_extract_sql(lookup_type, field_name)

+ 6 - 0
django/db/backends/oracle/operations.py

@@ -128,6 +128,12 @@ WHEN (new.%(col_name)s IS NULL)
         sql = 'TRUNC(%s)' % field_name
         return sql, []
 
+    def datetime_cast_time_sql(self, field_name, tzname):
+        # Since `TimeField` values are stored as TIMESTAMP where only the date
+        # part is ignored, convert the field to the specified timezone.
+        field_name = self._convert_field_to_tz(field_name, tzname)
+        return field_name, []
+
     def datetime_extract_sql(self, lookup_type, field_name, tzname):
         field_name = self._convert_field_to_tz(field_name, tzname)
         sql = self.date_extract_sql(lookup_type, field_name)

+ 5 - 0
django/db/backends/postgresql/operations.py

@@ -45,6 +45,11 @@ class DatabaseOperations(BaseDatabaseOperations):
         sql = '(%s)::date' % field_name
         return sql, params
 
+    def datetime_cast_time_sql(self, field_name, tzname):
+        field_name, params = self._convert_field_to_tz(field_name, tzname)
+        sql = '(%s)::time' % field_name
+        return sql, params
+
     def datetime_extract_sql(self, lookup_type, field_name, tzname):
         field_name, params = self._convert_field_to_tz(field_name, tzname)
         sql = self.date_extract_sql(lookup_type, field_name)

+ 8 - 0
django/db/backends/sqlite3/base.py

@@ -210,6 +210,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         conn.create_function("django_date_extract", 2, _sqlite_date_extract)
         conn.create_function("django_date_trunc", 2, _sqlite_date_trunc)
         conn.create_function("django_datetime_cast_date", 2, _sqlite_datetime_cast_date)
+        conn.create_function("django_datetime_cast_time", 2, _sqlite_datetime_cast_time)
         conn.create_function("django_datetime_extract", 3, _sqlite_datetime_extract)
         conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc)
         conn.create_function("django_time_extract", 2, _sqlite_time_extract)
@@ -403,6 +404,13 @@ def _sqlite_datetime_cast_date(dt, tzname):
     return dt.date().isoformat()
 
 
+def _sqlite_datetime_cast_time(dt, tzname):
+    dt = _sqlite_datetime_parse(dt, tzname)
+    if dt is None:
+        return None
+    return dt.time().isoformat()
+
+
 def _sqlite_datetime_extract(lookup_type, dt, tzname):
     dt = _sqlite_datetime_parse(dt, tzname)
     if dt is None:

+ 4 - 0
django/db/backends/sqlite3/operations.py

@@ -85,6 +85,10 @@ class DatabaseOperations(BaseDatabaseOperations):
         self._require_pytz()
         return "django_datetime_cast_date(%s, %%s)" % field_name, [tzname]
 
+    def datetime_cast_time_sql(self, field_name, tzname):
+        self._require_pytz()
+        return "django_datetime_cast_time(%s, %%s)" % field_name, [tzname]
+
     def datetime_extract_sql(self, lookup_type, field_name, tzname):
         # Same comment as in date_extract_sql.
         self._require_pytz()

+ 2 - 2
django/db/models/functions/__init__.py

@@ -5,7 +5,7 @@ from .base import (
 from .datetime import (
     Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
     ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
-    TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
+    TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
 )
 
 __all__ = [
@@ -16,5 +16,5 @@ __all__ = [
     'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
     'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
     'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
-    'TruncSecond', 'TruncYear',
+    'TruncSecond', 'TruncTime', 'TruncYear',
 ]

+ 18 - 0
django/db/models/functions/datetime.py

@@ -239,6 +239,23 @@ class TruncDate(TruncBase):
         return sql, lhs_params
 
 
+class TruncTime(TruncBase):
+    kind = 'time'
+    lookup_name = 'time'
+
+    @cached_property
+    def output_field(self):
+        return TimeField()
+
+    def as_sql(self, compiler, connection):
+        # Cast to date rather than truncate to date.
+        lhs, lhs_params = compiler.compile(self.lhs)
+        tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
+        sql, tz_params = connection.ops.datetime_cast_time_sql(lhs, tzname)
+        lhs_params.extend(tz_params)
+        return sql, lhs_params
+
+
 class TruncHour(TruncBase):
     kind = 'hour'
 
@@ -252,3 +269,4 @@ class TruncSecond(TruncBase):
 
 
 DateTimeField.register_lookup(TruncDate)
+DateTimeField.register_lookup(TruncTime)

+ 11 - 0
docs/ref/models/database-functions.txt

@@ -686,6 +686,17 @@ that deal with time-parts can be used with ``TimeField``::
 truncate function. It's also registered as a transform on  ``DateTimeField`` as
 ``__date``.
 
+.. class:: TruncTime(expression, **extra)
+
+.. versionadded:: 1.11
+
+    .. attribute:: lookup_name = 'time'
+    .. attribute:: output_field = TimeField()
+
+``TruncTime`` casts ``expression`` to a time rather than using the built-in SQL
+truncate function. It's also registered as a transform on ``DateTimeField`` as
+``__time``.
+
 .. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
 
     .. attribute:: kind = 'day'

+ 21 - 0
docs/ref/models/querysets.txt

@@ -2674,6 +2674,27 @@ When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
 current time zone before filtering. This requires :ref:`time zone definitions
 in the database <database-time-zone-definitions>`.
 
+.. fieldlookup:: time
+
+``time``
+~~~~~~~~
+
+.. versionadded:: 1.11
+
+For datetime fields, casts the value as time. Allows chaining additional field
+lookups. Takes a :class:`datetime.time` value.
+
+Example::
+
+    Entry.objects.filter(pub_date__time=datetime.time(14, 30))
+    Entry.objects.filter(pub_date__time__between=(datetime.time(8), datetime.time(17)))
+
+(No equivalent SQL code fragment is included for this lookup because
+implementation of the relevant query varies among different database engines.)
+
+When :setting:`USE_TZ` is ``True``, fields are converted to the current time
+zone before filtering.
+
 .. fieldlookup:: hour
 
 ``hour``

+ 8 - 0
docs/releases/1.11.txt

@@ -201,6 +201,10 @@ Models
 * Added support for time truncation to
   :class:`~django.db.models.functions.datetime.Trunc` functions.
 
+* Added the :class:`~django.db.models.functions.datetime.TruncTime` function
+  to truncate :class:`~django.db.models.DateTimeField` to its time component
+  and exposed it through the :lookup:`time` lookup.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -273,6 +277,10 @@ Database backend API
   ``lookup_type`` argument can be either ``'hour'``, ``'minute'``, or
   ``'second'``.
 
+* The ``DatabaseOperations.datetime_cast_time_sql()`` method is added to
+  support the :lookup:`time` lookup. It accepts a ``field_name`` and ``tzname``
+  arguments and returns the SQL necessary to cast a datetime value to time value.
+
 Dropped support for PostgreSQL 9.2 and PostGIS 2.0
 --------------------------------------------------
 

+ 25 - 1
tests/db_functions/test_datetime.py

@@ -9,7 +9,7 @@ from django.db.models import DateField, DateTimeField, IntegerField, TimeField
 from django.db.models.functions import (
     Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
     ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
-    TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
+    TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
 )
 from django.test import TestCase, override_settings
 from django.utils import timezone
@@ -512,6 +512,30 @@ class DateFunctionTests(TestCase):
         with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"):
             list(DTModel.objects.annotate(truncated=TruncDate('start_time', output_field=TimeField())))
 
+    def test_trunc_time_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncTime('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, start_datetime.time()),
+                (end_datetime, end_datetime.time()),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__time=TruncTime('start_datetime')).count(), 2)
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to TimeField"):
+            list(DTModel.objects.annotate(truncated=TruncTime('start_date')))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to TimeField"):
+            list(DTModel.objects.annotate(truncated=TruncTime('start_date', output_field=DateField())))
+
     def test_trunc_day_func(self):
         start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
         end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'day')