summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSigurd Ljødal <sigurd@ljodal.no>2017-09-28 22:39:03 +0200
committerTim Graham <timograham@gmail.com>2018-01-27 09:59:13 -0500
commita455e732a0011dfbd15eddaa4fe60cb9439326e0 (patch)
tree1df72fb36c377ae630e1bbc8a588662f8f3cb46f
parentf229049d6cd186025f4033dc47608b0c9731e882 (diff)
Fixed #28650 -- Added TruncWeek database function.
-rw-r--r--django/db/backends/mysql/operations.py10
-rw-r--r--django/db/backends/oracle/operations.py4
-rw-r--r--django/db/backends/sqlite3/base.py6
-rw-r--r--django/db/models/functions/__init__.py4
-rw-r--r--django/db/models/functions/datetime.py8
-rw-r--r--django/db/models/query.py8
-rw-r--r--docs/ref/models/database-functions.txt10
-rw-r--r--docs/ref/models/querysets.txt24
-rw-r--r--docs/releases/2.1.txt4
-rw-r--r--tests/dates/tests.py9
-rw-r--r--tests/datetimes/tests.py9
-rw-r--r--tests/db_functions/test_datetime.py36
12 files changed, 116 insertions, 16 deletions
diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py
index ad4c13652e..2961d6449b 100644
--- a/django/db/backends/mysql/operations.py
+++ b/django/db/backends/mysql/operations.py
@@ -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:
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index bea03e355e..748f606368 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -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':
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 857b050361..0971c500dd 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -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':
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
index 0680200c7a..0815628dd0 100644
--- a/django/db/models/functions/__init__.py
+++ b/django/db/models/functions/__init__.py
@@ -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',
diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py
index 44bcb731e6..74b88b77ca 100644
--- a/django/db/models/functions/datetime.py
+++ b/django/db/models/functions/datetime.py
@@ -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'
diff --git a/django/db/models/query.py b/django/db/models/query.py
index e17f8da1a8..44bc51d0e0 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -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:
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index 97634d3584..68943e8c8e 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -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
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 5064a42c9e..10251efc72 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -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::
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 24ab0faa50..323da06fd6 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -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
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/dates/tests.py b/tests/dates/tests.py
index 9cb77fe9e2..ebdf05812e 100644
--- a/tests/dates/tests.py
+++ b/tests/dates/tests.py
@@ -56,6 +56,12 @@ class DatesTests(TestCase):
],
)
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),
datetime.date(2010, 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):
diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py
index 50a83f0371..a9f1199f06 100644
--- a/tests/datetimes/tests.py
+++ b/tests/datetimes/tests.py
@@ -54,6 +54,12 @@ class DateTimesTests(TestCase):
],
)
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),
datetime.datetime(2010, 7, 28),
@@ -99,6 +105,9 @@ class DateTimesTests(TestCase):
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)",
"datetime.datetime(2005, 7, 29, 0, 0)",
diff --git a/tests/db_functions/test_datetime.py b/tests/db_functions/test_datetime.py
index e2f786bf6b..dc4c911ab9 100644
--- a/tests/db_functions/test_datetime.py
+++ b/tests/db_functions/test_datetime.py
@@ -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')