浏览代码

Fixed #33279 -- Fixed handling time zones with "-" sign in names.

Thanks yakimka for the report.

Regression in fde9b7d35e4e185903cc14aa587ca870037941b1.
Can Sarigol 3 年之前
父节点
当前提交
661316b066

+ 3 - 5
django/db/backends/mysql/operations.py

@@ -2,6 +2,7 @@ import uuid
 
 from django.conf import settings
 from django.db.backends.base.operations import BaseDatabaseOperations
+from django.db.backends.utils import split_tzname_delta
 from django.db.models import Exists, ExpressionWrapper, Lookup
 from django.utils import timezone
 from django.utils.encoding import force_str
@@ -77,11 +78,8 @@ class DatabaseOperations(BaseDatabaseOperations):
             return "DATE(%s)" % (field_name)
 
     def _prepare_tzname_delta(self, tzname):
-        if '+' in tzname:
-            return tzname[tzname.find('+'):]
-        elif '-' in tzname:
-            return tzname[tzname.find('-'):]
-        return tzname
+        tzname, sign, offset = split_tzname_delta(tzname)
+        return f'{sign}{offset}' if offset else tzname
 
     def _convert_field_to_tz(self, field_name, tzname):
         if tzname and settings.USE_TZ and self.connection.timezone_name != tzname:

+ 5 - 6
django/db/backends/oracle/operations.py

@@ -5,7 +5,9 @@ from functools import lru_cache
 from django.conf import settings
 from django.db import DatabaseError, NotSupportedError
 from django.db.backends.base.operations import BaseDatabaseOperations
-from django.db.backends.utils import strip_quotes, truncate_name
+from django.db.backends.utils import (
+    split_tzname_delta, strip_quotes, truncate_name,
+)
 from django.db.models import AutoField, Exists, ExpressionWrapper, Lookup
 from django.db.models.expressions import RawSQL
 from django.db.models.sql.where import WhereNode
@@ -108,11 +110,8 @@ END;
     _tzname_re = _lazy_re_compile(r'^[\w/:+-]+$')
 
     def _prepare_tzname_delta(self, tzname):
-        if '+' in tzname:
-            return tzname[tzname.find('+'):]
-        elif '-' in tzname:
-            return tzname[tzname.find('-'):]
-        return tzname
+        tzname, sign, offset = split_tzname_delta(tzname)
+        return f'{sign}{offset}' if offset else tzname
 
     def _convert_field_to_tz(self, field_name, tzname):
         if not (settings.USE_TZ and tzname):

+ 5 - 4
django/db/backends/postgresql/operations.py

@@ -2,6 +2,7 @@ from psycopg2.extras import Inet
 
 from django.conf import settings
 from django.db.backends.base.operations import BaseDatabaseOperations
+from django.db.backends.utils import split_tzname_delta
 
 
 class DatabaseOperations(BaseDatabaseOperations):
@@ -44,10 +45,10 @@ class DatabaseOperations(BaseDatabaseOperations):
         return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
 
     def _prepare_tzname_delta(self, tzname):
-        if '+' in tzname:
-            return tzname.replace('+', '-')
-        elif '-' in tzname:
-            return tzname.replace('-', '+')
+        tzname, sign, offset = split_tzname_delta(tzname)
+        if offset:
+            sign = '-' if sign == '+' else '+'
+            return f'{tzname}{sign}{offset}'
         return tzname
 
     def _convert_field_to_tz(self, field_name, tzname):

+ 5 - 8
django/db/backends/sqlite3/base.py

@@ -434,14 +434,11 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
     if conn_tzname:
         dt = dt.replace(tzinfo=timezone_constructor(conn_tzname))
     if tzname is not None and tzname != conn_tzname:
-        sign_index = tzname.find('+') + tzname.find('-') + 1
-        if sign_index > -1:
-            sign = tzname[sign_index]
-            tzname, offset = tzname.split(sign)
-            if offset:
-                hours, minutes = offset.split(':')
-                offset_delta = datetime.timedelta(hours=int(hours), minutes=int(minutes))
-                dt += offset_delta if sign == '+' else -offset_delta
+        tzname, sign, offset = backend_utils.split_tzname_delta(tzname)
+        if offset:
+            hours, minutes = offset.split(':')
+            offset_delta = datetime.timedelta(hours=int(hours), minutes=int(minutes))
+            dt += offset_delta if sign == '+' else -offset_delta
         dt = timezone.localtime(dt, timezone_constructor(tzname))
     return dt
 

