mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		| @@ -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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user