Fixed #28650 -- Added TruncWeek database function.
This commit is contained in:
parent
f229049d6c
commit
a455e732a0
|
@ -53,6 +53,10 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
return "MAKEDATE(YEAR(%s), 1) + INTERVAL QUARTER(%s) QUARTER - INTERVAL 1 QUARTER" % (
|
||||
field_name, field_name
|
||||
)
|
||||
elif lookup_type == 'week':
|
||||
return "DATE_SUB(%s, INTERVAL WEEKDAY(%s) DAY)" % (
|
||||
field_name, field_name
|
||||
)
|
||||
else:
|
||||
return "DATE(%s)" % (field_name)
|
||||
|
||||
|
@ -84,6 +88,12 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||
"INTERVAL QUARTER({field_name}) QUARTER - " +
|
||||
"INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
|
||||
).format(field_name=field_name)
|
||||
if lookup_type == 'week':
|
||||
return (
|
||||
"CAST(DATE_FORMAT(DATE_SUB({field_name}, "
|
||||
"INTERVAL WEEKDAY({field_name}) DAY), "
|
||||
"'%%Y-%%m-%%d 00:00:00') AS DATETIME)"
|
||||
).format(field_name=field_name)
|
||||
try:
|
||||
i = fields.index(lookup_type) + 1
|
||||
except ValueError:
|
||||
|
|
|
@ -78,6 +78,8 @@ END;
|
|||
return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
||||
elif lookup_type == 'quarter':
|
||||
return "TRUNC(%s, 'Q')" % field_name
|
||||
elif lookup_type == 'week':
|
||||
return "TRUNC(%s, 'IW')" % field_name
|
||||
else:
|
||||
return "TRUNC(%s)" % field_name
|
||||
|
||||
|
@ -116,6 +118,8 @@ END;
|
|||
sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
||||
elif lookup_type == 'quarter':
|
||||
sql = "TRUNC(%s, 'Q')" % field_name
|
||||
elif lookup_type == 'week':
|
||||
sql = "TRUNC(%s, 'IW')" % field_name
|
||||
elif lookup_type == 'day':
|
||||
sql = "TRUNC(%s)" % field_name
|
||||
elif lookup_type == 'hour':
|
||||
|
|
|
@ -335,6 +335,9 @@ def _sqlite_date_trunc(lookup_type, dt):
|
|||
return '%i-%02i-01' % (dt.year, month_in_quarter)
|
||||
elif lookup_type == 'month':
|
||||
return "%i-%02i-01" % (dt.year, dt.month)
|
||||
elif lookup_type == 'week':
|
||||
dt = dt - datetime.timedelta(days=dt.weekday())
|
||||
return "%i-%02i-%02i" % (dt.year, dt.month, dt.day)
|
||||
elif lookup_type == 'day':
|
||||
return "%i-%02i-%02i" % (dt.year, dt.month, dt.day)
|
||||
|
||||
|
@ -403,6 +406,9 @@ def _sqlite_datetime_trunc(lookup_type, dt, tzname):
|
|||
return '%i-%02i-01 00:00:00' % (dt.year, month_in_quarter)
|
||||
elif lookup_type == 'month':
|
||||
return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
|
||||
elif lookup_type == 'week':
|
||||
dt = dt - datetime.timedelta(days=dt.weekday())
|
||||
return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)
|
||||
elif lookup_type == 'day':
|
||||
return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)
|
||||
elif lookup_type == 'hour':
|
||||
|
|
|
@ -3,7 +3,7 @@ from .datetime import (
|
|||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
||||
Now, Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
||||
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
||||
TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear,
|
||||
)
|
||||
from .text import (
|
||||
Concat, ConcatPair, Length, Lower, Replace, StrIndex, Substr, Upper,
|
||||
|
@ -21,7 +21,7 @@ __all__ = [
|
|||
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
|
||||
'ExtractYear', 'Now', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour',
|
||||
'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime',
|
||||
'TruncYear',
|
||||
'TruncWeek', 'TruncYear',
|
||||
# text
|
||||
'Concat', 'ConcatPair', 'Length', 'Lower', 'Replace', 'StrIndex', 'Substr',
|
||||
'Upper',
|
||||
|
|
|
@ -199,7 +199,8 @@ class TruncBase(TimezoneMixin, Transform):
|
|||
field.name, output_field.__class__.__name__ if has_explicit_output_field else 'DateTimeField'
|
||||
))
|
||||
elif isinstance(field, TimeField) and (
|
||||
isinstance(output_field, DateTimeField) or copy.kind in ('year', 'quarter', 'month', 'day', 'date')):
|
||||
isinstance(output_field, DateTimeField) or
|
||||
copy.kind in ('year', 'quarter', 'month', 'week', 'day', 'date')):
|
||||
raise ValueError("Cannot truncate TimeField '%s' to %s. " % (
|
||||
field.name, output_field.__class__.__name__ if has_explicit_output_field else 'DateTimeField'
|
||||
))
|
||||
|
@ -242,6 +243,11 @@ class TruncMonth(TruncBase):
|
|||
kind = 'month'
|
||||
|
||||
|
||||
class TruncWeek(TruncBase):
|
||||
"""Truncate to midnight on the Monday of the week."""
|
||||
kind = 'week'
|
||||
|
||||
|
||||
class TruncDay(TruncBase):
|
||||
kind = 'day'
|
||||
|
||||
|
|
|
@ -777,8 +777,8 @@ class QuerySet:
|
|||
Return a list of date objects representing all available dates for
|
||||
the given field_name, scoped to 'kind'.
|
||||
"""
|
||||
assert kind in ("year", "month", "day"), \
|
||||
"'kind' must be one of 'year', 'month' or 'day'."
|
||||
assert kind in ('year', 'month', 'week', 'day'), \
|
||||
"'kind' must be one of 'year', 'month', 'week', or 'day'."
|
||||
assert order in ('ASC', 'DESC'), \
|
||||
"'order' must be either 'ASC' or 'DESC'."
|
||||
return self.annotate(
|
||||
|
@ -793,8 +793,8 @@ class QuerySet:
|
|||
Return a list of datetime objects representing all available
|
||||
datetimes for the given field_name, scoped to 'kind'.
|
||||
"""
|
||||
assert kind in ("year", "month", "day", "hour", "minute", "second"), \
|
||||
"'kind' must be one of 'year', 'month', 'day', 'hour', 'minute' or 'second'."
|
||||
assert kind in ('year', 'month', 'week', 'day', 'hour', 'minute', 'second'), \
|
||||
"'kind' must be one of 'year', 'month', 'week', 'day', 'hour', 'minute', or 'second'."
|
||||
assert order in ('ASC', 'DESC'), \
|
||||
"'order' must be either 'ASC' or 'DESC'."
|
||||
if settings.USE_TZ:
|
||||
|
|
|
@ -439,6 +439,7 @@ return:
|
|||
* "year": 2015-01-01 00:00:00+00:00
|
||||
* "quarter": 2015-04-01 00:00:00+00:00
|
||||
* "month": 2015-06-01 00:00:00+00:00
|
||||
* "week": 2015-06-15 00:00:00+00:00
|
||||
* "day": 2015-06-15 00:00:00+00:00
|
||||
* "hour": 2015-06-15 14:00:00+00:00
|
||||
* "minute": 2015-06-15 14:30:00+00:00
|
||||
|
@ -452,6 +453,7 @@ values returned when this timezone is active will be:
|
|||
* "year": 2015-01-01 00:00:00+11:00
|
||||
* "quarter": 2015-04-01 00:00:00+10:00
|
||||
* "month": 2015-06-01 00:00:00+10:00
|
||||
* "week": 2015-06-16 00:00:00+10:00
|
||||
* "day": 2015-06-16 00:00:00+10:00
|
||||
* "hour": 2015-06-16 00:00:00+10:00
|
||||
* "minute": 2015-06-16 00:30:00+10:00
|
||||
|
@ -504,6 +506,14 @@ Usage example::
|
|||
|
||||
.. attribute:: kind = 'month'
|
||||
|
||||
.. class:: TruncWeek(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Truncates to midnight on the Monday of the week.
|
||||
|
||||
.. attribute:: kind = 'week'
|
||||
|
||||
.. class:: TruncQuarter(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
|
|
@ -694,13 +694,15 @@ objects representing all available dates of a particular kind within the
|
|||
contents of the ``QuerySet``.
|
||||
|
||||
``field`` should be the name of a ``DateField`` of your model.
|
||||
``kind`` should be either ``"year"``, ``"month"`` or ``"day"``. Each
|
||||
``datetime.date`` object in the result list is "truncated" to the given
|
||||
``type``.
|
||||
``kind`` should be either ``"year"``, ``"month"``, ``"week"``, or ``"day"``.
|
||||
Each :class:`datetime.date` object in the result list is "truncated" to the
|
||||
given ``type``.
|
||||
|
||||
* ``"year"`` returns a list of all distinct year values for the field.
|
||||
* ``"month"`` returns a list of all distinct year/month values for the
|
||||
field.
|
||||
* ``"week"`` returns a list of all distinct year/week values for the field. All
|
||||
dates will be a Monday.
|
||||
* ``"day"`` returns a list of all distinct year/month/day values for the
|
||||
field.
|
||||
|
||||
|
@ -713,6 +715,8 @@ Examples::
|
|||
[datetime.date(2005, 1, 1)]
|
||||
>>> Entry.objects.dates('pub_date', 'month')
|
||||
[datetime.date(2005, 2, 1), datetime.date(2005, 3, 1)]
|
||||
>>> Entry.objects.dates('pub_date', 'week')
|
||||
[datetime.date(2005, 2, 14), datetime.date(2005, 3, 14)]
|
||||
>>> Entry.objects.dates('pub_date', 'day')
|
||||
[datetime.date(2005, 2, 20), datetime.date(2005, 3, 20)]
|
||||
>>> Entry.objects.dates('pub_date', 'day', order='DESC')
|
||||
|
@ -720,6 +724,10 @@ Examples::
|
|||
>>> Entry.objects.filter(headline__contains='Lennon').dates('pub_date', 'day')
|
||||
[datetime.date(2005, 3, 20)]
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
"week" support was added.
|
||||
|
||||
``datetimes()``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -731,9 +739,9 @@ contents of the ``QuerySet``.
|
|||
|
||||
``field_name`` should be the name of a ``DateTimeField`` of your model.
|
||||
|
||||
``kind`` should be either ``"year"``, ``"month"``, ``"day"``, ``"hour"``,
|
||||
``"minute"`` or ``"second"``. Each ``datetime.datetime`` object in the result
|
||||
list is "truncated" to the given ``type``.
|
||||
``kind`` should be either ``"year"``, ``"month"``, ``"week"``, ``"day"``,
|
||||
``"hour"``, ``"minute"``, or ``"second"``. Each :class:`datetime.datetime`
|
||||
object in the result list is "truncated" to the given ``type``.
|
||||
|
||||
``order``, which defaults to ``'ASC'``, should be either ``'ASC'`` or
|
||||
``'DESC'``. This specifies how to order the results.
|
||||
|
@ -745,6 +753,10 @@ object. If it's ``None``, Django uses the :ref:`current time zone
|
|||
<default-current-time-zone>`. It has no effect when :setting:`USE_TZ` is
|
||||
``False``.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
"week" support was added.
|
||||
|
||||
.. _database-time-zone-definitions:
|
||||
|
||||
.. note::
|
||||
|
|
|
@ -173,6 +173,10 @@ Models
|
|||
* The new :class:`~django.db.models.functions.Replace` database function
|
||||
replaces strings in an expression.
|
||||
|
||||
* The new :class:`~django.db.models.functions.TruncWeek` function truncates
|
||||
:class:`~django.db.models.DateField` and
|
||||
:class:`~django.db.models.DateTimeField` to the Monday of a week.
|
||||
|
||||
Requests and Responses
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -55,6 +55,12 @@ class DatesTests(TestCase):
|
|||
datetime.date(2010, 7, 1),
|
||||
],
|
||||
)
|
||||
self.assertSequenceEqual(
|
||||
Comment.objects.dates("article__pub_date", "week"), [
|
||||
datetime.date(2005, 7, 25),
|
||||
datetime.date(2010, 7, 26),
|
||||
],
|
||||
)
|
||||
self.assertSequenceEqual(
|
||||
Comment.objects.dates("article__pub_date", "day"), [
|
||||
datetime.date(2005, 7, 28),
|
||||
|
@ -93,7 +99,8 @@ class DatesTests(TestCase):
|
|||
)
|
||||
|
||||
def test_dates_fails_when_given_invalid_kind_argument(self):
|
||||
with self.assertRaisesMessage(AssertionError, "'kind' must be one of 'year', 'month' or 'day'."):
|
||||
msg = "'kind' must be one of 'year', 'month', 'week', or 'day'."
|
||||
with self.assertRaisesMessage(AssertionError, msg):
|
||||
Article.objects.dates("pub_date", "bad_kind")
|
||||
|
||||
def test_dates_fails_when_given_invalid_order_argument(self):
|
||||
|
|
|
@ -53,6 +53,12 @@ class DateTimesTests(TestCase):
|
|||
datetime.datetime(2010, 7, 1),
|
||||
],
|
||||
)
|
||||
self.assertSequenceEqual(
|
||||
Comment.objects.datetimes("article__pub_date", "week"), [
|
||||
datetime.datetime(2005, 7, 25),
|
||||
datetime.datetime(2010, 7, 26),
|
||||
],
|
||||
)
|
||||
self.assertSequenceEqual(
|
||||
Comment.objects.datetimes("article__pub_date", "day"), [
|
||||
datetime.datetime(2005, 7, 28),
|
||||
|
@ -98,6 +104,9 @@ class DateTimesTests(TestCase):
|
|||
self.assertQuerysetEqual(
|
||||
Article.objects.datetimes('pub_date', 'month'),
|
||||
["datetime.datetime(2005, 7, 1, 0, 0)"])
|
||||
self.assertQuerysetEqual(
|
||||
Article.objects.datetimes('pub_date', 'week'),
|
||||
["datetime.datetime(2005, 7, 25, 0, 0)"])
|
||||
self.assertQuerysetEqual(
|
||||
Article.objects.datetimes('pub_date', 'day'),
|
||||
["datetime.datetime(2005, 7, 28, 0, 0)",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
|
@ -8,7 +8,7 @@ from django.db.models.functions import (
|
|||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
||||
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
||||
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
||||
TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear,
|
||||
)
|
||||
from django.test import (
|
||||
TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature,
|
||||
|
@ -34,6 +34,10 @@ def truncate_to(value, kind, tzinfo=None):
|
|||
if isinstance(value, datetime):
|
||||
return value.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return value
|
||||
if kind == 'week':
|
||||
if isinstance(value, datetime):
|
||||
return (value - timedelta(days=value.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return value - timedelta(days=value.weekday())
|
||||
if kind == 'month':
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
@ -536,6 +540,7 @@ class DateFunctionTests(TestCase):
|
|||
test_date_kind('year')
|
||||
test_date_kind('quarter')
|
||||
test_date_kind('month')
|
||||
test_date_kind('week')
|
||||
test_date_kind('day')
|
||||
test_time_kind('hour')
|
||||
test_time_kind('minute')
|
||||
|
@ -543,6 +548,7 @@ class DateFunctionTests(TestCase):
|
|||
test_datetime_kind('year')
|
||||
test_datetime_kind('quarter')
|
||||
test_datetime_kind('month')
|
||||
test_datetime_kind('week')
|
||||
test_datetime_kind('day')
|
||||
test_datetime_kind('hour')
|
||||
test_datetime_kind('minute')
|
||||
|
@ -656,6 +662,30 @@ class DateFunctionTests(TestCase):
|
|||
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncMonth('start_time', output_field=TimeField())))
|
||||
|
||||
def test_trunc_week_func(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'week')
|
||||
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=TruncWeek('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'week')),
|
||||
(end_datetime, truncate_to(end_datetime, 'week')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncWeek('start_datetime')).count(), 1)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncWeek('start_time')))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncWeek('start_time', output_field=TimeField())))
|
||||
|
||||
def test_trunc_date_func(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||
|
@ -960,6 +990,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
|||
test_date_kind('year')
|
||||
test_date_kind('quarter')
|
||||
test_date_kind('month')
|
||||
test_date_kind('week')
|
||||
test_date_kind('day')
|
||||
test_time_kind('hour')
|
||||
test_time_kind('minute')
|
||||
|
@ -967,6 +998,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
|||
test_datetime_kind('year')
|
||||
test_datetime_kind('quarter')
|
||||
test_datetime_kind('month')
|
||||
test_datetime_kind('week')
|
||||
test_datetime_kind('day')
|
||||
test_datetime_kind('hour')
|
||||
test_datetime_kind('minute')
|
||||
|
|
Loading…
Reference in New Issue