+ 13 - 0
django/db/backends/utils.py

@@ -7,6 +7,7 @@ from contextlib import contextmanager
 
 from django.db import NotSupportedError
 from django.utils.crypto import md5
+from django.utils.dateparse import parse_time
 
 logger = logging.getLogger('django.db.backends')
 
@@ -130,6 +131,18 @@ class CursorDebugWrapper(CursorWrapper):
             )
 
 
+def split_tzname_delta(tzname):
+    """
+    Split a time zone name into a 3-tuple of (name, sign, offset).
+    """
+    for sign in ['+', '-']:
+        if sign in tzname:
+            name, offset = tzname.rsplit(sign, 1)
+            if offset and parse_time(offset):
+                return name, sign, offset
+    return tzname, None, None
+
+
 ###############################################
 # Converters from database (string) to Python #
 ###############################################

+ 18 - 1
tests/backends/test_utils.py

@@ -3,7 +3,7 @@ from decimal import Decimal, Rounded
 
 from django.db import NotSupportedError, connection
 from django.db.backends.utils import (
-    format_number, split_identifier, truncate_name,
+    format_number, split_identifier, split_tzname_delta, truncate_name,
 )
 from django.test import (
     SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
@@ -57,6 +57,23 @@ class TestUtils(SimpleTestCase):
         with self.assertRaises(Rounded):
             equal('1234567890.1234', 5, None, '1234600000')
 
+    def test_split_tzname_delta(self):
+        tests = [
+            ('Asia/Ust+Nera', ('Asia/Ust+Nera', None, None)),
+            ('Asia/Ust-Nera', ('Asia/Ust-Nera', None, None)),
+            ('Asia/Ust+Nera-02:00', ('Asia/Ust+Nera', '-', '02:00')),
+            ('Asia/Ust-Nera+05:00', ('Asia/Ust-Nera', '+', '05:00')),
+            ('America/Coral_Harbour-01:00', ('America/Coral_Harbour', '-', '01:00')),
+            ('America/Coral_Harbour+02:30', ('America/Coral_Harbour', '+', '02:30')),
+            ('UTC+15:00', ('UTC', '+', '15:00')),
+            ('UTC-04:43', ('UTC', '-', '04:43')),
+            ('UTC', ('UTC', None, None)),
+            ('UTC+1', ('UTC+1', None, None)),
+        ]
+        for tzname, expected in tests:
+            with self.subTest(tzname=tzname):
+                self.assertEqual(split_tzname_delta(tzname), expected)
+
 
 class CursorWrapperTests(TransactionTestCase):
     available_apps = []

+ 23 - 0
tests/db_functions/datetime/test_extract_trunc.py

@@ -1210,6 +1210,29 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
                 self.assertEqual(melb_model.hour, 9)
                 self.assertEqual(melb_model.hour_melb, 9)
 
+    def test_extract_func_with_timezone_minus_no_offset(self):
+        start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
+        end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
+        start_datetime = timezone.make_aware(start_datetime)
+        end_datetime = timezone.make_aware(end_datetime)
+        self.create_model(start_datetime, end_datetime)
+        for ust_nera in self.get_timezones('Asia/Ust-Nera'):
+            with self.subTest(repr(ust_nera)):
+                qs = DTModel.objects.annotate(
+                    hour=ExtractHour('start_datetime'),
+                    hour_tz=ExtractHour('start_datetime', tzinfo=ust_nera),
+                ).order_by('start_datetime')
+
+                utc_model = qs.get()
+                self.assertEqual(utc_model.hour, 23)
+                self.assertEqual(utc_model.hour_tz, 9)
+
+                with timezone.override(ust_nera):
+                    ust_nera_model = qs.get()
+
+                self.assertEqual(ust_nera_model.hour, 9)
+                self.assertEqual(ust_nera_model.hour_tz, 9)
+
     def test_extract_func_explicit_timezone_priority(self):
         start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
         end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)