Fixed #26348 -- Added TruncTime and exposed it through the __time lookup.
Thanks Tim for the review.
This commit is contained in:
parent
082c52dbed
commit
8a4f017f45
|
@ -96,6 +96,12 @@ class BaseDatabaseOperations(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_cast_date() method')
|
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):
|
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||||
"""
|
"""
|
||||||
Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
|
Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
|
||||||
|
|
|
@ -51,6 +51,11 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
sql = "DATE(%s)" % field_name
|
sql = "DATE(%s)" % field_name
|
||||||
return sql, params
|
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):
|
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||||
field_name, params = self._convert_field_to_tz(field_name, tzname)
|
field_name, params = self._convert_field_to_tz(field_name, tzname)
|
||||||
sql = self.date_extract_sql(lookup_type, field_name)
|
sql = self.date_extract_sql(lookup_type, field_name)
|
||||||
|
|
|
@ -128,6 +128,12 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
sql = 'TRUNC(%s)' % field_name
|
sql = 'TRUNC(%s)' % field_name
|
||||||
return sql, []
|
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):
|
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||||
field_name = self._convert_field_to_tz(field_name, tzname)
|
field_name = self._convert_field_to_tz(field_name, tzname)
|
||||||
sql = self.date_extract_sql(lookup_type, field_name)
|
sql = self.date_extract_sql(lookup_type, field_name)
|
||||||
|
|
|
@ -45,6 +45,11 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
sql = '(%s)::date' % field_name
|
sql = '(%s)::date' % field_name
|
||||||
return sql, params
|
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):
|
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||||
field_name, params = self._convert_field_to_tz(field_name, tzname)
|
field_name, params = self._convert_field_to_tz(field_name, tzname)
|
||||||
sql = self.date_extract_sql(lookup_type, field_name)
|
sql = self.date_extract_sql(lookup_type, field_name)
|
||||||
|
|
|
@ -210,6 +210,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
conn.create_function("django_date_extract", 2, _sqlite_date_extract)
|
conn.create_function("django_date_extract", 2, _sqlite_date_extract)
|
||||||
conn.create_function("django_date_trunc", 2, _sqlite_date_trunc)
|
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_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_extract", 3, _sqlite_datetime_extract)
|
||||||
conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc)
|
conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc)
|
||||||
conn.create_function("django_time_extract", 2, _sqlite_time_extract)
|
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()
|
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):
|
def _sqlite_datetime_extract(lookup_type, dt, tzname):
|
||||||
dt = _sqlite_datetime_parse(dt, tzname)
|
dt = _sqlite_datetime_parse(dt, tzname)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
|
|
|
@ -85,6 +85,10 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
self._require_pytz()
|
self._require_pytz()
|
||||||
return "django_datetime_cast_date(%s, %%s)" % field_name, [tzname]
|
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):
|
def datetime_extract_sql(self, lookup_type, field_name, tzname):
|
||||||
# Same comment as in date_extract_sql.
|
# Same comment as in date_extract_sql.
|
||||||
self._require_pytz()
|
self._require_pytz()
|
||||||
|
|
|
@ -5,7 +5,7 @@ from .base import (
|
||||||
from .datetime import (
|
from .datetime import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
|
ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
|
||||||
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
|
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -16,5 +16,5 @@ __all__ = [
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
|
'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
|
||||||
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
||||||
'TruncSecond', 'TruncYear',
|
'TruncSecond', 'TruncTime', 'TruncYear',
|
||||||
]
|
]
|
||||||
|
|
|
@ -239,6 +239,23 @@ class TruncDate(TruncBase):
|
||||||
return sql, lhs_params
|
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):
|
class TruncHour(TruncBase):
|
||||||
kind = 'hour'
|
kind = 'hour'
|
||||||
|
|
||||||
|
@ -252,3 +269,4 @@ class TruncSecond(TruncBase):
|
||||||
|
|
||||||
|
|
||||||
DateTimeField.register_lookup(TruncDate)
|
DateTimeField.register_lookup(TruncDate)
|
||||||
|
DateTimeField.register_lookup(TruncTime)
|
||||||
|
|
|
@ -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
|
truncate function. It's also registered as a transform on ``DateTimeField`` as
|
||||||
``__date``.
|
``__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)
|
.. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
|
||||||
|
|
||||||
.. attribute:: kind = 'day'
|
.. attribute:: kind = 'day'
|
||||||
|
|
|
@ -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
|
current time zone before filtering. This requires :ref:`time zone definitions
|
||||||
in the database <database-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
|
.. fieldlookup:: hour
|
||||||
|
|
||||||
``hour``
|
``hour``
|
||||||
|
|
|
@ -201,6 +201,10 @@ Models
|
||||||
* Added support for time truncation to
|
* Added support for time truncation to
|
||||||
:class:`~django.db.models.functions.datetime.Trunc` functions.
|
: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
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -273,6 +277,10 @@ Database backend API
|
||||||
``lookup_type`` argument can be either ``'hour'``, ``'minute'``, or
|
``lookup_type`` argument can be either ``'hour'``, ``'minute'``, or
|
||||||
``'second'``.
|
``'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
|
Dropped support for PostgreSQL 9.2 and PostGIS 2.0
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.db.models import DateField, DateTimeField, IntegerField, TimeField
|
||||||
from django.db.models.functions import (
|
from django.db.models.functions import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
|
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.test import TestCase, override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -512,6 +512,30 @@ class DateFunctionTests(TestCase):
|
||||||
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"):
|
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"):
|
||||||
list(DTModel.objects.annotate(truncated=TruncDate('start_time', output_field=TimeField())))
|
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):
|
def test_trunc_day_func(self):
|
||||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
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')
|
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'day')
|
||||||
|
|
Loading…
Reference in New Issue