mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[4.0.x] Fixed #33279 -- Fixed handling time zones with "-" sign in names.
Thanks yakimka for the report. Regression infde9b7d35e. Backport of661316b066from main.
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							45de30dc69
						
					
				
				
					commit
					d54aa49a7d
				
			| @@ -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.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  |  | ||||||
| @@ -76,11 +77,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): | ||||||
|   | |||||||
| @@ -433,14 +433,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 @@ import time | |||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
|  |  | ||||||
| from django.db import NotSupportedError | from django.db import NotSupportedError | ||||||
|  | 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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user