Fixed #25240 -- Added ExtractWeek and exposed it through the __week lookup.
Thanks to Mariusz Felisiak and Tim Graham for review.
This commit is contained in:
parent
2dc07da497
commit
1446902be4
|
@ -24,7 +24,13 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
# DAYOFWEEK() returns an integer, 1-7, Sunday=1.
|
# DAYOFWEEK() returns an integer, 1-7, Sunday=1.
|
||||||
# Note: WEEKDAY() returns 0-6, Monday=0.
|
# Note: WEEKDAY() returns 0-6, Monday=0.
|
||||||
return "DAYOFWEEK(%s)" % field_name
|
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:
|
else:
|
||||||
|
# EXTRACT returns 1-53 based on ISO-8601 for the week number.
|
||||||
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
||||||
|
|
||||||
def date_trunc_sql(self, lookup_type, field_name):
|
def date_trunc_sql(self, lookup_type, field_name):
|
||||||
|
|
|
@ -84,6 +84,9 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
if lookup_type == 'week_day':
|
if lookup_type == 'week_day':
|
||||||
# TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
|
# TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
|
||||||
return "TO_CHAR(%s, 'D')" % field_name
|
return "TO_CHAR(%s, 'D')" % field_name
|
||||||
|
elif lookup_type == 'week':
|
||||||
|
# IW = ISO week number
|
||||||
|
return "TO_CHAR(%s, 'IW')" % field_name
|
||||||
else:
|
else:
|
||||||
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
|
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
|
||||||
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
||||||
|
|
|
@ -344,6 +344,8 @@ def _sqlite_date_extract(lookup_type, dt):
|
||||||
return None
|
return None
|
||||||
if lookup_type == 'week_day':
|
if lookup_type == 'week_day':
|
||||||
return (dt.isoweekday() % 7) + 1
|
return (dt.isoweekday() % 7) + 1
|
||||||
|
elif lookup_type == 'week':
|
||||||
|
return dt.isocalendar()[1]
|
||||||
else:
|
else:
|
||||||
return getattr(dt, lookup_type)
|
return getattr(dt, lookup_type)
|
||||||
|
|
||||||
|
@ -406,6 +408,8 @@ def _sqlite_datetime_extract(lookup_type, dt, tzname):
|
||||||
return None
|
return None
|
||||||
if lookup_type == 'week_day':
|
if lookup_type == 'week_day':
|
||||||
return (dt.isoweekday() % 7) + 1
|
return (dt.isoweekday() % 7) + 1
|
||||||
|
elif lookup_type == 'week':
|
||||||
|
return dt.isocalendar()[1]
|
||||||
else:
|
else:
|
||||||
return getattr(dt, lookup_type)
|
return getattr(dt, lookup_type)
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ 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, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
|
||||||
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
|
TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
|
||||||
|
TruncYear,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -14,7 +15,7 @@ __all__ = [
|
||||||
'Lower', 'Now', 'Substr', 'Upper',
|
'Lower', 'Now', 'Substr', 'Upper',
|
||||||
# datetime
|
# datetime
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
|
'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
|
||||||
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
||||||
'TruncSecond', 'TruncTime', 'TruncYear',
|
'TruncSecond', 'TruncTime', 'TruncYear',
|
||||||
]
|
]
|
||||||
|
|
|
@ -87,6 +87,14 @@ class ExtractDay(Extract):
|
||||||
lookup_name = 'day'
|
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):
|
class ExtractWeekDay(Extract):
|
||||||
"""
|
"""
|
||||||
Return Sunday=1 through Saturday=7.
|
Return Sunday=1 through Saturday=7.
|
||||||
|
@ -112,6 +120,7 @@ DateField.register_lookup(ExtractYear)
|
||||||
DateField.register_lookup(ExtractMonth)
|
DateField.register_lookup(ExtractMonth)
|
||||||
DateField.register_lookup(ExtractDay)
|
DateField.register_lookup(ExtractDay)
|
||||||
DateField.register_lookup(ExtractWeekDay)
|
DateField.register_lookup(ExtractWeekDay)
|
||||||
|
DateField.register_lookup(ExtractWeek)
|
||||||
|
|
||||||
TimeField.register_lookup(ExtractHour)
|
TimeField.register_lookup(ExtractHour)
|
||||||
TimeField.register_lookup(ExtractMinute)
|
TimeField.register_lookup(ExtractMinute)
|
||||||
|
|
|
@ -313,6 +313,7 @@ Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
|
||||||
* "year": 2015
|
* "year": 2015
|
||||||
* "month": 6
|
* "month": 6
|
||||||
* "day": 15
|
* "day": 15
|
||||||
|
* "week": 25
|
||||||
* "week_day": 2
|
* "week_day": 2
|
||||||
* "hour": 23
|
* "hour": 23
|
||||||
* "minute": 30
|
* "minute": 30
|
||||||
|
@ -340,6 +341,14 @@ returned when this timezone is active will be the same as above except for:
|
||||||
>>> (dt.isoweekday() % 7) + 1
|
>>> (dt.isoweekday() % 7) + 1
|
||||||
2
|
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
|
Each ``lookup_name`` above has a corresponding ``Extract`` subclass (listed
|
||||||
below) that should typically be used instead of the more verbose equivalent,
|
below) that should typically be used instead of the more verbose equivalent,
|
||||||
e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
|
e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
|
||||||
|
@ -382,6 +391,12 @@ Usage example::
|
||||||
|
|
||||||
.. attribute:: lookup_name = 'week_day'
|
.. 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
|
These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
|
||||||
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
|
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
|
||||||
as ``__(lookup_name)``, e.g. ``__year``.
|
as ``__(lookup_name)``, e.g. ``__year``.
|
||||||
|
|
|
@ -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
|
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:: 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
|
.. fieldlookup:: week_day
|
||||||
|
|
||||||
``week_day``
|
``week_day``
|
||||||
|
|
|
@ -306,6 +306,11 @@ 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.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
|
* Added the :class:`~django.db.models.functions.datetime.TruncTime` function
|
||||||
to truncate :class:`~django.db.models.DateTimeField` to its time component
|
to truncate :class:`~django.db.models.DateTimeField` to its time component
|
||||||
and exposed it through the :lookup:`time` lookup.
|
and exposed it through the :lookup:`time` lookup.
|
||||||
|
|
|
@ -9,8 +9,9 @@ from django.db import connection
|
||||||
from django.db.models import DateField, DateTimeField, IntegerField, TimeField
|
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, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
|
||||||
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime, TruncYear,
|
TruncDay, 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
|
||||||
|
@ -166,6 +167,11 @@ class DateFunctionTests(TestCase):
|
||||||
[(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
|
[(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
|
||||||
lambda m: (m.start_datetime, m.extracted)
|
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(
|
self.assertQuerysetEqual(
|
||||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
|
DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
|
||||||
[
|
[
|
||||||
|
@ -254,6 +260,53 @@ class DateFunctionTests(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(DTModel.objects.filter(start_datetime__day=ExtractDay('start_datetime')).count(), 2)
|
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):
|
def test_extract_weekday_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 = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||||
|
@ -669,6 +722,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
qs = DTModel.objects.annotate(
|
qs = DTModel.objects.annotate(
|
||||||
day=Extract('start_datetime', 'day'),
|
day=Extract('start_datetime', 'day'),
|
||||||
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
|
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
|
||||||
|
week=Extract('start_datetime', 'week', tzinfo=melb),
|
||||||
weekday=ExtractWeekDay('start_datetime'),
|
weekday=ExtractWeekDay('start_datetime'),
|
||||||
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
||||||
hour=ExtractHour('start_datetime'),
|
hour=ExtractHour('start_datetime'),
|
||||||
|
@ -678,6 +732,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
utc_model = qs.get()
|
utc_model = qs.get()
|
||||||
self.assertEqual(utc_model.day, 15)
|
self.assertEqual(utc_model.day, 15)
|
||||||
self.assertEqual(utc_model.day_melb, 16)
|
self.assertEqual(utc_model.day_melb, 16)
|
||||||
|
self.assertEqual(utc_model.week, 25)
|
||||||
self.assertEqual(utc_model.weekday, 2)
|
self.assertEqual(utc_model.weekday, 2)
|
||||||
self.assertEqual(utc_model.weekday_melb, 3)
|
self.assertEqual(utc_model.weekday_melb, 3)
|
||||||
self.assertEqual(utc_model.hour, 23)
|
self.assertEqual(utc_model.hour, 23)
|
||||||
|
@ -688,6 +743,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
|
|
||||||
self.assertEqual(melb_model.day, 16)
|
self.assertEqual(melb_model.day, 16)
|
||||||
self.assertEqual(melb_model.day_melb, 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, 3)
|
||||||
self.assertEqual(melb_model.weekday_melb, 3)
|
self.assertEqual(melb_model.weekday_melb, 3)
|
||||||
self.assertEqual(melb_model.hour, 9)
|
self.assertEqual(melb_model.hour, 9)
|
||||||
|
|
Loading…
Reference in New Issue