mirror of https://github.com/django/django.git
Fixed #33279 -- Fixed handling time zones with "-" sign in names.
Thanks yakimka for the report.
Regression in fde9b7d35e
.
This commit is contained in:
parent
78163d1ac4
commit
661316b066
|
@ -2,6 +2,7 @@ import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.backends.base.operations import BaseDatabaseOperations
|
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.db.models import Exists, ExpressionWrapper, Lookup
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
@ -77,11 +78,8 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return "DATE(%s)" % (field_name)
|
return "DATE(%s)" % (field_name)
|
||||||
|
|
||||||
def _prepare_tzname_delta(self, tzname):
|
def _prepare_tzname_delta(self, tzname):
|
||||||
if '+' in tzname:
|
tzname, sign, offset = split_tzname_delta(tzname)
|
||||||
return tzname[tzname.find('+'):]
|
return f'{sign}{offset}' if offset else tzname
|
||||||
elif '-' in tzname:
|
|
||||||
return tzname[tzname.find('-'):]
|
|
||||||
return tzname
|
|
||||||
|
|
||||||
def _convert_field_to_tz(self, field_name, tzname):
|
def _convert_field_to_tz(self, field_name, tzname):
|
||||||
if tzname and settings.USE_TZ and self.connection.timezone_name != tzname:
|
if tzname and settings.USE_TZ and self.connection.timezone_name != tzname:
|
||||||
|
|
|
@ -5,7 +5,9 @@ from functools import lru_cache
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import DatabaseError, NotSupportedError
|
from django.db import DatabaseError, NotSupportedError
|
||||||
from django.db.backends.base.operations import BaseDatabaseOperations
|
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 import AutoField, Exists, ExpressionWrapper, Lookup
|
||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
from django.db.models.sql.where import WhereNode
|
from django.db.models.sql.where import WhereNode
|
||||||
|
@ -108,11 +110,8 @@ END;
|
||||||
_tzname_re = _lazy_re_compile(r'^[\w/:+-]+$')
|
_tzname_re = _lazy_re_compile(r'^[\w/:+-]+$')
|
||||||
|
|
||||||
def _prepare_tzname_delta(self, tzname):
|
def _prepare_tzname_delta(self, tzname):
|
||||||
if '+' in tzname:
|
tzname, sign, offset = split_tzname_delta(tzname)
|
||||||
return tzname[tzname.find('+'):]
|
return f'{sign}{offset}' if offset else tzname
|
||||||
elif '-' in tzname:
|
|
||||||
return tzname[tzname.find('-'):]
|
|
||||||
return tzname
|
|
||||||
|
|
||||||
def _convert_field_to_tz(self, field_name, tzname):
|
def _convert_field_to_tz(self, field_name, tzname):
|
||||||
if not (settings.USE_TZ and tzname):
|
if not (settings.USE_TZ and tzname):
|
||||||
|
|
|
@ -2,6 +2,7 @@ from psycopg2.extras import Inet
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.backends.base.operations import BaseDatabaseOperations
|
from django.db.backends.base.operations import BaseDatabaseOperations
|
||||||
|
from django.db.backends.utils import split_tzname_delta
|
||||||
|
|
||||||
|
|
||||||
class DatabaseOperations(BaseDatabaseOperations):
|
class DatabaseOperations(BaseDatabaseOperations):
|
||||||
|
@ -44,10 +45,10 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
|
return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
|
||||||
|
|
||||||
def _prepare_tzname_delta(self, tzname):
|
def _prepare_tzname_delta(self, tzname):
|
||||||
if '+' in tzname:
|
tzname, sign, offset = split_tzname_delta(tzname)
|
||||||
return tzname.replace('+', '-')
|
if offset:
|
||||||
elif '-' in tzname:
|
sign = '-' if sign == '+' else '+'
|
||||||
return tzname.replace('-', '+')
|
return f'{tzname}{sign}{offset}'
|
||||||
return tzname
|
return tzname
|
||||||
|
|
||||||
def _convert_field_to_tz(self, field_name, tzname):
|
def _convert_field_to_tz(self, field_name, tzname):
|
||||||
|
|
|
@ -434,14 +434,11 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
|
||||||
if conn_tzname:
|
if conn_tzname:
|
||||||
dt = dt.replace(tzinfo=timezone_constructor(conn_tzname))
|
dt = dt.replace(tzinfo=timezone_constructor(conn_tzname))
|
||||||
if tzname is not None and tzname != conn_tzname:
|
if tzname is not None and tzname != conn_tzname:
|
||||||
sign_index = tzname.find('+') + tzname.find('-') + 1
|
tzname, sign, offset = backend_utils.split_tzname_delta(tzname)
|
||||||
if sign_index > -1:
|
if offset:
|
||||||
sign = tzname[sign_index]
|
hours, minutes = offset.split(':')
|
||||||
tzname, offset = tzname.split(sign)
|
offset_delta = datetime.timedelta(hours=int(hours), minutes=int(minutes))
|
||||||
if offset:
|
dt += offset_delta if sign == '+' else -offset_delta
|
||||||
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))
|
dt = timezone.localtime(dt, timezone_constructor(tzname))
|
||||||
return dt
|
return dt
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||||
|
|
||||||
from django.db import NotSupportedError
|
from django.db import NotSupportedError
|
||||||
from django.utils.crypto import md5
|
from django.utils.crypto import md5
|
||||||
|
from django.utils.dateparse import parse_time
|
||||||
|
|
||||||
logger = logging.getLogger('django.db.backends')
|
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 #
|
# Converters from database (string) to Python #
|
||||||
###############################################
|
###############################################
|
||||||
|
|
|
@ -3,7 +3,7 @@ from decimal import Decimal, Rounded
|
||||||
|
|
||||||
from django.db import NotSupportedError, connection
|
from django.db import NotSupportedError, connection
|
||||||
from django.db.backends.utils import (
|
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 (
|
from django.test import (
|
||||||
SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
||||||
|
@ -57,6 +57,23 @@ class TestUtils(SimpleTestCase):
|
||||||
with self.assertRaises(Rounded):
|
with self.assertRaises(Rounded):
|
||||||
equal('1234567890.1234', 5, None, '1234600000')
|
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):
|
class CursorWrapperTests(TransactionTestCase):
|
||||||
available_apps = []
|
available_apps = []
|
||||||
|
|
|
@ -1210,6 +1210,29 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
self.assertEqual(melb_model.hour, 9)
|
self.assertEqual(melb_model.hour, 9)
|
||||||
self.assertEqual(melb_model.hour_melb, 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):
|
def test_extract_func_explicit_timezone_priority(self):
|
||||||
start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
|
start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
|
||||||
end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
|
end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
|
||||||
|
|
Loading…
Reference in New Issue