Fixed #33037 -- Fixed Trunc() with offset timezones on MySQL, SQLite, Oracle.

This commit is contained in:
Shafiya Adzhani 2024-02-03 20:05:15 +07:00 committed by Mariusz Felisiak
parent 2aa8388110
commit 22285d366c
4 changed files with 36 additions and 16 deletions

View File

@ -919,6 +919,7 @@ answer newbie questions, and generally made Django that much better:
Sergey Fedoseev <fedoseev.sergey@gmail.com> Sergey Fedoseev <fedoseev.sergey@gmail.com>
Sergey Kolosov <m17.admin@gmail.com> Sergey Kolosov <m17.admin@gmail.com>
Seth Hill <sethrh@gmail.com> Seth Hill <sethrh@gmail.com>
Shafiya Adzhani <adz.arsym@gmail.com>
Shai Berger <shai@platonix.com> Shai Berger <shai@platonix.com>
Shannon -jj Behrens <https://www.jjinux.com/> Shannon -jj Behrens <https://www.jjinux.com/>
Shawn Milochik <shawn@milochik.com> Shawn Milochik <shawn@milochik.com>

View File

@ -118,7 +118,10 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
hours, minutes = offset.split(":") hours, minutes = offset.split(":")
offset_delta = timedelta(hours=int(hours), minutes=int(minutes)) offset_delta = timedelta(hours=int(hours), minutes=int(minutes))
dt += offset_delta if sign == "+" else -offset_delta dt += offset_delta if sign == "+" else -offset_delta
dt = timezone.localtime(dt, zoneinfo.ZoneInfo(tzname)) # The tzname may originally be just the offset e.g. "+3:00",
# which becomes an empty string after splitting the sign and offset.
# In this case, use the conn_tzname as fallback.
dt = timezone.localtime(dt, zoneinfo.ZoneInfo(tzname or conn_tzname))
return dt return dt

View File

@ -200,6 +200,8 @@ def split_tzname_delta(tzname):
if sign in tzname: if sign in tzname:
name, offset = tzname.rsplit(sign, 1) name, offset = tzname.rsplit(sign, 1)
if offset and parse_time(offset): if offset and parse_time(offset):
if ":" not in offset:
offset = f"{offset}:00"
return name, sign, offset return name, sign, offset
return tzname, None, None return tzname, None, None

View File

@ -1832,17 +1832,18 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
end_datetime = timezone.make_aware(end_datetime) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
self.create_model(end_datetime, start_datetime) self.create_model(end_datetime, start_datetime)
melb = zoneinfo.ZoneInfo("Australia/Melbourne")
def assertDatetimeKind(kind): def assertDatetimeKind(kind, tzinfo):
truncated_start = truncate_to(start_datetime.astimezone(melb), kind, melb) truncated_start = truncate_to(
truncated_end = truncate_to(end_datetime.astimezone(melb), kind, melb) start_datetime.astimezone(tzinfo), kind, tzinfo
)
truncated_end = truncate_to(end_datetime.astimezone(tzinfo), kind, tzinfo)
queryset = DTModel.objects.annotate( queryset = DTModel.objects.annotate(
truncated=Trunc( truncated=Trunc(
"start_datetime", "start_datetime",
kind, kind,
output_field=DateTimeField(), output_field=DateTimeField(),
tzinfo=melb, tzinfo=tzinfo,
) )
).order_by("start_datetime") ).order_by("start_datetime")
self.assertSequenceEqual( self.assertSequenceEqual(
@ -1853,15 +1854,17 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
], ],
) )
def assertDatetimeToDateKind(kind): def assertDatetimeToDateKind(kind, tzinfo):
truncated_start = truncate_to(start_datetime.astimezone(melb).date(), kind) truncated_start = truncate_to(
truncated_end = truncate_to(end_datetime.astimezone(melb).date(), kind) start_datetime.astimezone(tzinfo).date(), kind
)
truncated_end = truncate_to(end_datetime.astimezone(tzinfo).date(), kind)
queryset = DTModel.objects.annotate( queryset = DTModel.objects.annotate(
truncated=Trunc( truncated=Trunc(
"start_datetime", "start_datetime",
kind, kind,
output_field=DateField(), output_field=DateField(),
tzinfo=melb, tzinfo=tzinfo,
), ),
).order_by("start_datetime") ).order_by("start_datetime")
self.assertSequenceEqual( self.assertSequenceEqual(
@ -1872,15 +1875,17 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
], ],
) )
def assertDatetimeToTimeKind(kind): def assertDatetimeToTimeKind(kind, tzinfo):
truncated_start = truncate_to(start_datetime.astimezone(melb).time(), kind) truncated_start = truncate_to(
truncated_end = truncate_to(end_datetime.astimezone(melb).time(), kind) start_datetime.astimezone(tzinfo).time(), kind
)
truncated_end = truncate_to(end_datetime.astimezone(tzinfo).time(), kind)
queryset = DTModel.objects.annotate( queryset = DTModel.objects.annotate(
truncated=Trunc( truncated=Trunc(
"start_datetime", "start_datetime",
kind, kind,
output_field=TimeField(), output_field=TimeField(),
tzinfo=melb, tzinfo=tzinfo,
) )
).order_by("start_datetime") ).order_by("start_datetime")
self.assertSequenceEqual( self.assertSequenceEqual(
@ -1891,6 +1896,10 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
], ],
) )
timezones = [
zoneinfo.ZoneInfo("Australia/Melbourne"),
zoneinfo.ZoneInfo("Etc/GMT+10"),
]
date_truncations = ["year", "quarter", "month", "week", "day"] date_truncations = ["year", "quarter", "month", "week", "day"]
time_truncations = ["hour", "minute", "second"] time_truncations = ["hour", "minute", "second"]
tests = [ tests = [
@ -1900,8 +1909,13 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
] ]
for assertion, truncations in tests: for assertion, truncations in tests:
for truncation in truncations: for truncation in truncations:
with self.subTest(assertion=assertion.__name__, truncation=truncation): for tzinfo in timezones:
assertion(truncation) with self.subTest(
assertion=assertion.__name__,
truncation=truncation,
tzinfo=tzinfo.key,
):
assertion(truncation, tzinfo)
qs = DTModel.objects.filter( qs = DTModel.objects.filter(
start_datetime__date=Trunc( start_datetime__date=Trunc(