Code

Ticket #17260: dates_in_tz.diff

File dates_in_tz.diff, 11.6 KB (added by akaariai, 2 years ago)
Line 
1diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
2index 7674f5c..c8f010a 100644
3--- a/django/db/backends/__init__.py
4+++ b/django/db/backends/__init__.py
5@@ -1,4 +1,7 @@
6+import datetime
7+
8 from django.db.utils import DatabaseError
9+from django.utils import timezone
10 
11 try:
12     import thread
13@@ -807,9 +810,17 @@ class BaseDatabaseOperations(object):
14 
15         `value` is an int, containing the looked-up year.
16         """
17-        first = '%s-01-01 00:00:00'
18-        second = '%s-12-31 23:59:59.999999'
19-        return [first % value, second % value]
20+        tz = timezone.get_current_timezone() if settings.USE_TZ else None
21+        first_dt = datetime.datetime(year=value, month=01, day=01, tzinfo=tz)
22+        second_dt = datetime.datetime(year=value, month=12, day=31, hour=23,
23+                                      minute=59, second=59, microsecond=999999,
24+                                      tzinfo=tz)
25+        if settings.USE_TZ:
26+            first_dt = first_dt.astimezone(timezone.utc)
27+            second_dt = second_dt.astimezone(timezone.utc)
28+        first = first_dt.strftime('%Y-%m-%d %H:%M:%S')
29+        second = second_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
30+        return [first, second]
31 
32     def year_lookup_bounds_for_date_field(self, value):
33         """
34diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
35index 949a05c..861d742 100644
36--- a/django/db/backends/postgresql_psycopg2/operations.py
37+++ b/django/db/backends/postgresql_psycopg2/operations.py
38@@ -1,4 +1,6 @@
39 from django.db.backends import BaseDatabaseOperations
40+from django.conf import settings
41+from django.utils import timezone
42 
43 
44 class DatabaseOperations(BaseDatabaseOperations):
45@@ -7,11 +9,18 @@ class DatabaseOperations(BaseDatabaseOperations):
46 
47     def date_extract_sql(self, lookup_type, field_name):
48         # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
49+        field_sql = field_name
50+        if settings.USE_TZ:
51+            # Hey, I finally managed to get the UTC offset! And in oh-so-beautiful way...
52+            # Will this break at DST?
53+            time_offset = timezone.now().astimezone(timezone.get_current_timezone()).utcoffset()
54+            if time_offset:
55+                field_sql = self.date_interval_sql(field_name, '+', time_offset)
56         if lookup_type == 'week_day':
57             # For consistency across backends, we return Sunday=1, Saturday=7.
58-            return "EXTRACT('dow' FROM %s) + 1" % field_name
59+            return "EXTRACT('dow' FROM %s) + 1" % field_sql
60         else:
61-            return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)
62+            return "EXTRACT('%s' FROM %s)" % (lookup_type, field_sql)
63 
64     def date_interval_sql(self, sql, connector, timedelta):
65         """
66@@ -32,7 +41,23 @@ class DatabaseOperations(BaseDatabaseOperations):
67 
68     def date_trunc_sql(self, lookup_type, field_name):
69         # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
70-        return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
71+        field_sql = field_name
72+        if settings.USE_TZ:
73+            # Hey, I finally managed to get the UTC offset! And in oh-so-beautiful way...
74+            # Will this break at DST?
75+            time_offset = timezone.now().astimezone(timezone.get_current_timezone()).utcoffset()
76+            if time_offset:
77+                field_sql = self.date_interval_sql(field_name, '+', time_offset)
78+            # Alternate way: better except we don't know if PostgreSQL happens to support this
79+            # timezone... For example the testing returns +0300 for me, which really isn't a
80+            # timezone name :( The above way is a hack at its core, we really don't know if the
81+            # offset is correct for the date in the DB. It is likely not going to matter, the
82+            # DST offset is usually just one hour. But for example some country can change
83+            # their time zone for 23 hours, and that would break date lookups. Maybe a bit
84+            # non-realistic problem, but to be totally correct we should use PostgreSQL's at
85+            # time zone, not adding the current UTC offset.
86+            # field_sql = field_name + " AT TIME ZONE '%s'" % timezone.get_current_timezone_name()
87+        return "DATE_TRUNC('%s', %s)" % (lookup_type, field_sql)
88 
89     def deferrable_sql(self):
90         return " DEFERRABLE INITIALLY DEFERRED"
91diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
92index 0b19442..421d883 100644
93--- a/django/db/backends/sqlite3/base.py
94+++ b/django/db/backends/sqlite3/base.py
95@@ -127,6 +127,25 @@ class DatabaseOperations(BaseDatabaseOperations):
96         # single quotes are used because this is a string (and could otherwise
97         # cause a collision with a field name).
98         return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name)
99+
100+    def year_lookup_bounds(self, value):
101+        """
102+        Returns a two-elements list with the lower and upper bound to be used
103+        with a BETWEEN operator to query a field value using a year lookup
104+
105+        `value` is an int, containing the looked-up year.
106+        """
107+        tz = timezone.get_current_timezone() if settings.USE_TZ else None
108+        first_dt = datetime.datetime(year=value, month=01, day=01, tzinfo=tz)
109+        second_dt = datetime.datetime(year=value, month=12, day=31, hour=23,
110+                                      minute=59, second=59, microsecond=999999,
111+                                      tzinfo=tz)
112+        if settings.USE_TZ:
113+            first_dt = first_dt.astimezone(timezone.utc)
114+            second_dt = second_dt.astimezone(timezone.utc)
115+        first = first_dt.strftime('%Y-%m-%d')
116+        second = second_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
117+        return [first, second]
118 
119     def drop_foreignkey_sql(self):
120         return ""
121@@ -178,11 +197,6 @@ class DatabaseOperations(BaseDatabaseOperations):
122 
123         return unicode(value)
124 
125-    def year_lookup_bounds(self, value):
126-        first = '%s-01-01'
127-        second = '%s-12-31 23:59:59.999999'
128-        return [first % value, second % value]
129-
130     def convert_values(self, value, field):
131         """SQLite returns floats when it should be returning decimals,
132         and gets dates and datetimes wrong.
133@@ -359,6 +373,9 @@ def _sqlite_extract(lookup_type, dt):
134         dt = util.typecast_timestamp(dt)
135     except (ValueError, TypeError):
136         return None
137+    if settings.USE_TZ:
138+        tz = timezone.get_current_timezone()
139+        dt = dt.astimezone(tz)
140     if lookup_type == 'week_day':
141         return (dt.isoweekday() % 7) + 1
142     else:
143@@ -369,6 +386,9 @@ def _sqlite_date_trunc(lookup_type, dt):
144         dt = util.typecast_timestamp(dt)
145     except (ValueError, TypeError):
146         return None
147+    if settings.USE_TZ:
148+        tz = timezone.get_current_timezone()
149+        dt = dt.astimezone(tz)
150     if lookup_type == 'year':
151         return "%i-01-01 00:00:00" % dt.year
152     elif lookup_type == 'month':
153diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py
154index a8d2c0c..81642df 100644
155--- a/tests/modeltests/timezones/tests.py
156+++ b/tests/modeltests/timezones/tests.py
157@@ -277,6 +277,15 @@ LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests)
158 
159 #@override_settings(USE_TZ=True)
160 class NewDatabaseTests(BaseDateTimeTests):
161+    @requires_tz_support
162+    def test_day_in_current_tz(self):
163+        dt = datetime.datetime(2012, 03, 05, 01, 00, 00)
164+        dt = dt.replace(tzinfo=EAT)
165+        Event.objects.create(dt=dt)
166+        with timezone.override(UTC):
167+            self.assertEqual(Event.objects.dates('dt', 'day')[0].day, 4)
168+        with timezone.override(EAT):
169+            self.assertEqual(Event.objects.dates('dt', 'day')[0].day, 5)
170 
171     @requires_tz_support
172     @skipIf(sys.version_info < (2, 6), "this test requires Python >= 2.6")
173@@ -427,10 +436,16 @@ class NewDatabaseTests(BaseDateTimeTests):
174         # implementation is changed to perform the aggregation is local time.
175         Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
176         Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
177-        self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1)
178-        self.assertEqual(Event.objects.filter(dt__month=1).count(), 1)
179-        self.assertEqual(Event.objects.filter(dt__day=1).count(), 1)
180-        self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1)
181+        with timezone.override(EAT):
182+            self.assertEqual(Event.objects.filter(dt__year=2011).count(), 2)
183+            self.assertEqual(Event.objects.filter(dt__month=1).count(), 2)
184+            self.assertEqual(Event.objects.filter(dt__day=1).count(), 2)
185+            self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 2)
186+        with timezone.override(UTC):
187+            self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1)
188+            self.assertEqual(Event.objects.filter(dt__month=1).count(), 1)
189+            self.assertEqual(Event.objects.filter(dt__day=1).count(), 1)
190+            self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1)
191 
192     def test_query_aggregation(self):
193         # Only min and max make sense for datetimes.
194@@ -469,18 +484,29 @@ class NewDatabaseTests(BaseDateTimeTests):
195         # Same comment as in test_query_date_related_filters.
196         Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
197         Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
198-        self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
199-                [datetime.datetime(2010, 1, 1, tzinfo=UTC),
200-                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
201-                transform=lambda d: d)
202-        self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
203-                [datetime.datetime(2010, 12, 1, tzinfo=UTC),
204-                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
205-                transform=lambda d: d)
206-        self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
207-                [datetime.datetime(2010, 12, 31, tzinfo=UTC),
208-                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
209-                transform=lambda d: d)
210+        with timezone.override(EAT):
211+            self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
212+                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
213+                    transform=lambda d: d)
214+            self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
215+                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
216+                    transform=lambda d: d)
217+            self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
218+                    [datetime.datetime(2011, 1, 1, tzinfo=UTC)],
219+                    transform=lambda d: d)
220+        with timezone.override(UTC):
221+            self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
222+                    [datetime.datetime(2010, 1, 1, tzinfo=UTC),
223+                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
224+                    transform=lambda d: d)
225+            self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
226+                    [datetime.datetime(2010, 12, 1, tzinfo=UTC),
227+                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
228+                    transform=lambda d: d)
229+            self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
230+                    [datetime.datetime(2010, 12, 31, tzinfo=UTC),
231+                    datetime.datetime(2011, 1, 1, tzinfo=UTC)],
232+                    transform=lambda d: d)
233 
234     def test_raw_sql(self):
235         # Regression test for #17755