mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +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.db.backends.base.operations import BaseDatabaseOperations | ||||
| from django.db.backends.utils import split_tzname_delta | ||||
| from django.utils import timezone | ||||
| from django.utils.encoding import force_str | ||||
|  | ||||
| @@ -76,11 +77,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,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): | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -433,14 +433,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 | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import time | ||||
| from contextlib import contextmanager | ||||
|  | ||||
| from django.db import NotSupportedError | ||||
| 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 # | ||||
| ############################################### | ||||
|   | ||||
| @@ -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 = [] | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user