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. | ||||
|             # 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): | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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', | ||||
| ] | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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``. | ||||
|   | ||||
| @@ -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`` | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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 | ||||
| @@ -166,6 +167,11 @@ class DateFunctionTests(TestCase): | ||||
|             [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)], | ||||
|             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'), | ||||
|             [ | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user