summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMads Jensen <mje@inducks.org>2016-11-11 14:01:40 +0100
committerTim Graham <timograham@gmail.com>2016-11-11 08:01:40 -0500
commit1446902be48ebf19bfe484371897a2815dd21fca (patch)
tree25945ce54301b3b33d4e975bfed5421ea5224d91
parent2dc07da497f5ae95ad34a4714e330fae1fa8413c (diff)
Fixed #25240 -- Added ExtractWeek and exposed it through the __week lookup.
Thanks to Mariusz Felisiak and Tim Graham for review.
-rw-r--r--django/db/backends/mysql/operations.py6
-rw-r--r--django/db/backends/oracle/operations.py3
-rw-r--r--django/db/backends/sqlite3/base.py4
-rw-r--r--django/db/models/functions/__init__.py7
-rw-r--r--django/db/models/functions/datetime.py9
-rw-r--r--docs/ref/models/database-functions.txt15
-rw-r--r--docs/ref/models/querysets.txt22
-rw-r--r--docs/releases/1.11.txt5
-rw-r--r--tests/db_functions/test_datetime.py60
9 files changed, 126 insertions, 5 deletions
diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py
index 306a0378b2..772ae1e37d 100644
--- a/django/db/backends/mysql/operations.py
+++ b/django/db/backends/mysql/operations.py
@@ -24,7 +24,13 @@ class DatabaseOperations(BaseDatabaseOperations):
# DAYOFWEEK() returns an integer, 1-7, Sunday=1.
# Note: WEEKDAY() returns 0-6, Monday=0.
return "DAYOFWEEK(%s)" % field_name
+ elif lookup_type == 'week':
+ # Override the value of default_week_format for consistency with
+ # other database backends.
+ # Mode 3: Monday, 1-53, with 4 or more days this year.
+ return "WEEK(%s, 3)" % field_name
else:
+ # EXTRACT returns 1-53 based on ISO-8601 for the week number.
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
def date_trunc_sql(self, lookup_type, field_name):
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 8191f89dd0..82be9c5241 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -84,6 +84,9 @@ WHEN (new.%(col_name)s IS NULL)
if lookup_type == 'week_day':
# TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
return "TO_CHAR(%s, 'D')" % field_name
+ elif lookup_type == 'week':
+ # IW = ISO week number
+ return "TO_CHAR(%s, 'IW')" % field_name
else:
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 66ad27842b..aee012c7e6 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -344,6 +344,8 @@ def _sqlite_date_extract(lookup_type, dt):
return None
if lookup_type == 'week_day':
return (dt.isoweekday() % 7) + 1
+ elif lookup_type == 'week':
+ return dt.isocalendar()[1]
else:
return getattr(dt, lookup_type)
@@ -406,6 +408,8 @@ def _sqlite_datetime_extract(lookup_type, dt, tzname):
return None
if lookup_type == 'week_day':
return (dt.isoweekday() % 7) + 1
+ elif lookup_type == 'week':
+ return dt.isocalendar()[1]
else:
return getattr(dt, lookup_type)
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
index dd45d7bb4c..eeafc1dc48 100644
--- a/django/db/models/functions/__init__.py
+++ b/django/db/models/functions/__init__.py
@@ -4,8 +4,9 @@ from .base import (
)
from .datetime import (
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
- ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
- TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
+ ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
+ TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
+ TruncYear,
)
__all__ = [
@@ -14,7 +15,7 @@ __all__ = [
'Lower', 'Now', 'Substr', 'Upper',
# datetime
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
- 'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
+ 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
'TruncSecond', 'TruncTime', 'TruncYear',
]
diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py
index 341c53e162..7b137eabef 100644
--- a/django/db/models/functions/datetime.py
+++ b/django/db/models/functions/datetime.py
@@ -87,6 +87,14 @@ class ExtractDay(Extract):
lookup_name = 'day'
+class ExtractWeek(Extract):
+ """
+ Return 1-52 or 53, based on ISO-8601, i.e., Monday is the first of the
+ week.
+ """
+ lookup_name = 'week'
+
+
class ExtractWeekDay(Extract):
"""
Return Sunday=1 through Saturday=7.
@@ -112,6 +120,7 @@ DateField.register_lookup(ExtractYear)
DateField.register_lookup(ExtractMonth)
DateField.register_lookup(ExtractDay)
DateField.register_lookup(ExtractWeekDay)
+DateField.register_lookup(ExtractWeek)
TimeField.register_lookup(ExtractHour)
TimeField.register_lookup(ExtractMinute)
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index 0d9c80fa25..ad653a710b 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -313,6 +313,7 @@ Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
* "year": 2015
* "month": 6
* "day": 15
+* "week": 25
* "week_day": 2
* "hour": 23
* "minute": 30
@@ -340,6 +341,14 @@ returned when this timezone is active will be the same as above except for:
>>> (dt.isoweekday() % 7) + 1
2
+.. admonition:: ``week`` values
+
+ The ``week`` ``lookup_type`` is calculated based on `ISO-8601
+ <https://en.wikipedia.org/wiki/ISO-8601>`_, i.e.,
+ a week starts on a Monday. The first week is the one with the majority
+ of the days, i.e., a week that starts on or before Thursday. The value
+ returned is in the range 1 to 52 or 53.
+
Each ``lookup_name`` above has a corresponding ``Extract`` subclass (listed
below) that should typically be used instead of the more verbose equivalent,
e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
@@ -382,6 +391,12 @@ Usage example::
.. attribute:: lookup_name = 'week_day'
+.. class:: ExtractWeek(expression, tzinfo=None, **extra)
+
+ .. versionadded:: 1.11
+
+ .. attribute:: lookup_name = 'week'
+
These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
as ``__(lookup_name)``, e.g. ``__year``.
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 7bcf2fc6ed..0d89f221b9 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -2703,6 +2703,28 @@ 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:: week
+
+``week``
+~~~~~~~~
+
+.. versionadded:: 1.11
+
+For date and datetime fields, return the week number (1-52 or 53) according
+to `ISO-8601 <https://en.wikipedia.org/wiki/ISO-8601>`_, i.e., weeks start
+on a Monday and the first week starts on or before Thursday.
+
+Example::
+
+ Entry.objects.filter(pub_date__week=52)
+ Entry.objects.filter(pub_date__week__gte=32, pub_date__week__lte=38)
+
+(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:: week_day
``week_day``
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index 77a1c3ea12..d26a6a0a51 100644
--- a/docs/releases/1.11.txt
+++ b/docs/releases/1.11.txt
@@ -306,6 +306,11 @@ Models
* Added support for time truncation to
:class:`~django.db.models.functions.datetime.Trunc` functions.
+* Added the :class:`~django.db.models.functions.datetime.ExtractWeek` function
+ to extract the week from :class:`~django.db.models.DateField` and
+ :class:`~django.db.models.DateTimeField` and exposed it through the
+ :lookup:`week` lookup.
+
* 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.
diff --git a/tests/db_functions/test_datetime.py b/tests/db_functions/test_datetime.py
index f9a9a54c1c..a6c18438a6 100644
--- a/tests/db_functions/test_datetime.py
+++ b/tests/db_functions/test_datetime.py
@@ -9,8 +9,9 @@ from django.db import connection
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, TruncTime, TruncYear,
+ ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
+ TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
+ TruncYear,
)
from django.test import TestCase, override_settings
from django.utils import timezone
@@ -167,6 +168,11 @@ class DateFunctionTests(TestCase):
lambda m: (m.start_datetime, m.extracted)
)
self.assertQuerysetEqual(
+ DTModel.objects.annotate(extracted=Extract('start_datetime', 'week')).order_by('start_datetime'),
+ [(start_datetime, 25), (end_datetime, 24)],
+ lambda m: (m.start_datetime, m.extracted)
+ )
+ self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
[
(start_datetime, (start_datetime.isoweekday() % 7) + 1),
@@ -254,6 +260,53 @@ class DateFunctionTests(TestCase):
)
self.assertEqual(DTModel.objects.filter(start_datetime__day=ExtractDay('start_datetime')).count(), 2)
+ def test_extract_week_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=ExtractWeek('start_datetime')).order_by('start_datetime'),
+ [(start_datetime, 25), (end_datetime, 24)],
+ lambda m: (m.start_datetime, m.extracted)
+ )
+ self.assertQuerysetEqual(
+ DTModel.objects.annotate(extracted=ExtractWeek('start_date')).order_by('start_datetime'),
+ [(start_datetime, 25), (end_datetime, 24)],
+ lambda m: (m.start_datetime, m.extracted)
+ )
+ # both dates are from the same week.
+ self.assertEqual(DTModel.objects.filter(start_datetime__week=ExtractWeek('start_datetime')).count(), 2)
+
+ def test_extract_week_func_boundaries(self):
+ end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+ if settings.USE_TZ:
+ end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+
+ week_52_day_2014 = microsecond_support(datetime(2014, 12, 27, 13, 0)) # Sunday
+ week_1_day_2014_2015 = microsecond_support(datetime(2014, 12, 31, 13, 0)) # Wednesday
+ week_53_day_2015 = microsecond_support(datetime(2015, 12, 31, 13, 0)) # Thursday
+ if settings.USE_TZ:
+ week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015, is_dst=False)
+ week_52_day_2014 = timezone.make_aware(week_52_day_2014, is_dst=False)
+ week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False)
+
+ days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015]
+ self.create_model(week_53_day_2015, end_datetime)
+ self.create_model(week_52_day_2014, end_datetime)
+ self.create_model(week_1_day_2014_2015, end_datetime)
+ qs = DTModel.objects.filter(start_datetime__in=days).annotate(
+ extracted=ExtractWeek('start_datetime'),
+ ).order_by('start_datetime')
+ self.assertQuerysetEqual(qs, [
+ (week_52_day_2014, 52),
+ (week_1_day_2014_2015, 1),
+ (week_53_day_2015, 53),
+ ], lambda m: (m.start_datetime, m.extracted))
+
def test_extract_weekday_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))
@@ -669,6 +722,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
qs = DTModel.objects.annotate(
day=Extract('start_datetime', 'day'),
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
+ week=Extract('start_datetime', 'week', tzinfo=melb),
weekday=ExtractWeekDay('start_datetime'),
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
hour=ExtractHour('start_datetime'),
@@ -678,6 +732,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
utc_model = qs.get()
self.assertEqual(utc_model.day, 15)
self.assertEqual(utc_model.day_melb, 16)
+ self.assertEqual(utc_model.week, 25)
self.assertEqual(utc_model.weekday, 2)
self.assertEqual(utc_model.weekday_melb, 3)
self.assertEqual(utc_model.hour, 23)
@@ -688,6 +743,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(melb_model.day, 16)
self.assertEqual(melb_model.day_melb, 16)
+ self.assertEqual(melb_model.week, 25)
self.assertEqual(melb_model.weekday, 3)
self.assertEqual(melb_model.weekday_melb, 3)
self.assertEqual(melb_model.hour, 9)