Refs #32365 -- Allowed use of non-pytz timezone implementations.
This commit is contained in:
parent
73ffc73b68
commit
10d1261984
|
@ -161,6 +161,11 @@ def from_current_timezone(value):
|
||||||
if settings.USE_TZ and value is not None and timezone.is_naive(value):
|
if settings.USE_TZ and value is not None and timezone.is_naive(value):
|
||||||
current_timezone = timezone.get_current_timezone()
|
current_timezone = timezone.get_current_timezone()
|
||||||
try:
|
try:
|
||||||
|
if (
|
||||||
|
not timezone._is_pytz_zone(current_timezone) and
|
||||||
|
timezone._datetime_ambiguous_or_imaginary(value, current_timezone)
|
||||||
|
):
|
||||||
|
raise ValueError('Ambiguous or non-existent time.')
|
||||||
return timezone.make_aware(value, current_timezone)
|
return timezone.make_aware(value, current_timezone)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|
|
@ -20,7 +20,8 @@ from django.utils.dates import (
|
||||||
)
|
)
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
from django.utils.timezone import (
|
from django.utils.timezone import (
|
||||||
get_default_timezone, is_aware, is_naive, make_aware,
|
_datetime_ambiguous_or_imaginary, get_default_timezone, is_aware, is_naive,
|
||||||
|
make_aware,
|
||||||
)
|
)
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
@ -160,15 +161,9 @@ class TimeFormat(Formatter):
|
||||||
if not self.timezone:
|
if not self.timezone:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
name = None
|
if not _datetime_ambiguous_or_imaginary(self.data, self.timezone):
|
||||||
try:
|
|
||||||
name = self.timezone.tzname(self.data)
|
name = self.timezone.tzname(self.data)
|
||||||
except Exception:
|
else:
|
||||||
# pytz raises AmbiguousTimeError during the autumn DST change.
|
|
||||||
# This happens mainly when __init__ receives a naive datetime
|
|
||||||
# and sets self.timezone = get_default_timezone().
|
|
||||||
pass
|
|
||||||
if name is None:
|
|
||||||
name = self.format('O')
|
name = self.format('O')
|
||||||
return str(name)
|
return str(name)
|
||||||
|
|
||||||
|
@ -184,16 +179,13 @@ class TimeFormat(Formatter):
|
||||||
|
|
||||||
If timezone information is not available, return an empty string.
|
If timezone information is not available, return an empty string.
|
||||||
"""
|
"""
|
||||||
if not self.timezone:
|
if (
|
||||||
|
not self.timezone or
|
||||||
|
_datetime_ambiguous_or_imaginary(self.data, self.timezone)
|
||||||
|
):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
try:
|
offset = self.timezone.utcoffset(self.data)
|
||||||
offset = self.timezone.utcoffset(self.data)
|
|
||||||
except Exception:
|
|
||||||
# pytz raises AmbiguousTimeError during the autumn DST change.
|
|
||||||
# This happens mainly when __init__ receives a naive datetime
|
|
||||||
# and sets self.timezone = get_default_timezone().
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# `offset` is a datetime.timedelta. For negative values (to the west of
|
# `offset` is a datetime.timedelta. For negative values (to the west of
|
||||||
# UTC) only days can be negative (days=-1) and seconds are always
|
# UTC) only days can be negative (days=-1) and seconds are always
|
||||||
|
@ -232,16 +224,12 @@ class DateFormat(TimeFormat):
|
||||||
|
|
||||||
def I(self): # NOQA: E743, E741
|
def I(self): # NOQA: E743, E741
|
||||||
"'1' if Daylight Savings Time, '0' otherwise."
|
"'1' if Daylight Savings Time, '0' otherwise."
|
||||||
try:
|
if (
|
||||||
if self.timezone and self.timezone.dst(self.data):
|
not self.timezone or
|
||||||
return '1'
|
_datetime_ambiguous_or_imaginary(self.data, self.timezone)
|
||||||
else:
|
):
|
||||||
return '0'
|
|
||||||
except Exception:
|
|
||||||
# pytz raises AmbiguousTimeError during the autumn DST change.
|
|
||||||
# This happens mainly when __init__ receives a naive datetime
|
|
||||||
# and sets self.timezone = get_default_timezone().
|
|
||||||
return ''
|
return ''
|
||||||
|
return '1' if self.timezone.dst(self.data) else '0'
|
||||||
|
|
||||||
def j(self):
|
def j(self):
|
||||||
"Day of the month without leading zeros; i.e. '1' to '31'"
|
"Day of the month without leading zeros; i.e. '1' to '31'"
|
||||||
|
|
|
@ -24,6 +24,11 @@ __all__ = [
|
||||||
# UTC time zone as a tzinfo instance.
|
# UTC time zone as a tzinfo instance.
|
||||||
utc = pytz.utc
|
utc = pytz.utc
|
||||||
|
|
||||||
|
_PYTZ_BASE_CLASSES = (pytz.tzinfo.BaseTzInfo, pytz._FixedOffset)
|
||||||
|
# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
|
||||||
|
if not isinstance(pytz.UTC, pytz._FixedOffset):
|
||||||
|
_PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),)
|
||||||
|
|
||||||
|
|
||||||
def get_fixed_timezone(offset):
|
def get_fixed_timezone(offset):
|
||||||
"""Return a tzinfo instance with a fixed offset from UTC."""
|
"""Return a tzinfo instance with a fixed offset from UTC."""
|
||||||
|
@ -68,7 +73,7 @@ def get_current_timezone_name():
|
||||||
|
|
||||||
def _get_timezone_name(timezone):
|
def _get_timezone_name(timezone):
|
||||||
"""Return the name of ``timezone``."""
|
"""Return the name of ``timezone``."""
|
||||||
return timezone.tzname(None)
|
return str(timezone)
|
||||||
|
|
||||||
# Timezone selection functions.
|
# Timezone selection functions.
|
||||||
|
|
||||||
|
@ -229,7 +234,7 @@ def make_aware(value, timezone=None, is_dst=None):
|
||||||
"""Make a naive datetime.datetime in a given time zone aware."""
|
"""Make a naive datetime.datetime in a given time zone aware."""
|
||||||
if timezone is None:
|
if timezone is None:
|
||||||
timezone = get_current_timezone()
|
timezone = get_current_timezone()
|
||||||
if hasattr(timezone, 'localize'):
|
if _is_pytz_zone(timezone):
|
||||||
# This method is available for pytz time zones.
|
# This method is available for pytz time zones.
|
||||||
return timezone.localize(value, is_dst=is_dst)
|
return timezone.localize(value, is_dst=is_dst)
|
||||||
else:
|
else:
|
||||||
|
@ -249,3 +254,20 @@ def make_naive(value, timezone=None):
|
||||||
if is_naive(value):
|
if is_naive(value):
|
||||||
raise ValueError("make_naive() cannot be applied to a naive datetime")
|
raise ValueError("make_naive() cannot be applied to a naive datetime")
|
||||||
return value.astimezone(timezone).replace(tzinfo=None)
|
return value.astimezone(timezone).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_pytz_zone(tz):
|
||||||
|
"""Checks if a zone is a pytz zone."""
|
||||||
|
return isinstance(tz, _PYTZ_BASE_CLASSES)
|
||||||
|
|
||||||
|
|
||||||
|
def _datetime_ambiguous_or_imaginary(dt, tz):
|
||||||
|
if _is_pytz_zone(tz):
|
||||||
|
try:
|
||||||
|
tz.utcoffset(dt)
|
||||||
|
except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)
|
||||||
|
|
|
@ -943,21 +943,24 @@ appropriate entities.
|
||||||
:class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
|
:class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
|
||||||
defaults to the :ref:`current time zone <default-current-time-zone>`.
|
defaults to the :ref:`current time zone <default-current-time-zone>`.
|
||||||
|
|
||||||
The ``pytz.AmbiguousTimeError`` exception is raised if you try to make
|
When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is raised if
|
||||||
``value`` aware during a DST transition where the same time occurs twice
|
you try to make ``value`` aware during a DST transition where the same time
|
||||||
(when reverting from DST). Setting ``is_dst`` to ``True`` or ``False`` will
|
occurs twice (when reverting from DST). Setting ``is_dst`` to ``True`` or
|
||||||
avoid the exception by choosing if the time is pre-transition or
|
``False`` will avoid the exception by choosing if the time is
|
||||||
post-transition respectively.
|
pre-transition or post-transition respectively.
|
||||||
|
|
||||||
The ``pytz.NonExistentTimeError`` exception is raised if you try to make
|
When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is raised
|
||||||
``value`` aware during a DST transition such that the time never occurred.
|
if you try to make ``value`` aware during a DST transition such that the
|
||||||
For example, if the 2:00 hour is skipped during a DST transition, trying to
|
time never occurred. For example, if the 2:00 hour is skipped during a DST
|
||||||
make 2:30 aware in that time zone will raise an exception. To avoid that
|
transition, trying to make 2:30 aware in that time zone will raise an
|
||||||
you can use ``is_dst`` to specify how ``make_aware()`` should interpret
|
exception. To avoid that you can use ``is_dst`` to specify how
|
||||||
such a nonexistent time. If ``is_dst=True`` then the above time would be
|
``make_aware()`` should interpret such a nonexistent time. If
|
||||||
interpreted as 2:30 DST time (equivalent to 1:30 local time). Conversely,
|
``is_dst=True`` then the above time would be interpreted as 2:30 DST time
|
||||||
if ``is_dst=False`` the time would be interpreted as 2:30 standard time
|
(equivalent to 1:30 local time). Conversely, if ``is_dst=False`` the time
|
||||||
(equivalent to 3:30 local time).
|
would be interpreted as 2:30 standard time (equivalent to 3:30 local time).
|
||||||
|
|
||||||
|
The ``is_dst`` parameter has no effect when using non-``pytz`` timezone
|
||||||
|
implementations.
|
||||||
|
|
||||||
.. function:: make_naive(value, timezone=None)
|
.. function:: make_naive(value, timezone=None)
|
||||||
|
|
||||||
|
|
|
@ -657,6 +657,9 @@ MySQL 5.7 and higher.
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
* Django now supports non-``pytz`` time zones, such as Python 3.9+'s
|
||||||
|
:mod:`zoneinfo` module and its backport.
|
||||||
|
|
||||||
* The undocumented ``SpatiaLiteOperations.proj4_version()`` method is renamed
|
* The undocumented ``SpatiaLiteOperations.proj4_version()`` method is renamed
|
||||||
to ``proj_version()``.
|
to ``proj_version()``.
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,15 @@ to this problem is to use UTC in the code and use local time only when
|
||||||
interacting with end users.
|
interacting with end users.
|
||||||
|
|
||||||
Time zone support is disabled by default. To enable it, set :setting:`USE_TZ =
|
Time zone support is disabled by default. To enable it, set :setting:`USE_TZ =
|
||||||
True <USE_TZ>` in your settings file. Time zone support uses pytz_, which is
|
True <USE_TZ>` in your settings file. By default, time zone support uses pytz_,
|
||||||
installed when you install Django.
|
which is installed when you install Django; Django also supports the use of
|
||||||
|
other time zone implementations like :mod:`zoneinfo` by passing
|
||||||
|
:class:`~datetime.tzinfo` objects directly to functions in
|
||||||
|
:mod:`django.utils.timezone`.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
Support for non-``pytz`` timezone implementations was added.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -680,7 +687,8 @@ Usage
|
||||||
|
|
||||||
pytz_ provides helpers_, including a list of current time zones and a list
|
pytz_ provides helpers_, including a list of current time zones and a list
|
||||||
of all available time zones -- some of which are only of historical
|
of all available time zones -- some of which are only of historical
|
||||||
interest.
|
interest. :mod:`zoneinfo` also provides similar functionality via
|
||||||
|
:func:`zoneinfo.available_timezones`.
|
||||||
|
|
||||||
.. _pytz: http://pytz.sourceforge.net/
|
.. _pytz: http://pytz.sourceforge.net/
|
||||||
.. _more examples: http://pytz.sourceforge.net/#example-usage
|
.. _more examples: http://pytz.sourceforge.net/#example-usage
|
||||||
|
|
|
@ -7,6 +7,14 @@ from urllib.parse import parse_qsl, urljoin, urlparse
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from backports import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
zoneinfo = None
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import AdminSite, ModelAdmin
|
from django.contrib.admin import AdminSite, ModelAdmin
|
||||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||||
|
@ -63,6 +71,14 @@ for a staff account. Note that both fields may be case-sensitive."
|
||||||
MULTIPART_ENCTYPE = 'enctype="multipart/form-data"'
|
MULTIPART_ENCTYPE = 'enctype="multipart/form-data"'
|
||||||
|
|
||||||
|
|
||||||
|
def make_aware_datetimes(dt, iana_key):
|
||||||
|
"""Makes one aware datetime for each supported time zone provider."""
|
||||||
|
yield pytz.timezone(iana_key).localize(dt, is_dst=None)
|
||||||
|
|
||||||
|
if zoneinfo is not None:
|
||||||
|
yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key))
|
||||||
|
|
||||||
|
|
||||||
class AdminFieldExtractionMixin:
|
class AdminFieldExtractionMixin:
|
||||||
"""
|
"""
|
||||||
Helper methods for extracting data from AdminForm.
|
Helper methods for extracting data from AdminForm.
|
||||||
|
@ -995,24 +1011,26 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
||||||
@override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True)
|
@override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True)
|
||||||
def test_date_hierarchy_timezone_dst(self):
|
def test_date_hierarchy_timezone_dst(self):
|
||||||
# This datetime doesn't exist in this timezone due to DST.
|
# This datetime doesn't exist in this timezone due to DST.
|
||||||
date = pytz.timezone('America/Sao_Paulo').localize(datetime.datetime(2016, 10, 16, 15), is_dst=None)
|
for date in make_aware_datetimes(datetime.datetime(2016, 10, 16, 15), 'America/Sao_Paulo'):
|
||||||
q = Question.objects.create(question='Why?', expires=date)
|
with self.subTest(repr(date.tzinfo)):
|
||||||
Answer2.objects.create(question=q, answer='Because.')
|
q = Question.objects.create(question='Why?', expires=date)
|
||||||
response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
|
Answer2.objects.create(question=q, answer='Because.')
|
||||||
self.assertContains(response, 'question__expires__day=16')
|
response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
|
||||||
self.assertContains(response, 'question__expires__month=10')
|
self.assertContains(response, 'question__expires__day=16')
|
||||||
self.assertContains(response, 'question__expires__year=2016')
|
self.assertContains(response, 'question__expires__month=10')
|
||||||
|
self.assertContains(response, 'question__expires__year=2016')
|
||||||
|
|
||||||
@override_settings(TIME_ZONE='America/Los_Angeles', USE_TZ=True)
|
@override_settings(TIME_ZONE='America/Los_Angeles', USE_TZ=True)
|
||||||
def test_date_hierarchy_local_date_differ_from_utc(self):
|
def test_date_hierarchy_local_date_differ_from_utc(self):
|
||||||
# This datetime is 2017-01-01 in UTC.
|
# This datetime is 2017-01-01 in UTC.
|
||||||
date = pytz.timezone('America/Los_Angeles').localize(datetime.datetime(2016, 12, 31, 16))
|
for date in make_aware_datetimes(datetime.datetime(2016, 12, 31, 16), 'America/Los_Angeles'):
|
||||||
q = Question.objects.create(question='Why?', expires=date)
|
with self.subTest(repr(date.tzinfo)):
|
||||||
Answer2.objects.create(question=q, answer='Because.')
|
q = Question.objects.create(question='Why?', expires=date)
|
||||||
response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
|
Answer2.objects.create(question=q, answer='Because.')
|
||||||
self.assertContains(response, 'question__expires__day=31')
|
response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
|
||||||
self.assertContains(response, 'question__expires__month=12')
|
self.assertContains(response, 'question__expires__day=31')
|
||||||
self.assertContains(response, 'question__expires__year=2016')
|
self.assertContains(response, 'question__expires__month=12')
|
||||||
|
self.assertContains(response, 'question__expires__year=2016')
|
||||||
|
|
||||||
def test_sortable_by_columns_subset(self):
|
def test_sortable_by_columns_subset(self):
|
||||||
expected_sortable_fields = ('date', 'callable_year')
|
expected_sortable_fields = ('date', 'callable_year')
|
||||||
|
|
|
@ -2,6 +2,14 @@ from datetime import datetime, timedelta, timezone as datetime_timezone
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from backports import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
zoneinfo = None
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
|
DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
|
||||||
|
@ -21,6 +29,10 @@ from django.utils import timezone
|
||||||
|
|
||||||
from ..models import Author, DTModel, Fan
|
from ..models import Author, DTModel, Fan
|
||||||
|
|
||||||
|
ZONE_CONSTRUCTORS = (pytz.timezone,)
|
||||||
|
if zoneinfo is not None:
|
||||||
|
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,)
|
||||||
|
|
||||||
|
|
||||||
def truncate_to(value, kind, tzinfo=None):
|
def truncate_to(value, kind, tzinfo=None):
|
||||||
# Convert to target timezone before truncation
|
# Convert to target timezone before truncation
|
||||||
|
@ -1039,7 +1051,7 @@ class DateFunctionTests(TestCase):
|
||||||
outer = Author.objects.annotate(
|
outer = Author.objects.annotate(
|
||||||
newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField()))
|
newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField()))
|
||||||
)
|
)
|
||||||
tz = pytz.UTC if settings.USE_TZ else None
|
tz = timezone.utc if settings.USE_TZ else None
|
||||||
self.assertSequenceEqual(
|
self.assertSequenceEqual(
|
||||||
outer.order_by('name').values('name', 'newest_fan_year'),
|
outer.order_by('name').values('name', 'newest_fan_year'),
|
||||||
[
|
[
|
||||||
|
@ -1052,63 +1064,68 @@ class DateFunctionTests(TestCase):
|
||||||
@override_settings(USE_TZ=True, TIME_ZONE='UTC')
|
@override_settings(USE_TZ=True, TIME_ZONE='UTC')
|
||||||
class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
|
|
||||||
|
def get_timezones(self, key):
|
||||||
|
for constructor in ZONE_CONSTRUCTORS:
|
||||||
|
yield constructor(key)
|
||||||
|
|
||||||
def test_extract_func_with_timezone(self):
|
def test_extract_func_with_timezone(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)
|
||||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||||
self.create_model(start_datetime, end_datetime)
|
self.create_model(start_datetime, end_datetime)
|
||||||
melb = pytz.timezone('Australia/Melbourne')
|
|
||||||
delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
|
delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
|
||||||
delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
|
delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
|
||||||
|
|
||||||
qs = DTModel.objects.annotate(
|
for melb in self.get_timezones('Australia/Melbourne'):
|
||||||
day=Extract('start_datetime', 'day'),
|
with self.subTest(repr(melb)):
|
||||||
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
|
qs = DTModel.objects.annotate(
|
||||||
week=Extract('start_datetime', 'week', tzinfo=melb),
|
day=Extract('start_datetime', 'day'),
|
||||||
isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
|
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
|
||||||
weekday=ExtractWeekDay('start_datetime'),
|
week=Extract('start_datetime', 'week', tzinfo=melb),
|
||||||
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
|
||||||
isoweekday=ExtractIsoWeekDay('start_datetime'),
|
weekday=ExtractWeekDay('start_datetime'),
|
||||||
isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
|
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
||||||
quarter=ExtractQuarter('start_datetime', tzinfo=melb),
|
isoweekday=ExtractIsoWeekDay('start_datetime'),
|
||||||
hour=ExtractHour('start_datetime'),
|
isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
|
||||||
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
|
quarter=ExtractQuarter('start_datetime', tzinfo=melb),
|
||||||
hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
|
hour=ExtractHour('start_datetime'),
|
||||||
hour_with_delta_neg=ExtractHour('start_datetime', tzinfo=delta_tzinfo_neg),
|
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
|
||||||
minute_with_delta_neg=ExtractMinute('start_datetime', tzinfo=delta_tzinfo_neg),
|
hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
|
||||||
).order_by('start_datetime')
|
hour_with_delta_neg=ExtractHour('start_datetime', tzinfo=delta_tzinfo_neg),
|
||||||
|
minute_with_delta_neg=ExtractMinute('start_datetime', tzinfo=delta_tzinfo_neg),
|
||||||
|
).order_by('start_datetime')
|
||||||
|
|
||||||
utc_model = qs.get()
|
utc_model = qs.get()
|
||||||
self.assertEqual(utc_model.day, 15)
|
self.assertEqual(utc_model.day, 15)
|
||||||
self.assertEqual(utc_model.day_melb, 16)
|
self.assertEqual(utc_model.day_melb, 16)
|
||||||
self.assertEqual(utc_model.week, 25)
|
self.assertEqual(utc_model.week, 25)
|
||||||
self.assertEqual(utc_model.isoyear, 2015)
|
self.assertEqual(utc_model.isoyear, 2015)
|
||||||
self.assertEqual(utc_model.weekday, 2)
|
self.assertEqual(utc_model.weekday, 2)
|
||||||
self.assertEqual(utc_model.weekday_melb, 3)
|
self.assertEqual(utc_model.weekday_melb, 3)
|
||||||
self.assertEqual(utc_model.isoweekday, 1)
|
self.assertEqual(utc_model.isoweekday, 1)
|
||||||
self.assertEqual(utc_model.isoweekday_melb, 2)
|
self.assertEqual(utc_model.isoweekday_melb, 2)
|
||||||
self.assertEqual(utc_model.quarter, 2)
|
self.assertEqual(utc_model.quarter, 2)
|
||||||
self.assertEqual(utc_model.hour, 23)
|
self.assertEqual(utc_model.hour, 23)
|
||||||
self.assertEqual(utc_model.hour_melb, 9)
|
self.assertEqual(utc_model.hour_melb, 9)
|
||||||
self.assertEqual(utc_model.hour_with_delta_pos, 4)
|
self.assertEqual(utc_model.hour_with_delta_pos, 4)
|
||||||
self.assertEqual(utc_model.hour_with_delta_neg, 18)
|
self.assertEqual(utc_model.hour_with_delta_neg, 18)
|
||||||
self.assertEqual(utc_model.minute_with_delta_neg, 47)
|
self.assertEqual(utc_model.minute_with_delta_neg, 47)
|
||||||
|
|
||||||
with timezone.override(melb):
|
with timezone.override(melb):
|
||||||
melb_model = qs.get()
|
melb_model = qs.get()
|
||||||
|
|
||||||
self.assertEqual(melb_model.day, 16)
|
self.assertEqual(melb_model.day, 16)
|
||||||
self.assertEqual(melb_model.day_melb, 16)
|
self.assertEqual(melb_model.day_melb, 16)
|
||||||
self.assertEqual(melb_model.week, 25)
|
self.assertEqual(melb_model.week, 25)
|
||||||
self.assertEqual(melb_model.isoyear, 2015)
|
self.assertEqual(melb_model.isoyear, 2015)
|
||||||
self.assertEqual(melb_model.weekday, 3)
|
self.assertEqual(melb_model.weekday, 3)
|
||||||
self.assertEqual(melb_model.isoweekday, 2)
|
self.assertEqual(melb_model.isoweekday, 2)
|
||||||
self.assertEqual(melb_model.quarter, 2)
|
self.assertEqual(melb_model.quarter, 2)
|
||||||
self.assertEqual(melb_model.weekday_melb, 3)
|
self.assertEqual(melb_model.weekday_melb, 3)
|
||||||
self.assertEqual(melb_model.isoweekday_melb, 2)
|
self.assertEqual(melb_model.isoweekday_melb, 2)
|
||||||
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_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)
|
||||||
|
@ -1116,27 +1133,29 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||||
self.create_model(start_datetime, end_datetime)
|
self.create_model(start_datetime, end_datetime)
|
||||||
melb = pytz.timezone('Australia/Melbourne')
|
|
||||||
|
|
||||||
with timezone.override(melb):
|
for melb in self.get_timezones('Australia/Melbourne'):
|
||||||
model = DTModel.objects.annotate(
|
with self.subTest(repr(melb)):
|
||||||
day_melb=Extract('start_datetime', 'day'),
|
with timezone.override(melb):
|
||||||
day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
|
model = DTModel.objects.annotate(
|
||||||
).order_by('start_datetime').get()
|
day_melb=Extract('start_datetime', 'day'),
|
||||||
self.assertEqual(model.day_melb, 16)
|
day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
|
||||||
self.assertEqual(model.day_utc, 15)
|
).order_by('start_datetime').get()
|
||||||
|
self.assertEqual(model.day_melb, 16)
|
||||||
|
self.assertEqual(model.day_utc, 15)
|
||||||
|
|
||||||
def test_extract_invalid_field_with_timezone(self):
|
def test_extract_invalid_field_with_timezone(self):
|
||||||
melb = pytz.timezone('Australia/Melbourne')
|
for melb in self.get_timezones('Australia/Melbourne'):
|
||||||
msg = 'tzinfo can only be used with DateTimeField.'
|
with self.subTest(repr(melb)):
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
msg = 'tzinfo can only be used with DateTimeField.'
|
||||||
DTModel.objects.annotate(
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
day_melb=Extract('start_date', 'day', tzinfo=melb),
|
DTModel.objects.annotate(
|
||||||
).get()
|
day_melb=Extract('start_date', 'day', tzinfo=melb),
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
).get()
|
||||||
DTModel.objects.annotate(
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
hour_melb=Extract('start_time', 'hour', tzinfo=melb),
|
DTModel.objects.annotate(
|
||||||
).get()
|
hour_melb=Extract('start_time', 'hour', tzinfo=melb),
|
||||||
|
).get()
|
||||||
|
|
||||||
def test_trunc_timezone_applied_before_truncation(self):
|
def test_trunc_timezone_applied_before_truncation(self):
|
||||||
start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
|
start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
|
||||||
|
@ -1145,36 +1164,37 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||||
self.create_model(start_datetime, end_datetime)
|
self.create_model(start_datetime, end_datetime)
|
||||||
|
|
||||||
melb = pytz.timezone('Australia/Melbourne')
|
for melb, pacific in zip(
|
||||||
pacific = pytz.timezone('US/Pacific')
|
self.get_timezones('Australia/Melbourne'), self.get_timezones('America/Los_Angeles')
|
||||||
|
):
|
||||||
|
with self.subTest((repr(melb), repr(pacific))):
|
||||||
|
model = DTModel.objects.annotate(
|
||||||
|
melb_year=TruncYear('start_datetime', tzinfo=melb),
|
||||||
|
pacific_year=TruncYear('start_datetime', tzinfo=pacific),
|
||||||
|
melb_date=TruncDate('start_datetime', tzinfo=melb),
|
||||||
|
pacific_date=TruncDate('start_datetime', tzinfo=pacific),
|
||||||
|
melb_time=TruncTime('start_datetime', tzinfo=melb),
|
||||||
|
pacific_time=TruncTime('start_datetime', tzinfo=pacific),
|
||||||
|
).order_by('start_datetime').get()
|
||||||
|
|
||||||
model = DTModel.objects.annotate(
|
melb_start_datetime = start_datetime.astimezone(melb)
|
||||||
melb_year=TruncYear('start_datetime', tzinfo=melb),
|
pacific_start_datetime = start_datetime.astimezone(pacific)
|
||||||
pacific_year=TruncYear('start_datetime', tzinfo=pacific),
|
self.assertEqual(model.start_datetime, start_datetime)
|
||||||
melb_date=TruncDate('start_datetime', tzinfo=melb),
|
self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
|
||||||
pacific_date=TruncDate('start_datetime', tzinfo=pacific),
|
self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
|
||||||
melb_time=TruncTime('start_datetime', tzinfo=melb),
|
self.assertEqual(model.start_datetime.year, 2016)
|
||||||
pacific_time=TruncTime('start_datetime', tzinfo=pacific),
|
self.assertEqual(model.melb_year.year, 2016)
|
||||||
).order_by('start_datetime').get()
|
self.assertEqual(model.pacific_year.year, 2015)
|
||||||
|
self.assertEqual(model.melb_date, melb_start_datetime.date())
|
||||||
melb_start_datetime = start_datetime.astimezone(melb)
|
self.assertEqual(model.pacific_date, pacific_start_datetime.date())
|
||||||
pacific_start_datetime = start_datetime.astimezone(pacific)
|
self.assertEqual(model.melb_time, melb_start_datetime.time())
|
||||||
self.assertEqual(model.start_datetime, start_datetime)
|
self.assertEqual(model.pacific_time, pacific_start_datetime.time())
|
||||||
self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
|
|
||||||
self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
|
|
||||||
self.assertEqual(model.start_datetime.year, 2016)
|
|
||||||
self.assertEqual(model.melb_year.year, 2016)
|
|
||||||
self.assertEqual(model.pacific_year.year, 2015)
|
|
||||||
self.assertEqual(model.melb_date, melb_start_datetime.date())
|
|
||||||
self.assertEqual(model.pacific_date, pacific_start_datetime.date())
|
|
||||||
self.assertEqual(model.melb_time, melb_start_datetime.time())
|
|
||||||
self.assertEqual(model.pacific_time, pacific_start_datetime.time())
|
|
||||||
|
|
||||||
def test_trunc_ambiguous_and_invalid_times(self):
|
def test_trunc_ambiguous_and_invalid_times(self):
|
||||||
sao = pytz.timezone('America/Sao_Paulo')
|
sao = pytz.timezone('America/Sao_Paulo')
|
||||||
utc = pytz.timezone('UTC')
|
utc = timezone.utc
|
||||||
start_datetime = utc.localize(datetime(2016, 10, 16, 13))
|
start_datetime = datetime(2016, 10, 16, 13, tzinfo=utc)
|
||||||
end_datetime = utc.localize(datetime(2016, 2, 21, 1))
|
end_datetime = datetime(2016, 2, 21, 1, tzinfo=utc)
|
||||||
self.create_model(start_datetime, end_datetime)
|
self.create_model(start_datetime, end_datetime)
|
||||||
with timezone.override(sao):
|
with timezone.override(sao):
|
||||||
with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'):
|
with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'):
|
||||||
|
@ -1206,94 +1226,99 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
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 = pytz.timezone('Australia/Melbourne')
|
for melb in self.get_timezones('Australia/Melbourne'):
|
||||||
|
with self.subTest(repr(melb)):
|
||||||
def test_datetime_kind(kind):
|
def test_datetime_kind(kind):
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
DTModel.objects.annotate(
|
DTModel.objects.annotate(
|
||||||
truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb)
|
truncated=Trunc(
|
||||||
).order_by('start_datetime'),
|
'start_datetime', kind, output_field=DateTimeField(), tzinfo=melb
|
||||||
[
|
)
|
||||||
(start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
|
).order_by('start_datetime'),
|
||||||
(end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
|
[
|
||||||
],
|
(start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
|
||||||
lambda m: (m.start_datetime, m.truncated)
|
(end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
|
||||||
)
|
],
|
||||||
|
lambda m: (m.start_datetime, m.truncated)
|
||||||
def test_datetime_to_date_kind(kind):
|
|
||||||
self.assertQuerysetEqual(
|
|
||||||
DTModel.objects.annotate(
|
|
||||||
truncated=Trunc(
|
|
||||||
'start_datetime',
|
|
||||||
kind,
|
|
||||||
output_field=DateField(),
|
|
||||||
tzinfo=melb,
|
|
||||||
),
|
|
||||||
).order_by('start_datetime'),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
start_datetime,
|
|
||||||
truncate_to(start_datetime.astimezone(melb).date(), kind),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
end_datetime,
|
|
||||||
truncate_to(end_datetime.astimezone(melb).date(), kind),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
lambda m: (m.start_datetime, m.truncated),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_datetime_to_time_kind(kind):
|
|
||||||
self.assertQuerysetEqual(
|
|
||||||
DTModel.objects.annotate(
|
|
||||||
truncated=Trunc(
|
|
||||||
'start_datetime',
|
|
||||||
kind,
|
|
||||||
output_field=TimeField(),
|
|
||||||
tzinfo=melb,
|
|
||||||
)
|
)
|
||||||
).order_by('start_datetime'),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
start_datetime,
|
|
||||||
truncate_to(start_datetime.astimezone(melb).time(), kind),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
end_datetime,
|
|
||||||
truncate_to(end_datetime.astimezone(melb).time(), kind),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
lambda m: (m.start_datetime, m.truncated),
|
|
||||||
)
|
|
||||||
|
|
||||||
test_datetime_to_date_kind('year')
|
def test_datetime_to_date_kind(kind):
|
||||||
test_datetime_to_date_kind('quarter')
|
self.assertQuerysetEqual(
|
||||||
test_datetime_to_date_kind('month')
|
DTModel.objects.annotate(
|
||||||
test_datetime_to_date_kind('week')
|
truncated=Trunc(
|
||||||
test_datetime_to_date_kind('day')
|
'start_datetime',
|
||||||
test_datetime_to_time_kind('hour')
|
kind,
|
||||||
test_datetime_to_time_kind('minute')
|
output_field=DateField(),
|
||||||
test_datetime_to_time_kind('second')
|
tzinfo=melb,
|
||||||
test_datetime_kind('year')
|
),
|
||||||
test_datetime_kind('quarter')
|
).order_by('start_datetime'),
|
||||||
test_datetime_kind('month')
|
[
|
||||||
test_datetime_kind('week')
|
(
|
||||||
test_datetime_kind('day')
|
start_datetime,
|
||||||
test_datetime_kind('hour')
|
truncate_to(start_datetime.astimezone(melb).date(), kind),
|
||||||
test_datetime_kind('minute')
|
),
|
||||||
test_datetime_kind('second')
|
(
|
||||||
|
end_datetime,
|
||||||
|
truncate_to(end_datetime.astimezone(melb).date(), kind),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
lambda m: (m.start_datetime, m.truncated),
|
||||||
|
)
|
||||||
|
|
||||||
qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
|
def test_datetime_to_time_kind(kind):
|
||||||
self.assertEqual(qs.count(), 2)
|
self.assertQuerysetEqual(
|
||||||
|
DTModel.objects.annotate(
|
||||||
|
truncated=Trunc(
|
||||||
|
'start_datetime',
|
||||||
|
kind,
|
||||||
|
output_field=TimeField(),
|
||||||
|
tzinfo=melb,
|
||||||
|
)
|
||||||
|
).order_by('start_datetime'),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
start_datetime,
|
||||||
|
truncate_to(start_datetime.astimezone(melb).time(), kind),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
end_datetime,
|
||||||
|
truncate_to(end_datetime.astimezone(melb).time(), kind),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
lambda m: (m.start_datetime, m.truncated),
|
||||||
|
)
|
||||||
|
|
||||||
|
test_datetime_to_date_kind('year')
|
||||||
|
test_datetime_to_date_kind('quarter')
|
||||||
|
test_datetime_to_date_kind('month')
|
||||||
|
test_datetime_to_date_kind('week')
|
||||||
|
test_datetime_to_date_kind('day')
|
||||||
|
test_datetime_to_time_kind('hour')
|
||||||
|
test_datetime_to_time_kind('minute')
|
||||||
|
test_datetime_to_time_kind('second')
|
||||||
|
test_datetime_kind('year')
|
||||||
|
test_datetime_kind('quarter')
|
||||||
|
test_datetime_kind('month')
|
||||||
|
test_datetime_kind('week')
|
||||||
|
test_datetime_kind('day')
|
||||||
|
test_datetime_kind('hour')
|
||||||
|
test_datetime_kind('minute')
|
||||||
|
test_datetime_kind('second')
|
||||||
|
|
||||||
|
qs = DTModel.objects.filter(
|
||||||
|
start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField())
|
||||||
|
)
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
|
||||||
def test_trunc_invalid_field_with_timezone(self):
|
def test_trunc_invalid_field_with_timezone(self):
|
||||||
melb = pytz.timezone('Australia/Melbourne')
|
for melb in self.get_timezones('Australia/Melbourne'):
|
||||||
msg = 'tzinfo can only be used with DateTimeField.'
|
with self.subTest(repr(melb)):
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
msg = 'tzinfo can only be used with DateTimeField.'
|
||||||
DTModel.objects.annotate(
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
day_melb=Trunc('start_date', 'day', tzinfo=melb),
|
DTModel.objects.annotate(
|
||||||
).get()
|
day_melb=Trunc('start_date', 'day', tzinfo=melb),
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
).get()
|
||||||
DTModel.objects.annotate(
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
hour_melb=Trunc('start_time', 'hour', tzinfo=melb),
|
DTModel.objects.annotate(
|
||||||
).get()
|
hour_melb=Trunc('start_time', 'hour', tzinfo=melb),
|
||||||
|
).get()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
asgiref >= 3.2.10
|
asgiref >= 3.2.10
|
||||||
argon2-cffi >= 16.1.0
|
argon2-cffi >= 16.1.0
|
||||||
|
backports.zoneinfo; python_version < '3.9'
|
||||||
bcrypt
|
bcrypt
|
||||||
docutils
|
docutils
|
||||||
geoip2
|
geoip2
|
||||||
|
@ -17,4 +18,5 @@ PyYAML
|
||||||
selenium
|
selenium
|
||||||
sqlparse >= 0.2.2
|
sqlparse >= 0.2.2
|
||||||
tblib >= 1.5.0
|
tblib >= 1.5.0
|
||||||
|
tzdata
|
||||||
colorama; sys.platform == 'win32'
|
colorama; sys.platform == 'win32'
|
||||||
|
|
|
@ -7,6 +7,14 @@ from xml.dom.minidom import parseString
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from backports import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
zoneinfo = None
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
@ -51,6 +59,14 @@ UTC = timezone.utc
|
||||||
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
|
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
|
||||||
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
|
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
|
||||||
|
|
||||||
|
ZONE_CONSTRUCTORS = (pytz.timezone,)
|
||||||
|
if zoneinfo is not None:
|
||||||
|
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,)
|
||||||
|
|
||||||
|
|
||||||
|
def get_timezones(key):
|
||||||
|
return [constructor(key) for constructor in ZONE_CONSTRUCTORS]
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def override_database_connection_timezone(timezone):
|
def override_database_connection_timezone(timezone):
|
||||||
|
@ -326,16 +342,17 @@ class NewDatabaseTests(TestCase):
|
||||||
self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
|
self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
|
||||||
|
|
||||||
def test_query_filter_with_pytz_timezones(self):
|
def test_query_filter_with_pytz_timezones(self):
|
||||||
tz = pytz.timezone('Europe/Paris')
|
for tz in get_timezones('Europe/Paris'):
|
||||||
dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
|
with self.subTest(repr(tz)):
|
||||||
Event.objects.create(dt=dt)
|
dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
|
||||||
next = dt + datetime.timedelta(seconds=3)
|
Event.objects.create(dt=dt)
|
||||||
prev = dt - datetime.timedelta(seconds=3)
|
next = dt + datetime.timedelta(seconds=3)
|
||||||
self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
|
prev = dt - datetime.timedelta(seconds=3)
|
||||||
self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
|
self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
|
||||||
self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
|
self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
|
||||||
self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
|
self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
|
||||||
self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1)
|
self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
|
||||||
|
self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1)
|
||||||
|
|
||||||
def test_query_convert_timezones(self):
|
def test_query_convert_timezones(self):
|
||||||
# Connection timezone is equal to the current timezone, datetime
|
# Connection timezone is equal to the current timezone, datetime
|
||||||
|
@ -543,7 +560,7 @@ class NewDatabaseTests(TestCase):
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('SELECT CURRENT_TIMESTAMP')
|
cursor.execute('SELECT CURRENT_TIMESTAMP')
|
||||||
now = cursor.fetchone()[0]
|
now = cursor.fetchone()[0]
|
||||||
self.assertEqual(now.tzinfo.zone, 'Europe/Paris')
|
self.assertEqual(str(now.tzinfo), 'Europe/Paris')
|
||||||
|
|
||||||
@requires_tz_support
|
@requires_tz_support
|
||||||
def test_filter_date_field_with_aware_datetime(self):
|
def test_filter_date_field_with_aware_datetime(self):
|
||||||
|
@ -871,32 +888,26 @@ class TemplateTests(SimpleTestCase):
|
||||||
expected = results[k1][k2]
|
expected = results[k1][k2]
|
||||||
self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected))
|
self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected))
|
||||||
|
|
||||||
def test_localtime_filters_with_pytz(self):
|
def test_localtime_filters_with_iana(self):
|
||||||
"""
|
"""
|
||||||
Test the |localtime, |utc, and |timezone filters with pytz.
|
Test the |localtime, |utc, and |timezone filters with iana zones.
|
||||||
"""
|
"""
|
||||||
# Use a pytz timezone as local time
|
# Use an IANA timezone as local time
|
||||||
tpl = Template("{% load tz %}{{ dt|localtime }}|{{ dt|utc }}")
|
tpl = Template("{% load tz %}{{ dt|localtime }}|{{ dt|utc }}")
|
||||||
ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)})
|
ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)})
|
||||||
|
|
||||||
with self.settings(TIME_ZONE='Europe/Paris'):
|
with self.settings(TIME_ZONE='Europe/Paris'):
|
||||||
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00|2011-09-01T10:20:30+00:00")
|
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00|2011-09-01T10:20:30+00:00")
|
||||||
|
|
||||||
# Use a pytz timezone as argument
|
# Use an IANA timezone as argument
|
||||||
tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
|
for tz in get_timezones('Europe/Paris'):
|
||||||
ctx = Context({
|
with self.subTest(repr(tz)):
|
||||||
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
|
tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
|
||||||
'tz': pytz.timezone('Europe/Paris'),
|
ctx = Context({
|
||||||
})
|
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
|
||||||
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
|
'tz': pytz.timezone('Europe/Paris'),
|
||||||
|
})
|
||||||
# Use a pytz timezone name as argument
|
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
|
||||||
tpl = Template("{% load tz %}{{ dt|timezone:'Europe/Paris' }}")
|
|
||||||
ctx = Context({
|
|
||||||
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
|
|
||||||
'tz': pytz.timezone('Europe/Paris'),
|
|
||||||
})
|
|
||||||
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
|
|
||||||
|
|
||||||
def test_localtime_templatetag_invalid_argument(self):
|
def test_localtime_templatetag_invalid_argument(self):
|
||||||
with self.assertRaises(TemplateSyntaxError):
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
@ -945,20 +956,22 @@ class TemplateTests(SimpleTestCase):
|
||||||
"2011-09-01T13:20:30+03:00|2011-09-01T17:20:30+07:00|2011-09-01T13:20:30+03:00"
|
"2011-09-01T13:20:30+03:00|2011-09-01T17:20:30+07:00|2011-09-01T13:20:30+03:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_timezone_templatetag_with_pytz(self):
|
def test_timezone_templatetag_with_iana(self):
|
||||||
"""
|
"""
|
||||||
Test the {% timezone %} templatetag with pytz.
|
Test the {% timezone %} templatetag with IANA time zone providers.
|
||||||
"""
|
"""
|
||||||
tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
|
tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
|
||||||
|
|
||||||
# Use a pytz timezone as argument
|
# Use a IANA timezone as argument
|
||||||
ctx = Context({
|
for tz in get_timezones('Europe/Paris'):
|
||||||
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
|
with self.subTest(repr(tz)):
|
||||||
'tz': pytz.timezone('Europe/Paris'),
|
ctx = Context({
|
||||||
})
|
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
|
||||||
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
|
'tz': tz,
|
||||||
|
})
|
||||||
|
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
|
||||||
|
|
||||||
# Use a pytz timezone name as argument
|
# Use a IANA timezone name as argument
|
||||||
ctx = Context({
|
ctx = Context({
|
||||||
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
|
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
|
||||||
'tz': 'Europe/Paris',
|
'tz': 'Europe/Paris',
|
||||||
|
@ -991,13 +1004,15 @@ class TemplateTests(SimpleTestCase):
|
||||||
with timezone.override(UTC):
|
with timezone.override(UTC):
|
||||||
self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700")
|
self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700")
|
||||||
|
|
||||||
def test_get_current_timezone_templatetag_with_pytz(self):
|
def test_get_current_timezone_templatetag_with_iana(self):
|
||||||
"""
|
"""
|
||||||
Test the {% get_current_timezone %} templatetag with pytz.
|
Test the {% get_current_timezone %} templatetag with pytz.
|
||||||
"""
|
"""
|
||||||
tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
|
tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
|
||||||
with timezone.override(pytz.timezone('Europe/Paris')):
|
for tz in get_timezones('Europe/Paris'):
|
||||||
self.assertEqual(tpl.render(Context()), "Europe/Paris")
|
with self.subTest(repr(tz)):
|
||||||
|
with timezone.override(tz):
|
||||||
|
self.assertEqual(tpl.render(Context()), "Europe/Paris")
|
||||||
|
|
||||||
tpl = Template(
|
tpl = Template(
|
||||||
"{% load tz %}{% timezone 'Europe/Paris' %}"
|
"{% load tz %}{% timezone 'Europe/Paris' %}"
|
||||||
|
@ -1059,17 +1074,21 @@ class LegacyFormsTests(TestCase):
|
||||||
|
|
||||||
def test_form_with_non_existent_time(self):
|
def test_form_with_non_existent_time(self):
|
||||||
form = EventForm({'dt': '2011-03-27 02:30:00'})
|
form = EventForm({'dt': '2011-03-27 02:30:00'})
|
||||||
with timezone.override(pytz.timezone('Europe/Paris')):
|
for tz in get_timezones('Europe/Paris'):
|
||||||
# This is a bug.
|
with self.subTest(repr(tz)):
|
||||||
self.assertTrue(form.is_valid())
|
with timezone.override(tz):
|
||||||
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0))
|
# This is a bug.
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0))
|
||||||
|
|
||||||
def test_form_with_ambiguous_time(self):
|
def test_form_with_ambiguous_time(self):
|
||||||
form = EventForm({'dt': '2011-10-30 02:30:00'})
|
form = EventForm({'dt': '2011-10-30 02:30:00'})
|
||||||
with timezone.override(pytz.timezone('Europe/Paris')):
|
for tz in get_timezones('Europe/Paris'):
|
||||||
# This is a bug.
|
with self.subTest(repr(tz)):
|
||||||
self.assertTrue(form.is_valid())
|
with timezone.override(tz):
|
||||||
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0))
|
# This is a bug.
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0))
|
||||||
|
|
||||||
def test_split_form(self):
|
def test_split_form(self):
|
||||||
form = EventSplitForm({'dt_0': '2011-09-01', 'dt_1': '13:20:30'})
|
form = EventSplitForm({'dt_0': '2011-09-01', 'dt_1': '13:20:30'})
|
||||||
|
@ -1098,26 +1117,30 @@ class NewFormsTests(TestCase):
|
||||||
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
|
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
|
||||||
|
|
||||||
def test_form_with_non_existent_time(self):
|
def test_form_with_non_existent_time(self):
|
||||||
with timezone.override(pytz.timezone('Europe/Paris')):
|
for tz in get_timezones('Europe/Paris'):
|
||||||
form = EventForm({'dt': '2011-03-27 02:30:00'})
|
with self.subTest(repr(tz)):
|
||||||
self.assertFalse(form.is_valid())
|
with timezone.override(tz):
|
||||||
self.assertEqual(
|
form = EventForm({'dt': '2011-03-27 02:30:00'})
|
||||||
form.errors['dt'], [
|
self.assertFalse(form.is_valid())
|
||||||
'2011-03-27 02:30:00 couldn’t be interpreted in time zone '
|
self.assertEqual(
|
||||||
'Europe/Paris; it may be ambiguous or it may not exist.'
|
form.errors['dt'], [
|
||||||
]
|
'2011-03-27 02:30:00 couldn’t be interpreted in time zone '
|
||||||
)
|
'Europe/Paris; it may be ambiguous or it may not exist.'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def test_form_with_ambiguous_time(self):
|
def test_form_with_ambiguous_time(self):
|
||||||
with timezone.override(pytz.timezone('Europe/Paris')):
|
for tz in get_timezones('Europe/Paris'):
|
||||||
form = EventForm({'dt': '2011-10-30 02:30:00'})
|
with self.subTest(repr(tz)):
|
||||||
self.assertFalse(form.is_valid())
|
with timezone.override(tz):
|
||||||
self.assertEqual(
|
form = EventForm({'dt': '2011-10-30 02:30:00'})
|
||||||
form.errors['dt'], [
|
self.assertFalse(form.is_valid())
|
||||||
'2011-10-30 02:30:00 couldn’t be interpreted in time zone '
|
self.assertEqual(
|
||||||
'Europe/Paris; it may be ambiguous or it may not exist.'
|
form.errors['dt'], [
|
||||||
]
|
'2011-10-30 02:30:00 couldn’t be interpreted in time zone '
|
||||||
)
|
'Europe/Paris; it may be ambiguous or it may not exist.'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@requires_tz_support
|
@requires_tz_support
|
||||||
def test_split_form(self):
|
def test_split_form(self):
|
||||||
|
|
|
@ -1,14 +1,38 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from backports import zoneinfo
|
||||||
|
except ImportError:
|
||||||
|
zoneinfo = None
|
||||||
|
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
CET = pytz.timezone("Europe/Paris")
|
CET = pytz.timezone("Europe/Paris")
|
||||||
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
|
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
|
||||||
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
|
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
|
||||||
|
UTC = datetime.timezone.utc
|
||||||
|
|
||||||
|
HAS_ZONEINFO = zoneinfo is not None
|
||||||
|
|
||||||
|
if not HAS_ZONEINFO:
|
||||||
|
PARIS_ZI = None
|
||||||
|
PARIS_IMPLS = (CET,)
|
||||||
|
|
||||||
|
needs_zoneinfo = unittest.skip("Test requires zoneinfo")
|
||||||
|
else:
|
||||||
|
PARIS_ZI = zoneinfo.ZoneInfo('Europe/Paris')
|
||||||
|
PARIS_IMPLS = (CET, PARIS_ZI)
|
||||||
|
|
||||||
|
def needs_zoneinfo(f):
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
class TimezoneTests(SimpleTestCase):
|
class TimezoneTests(SimpleTestCase):
|
||||||
|
@ -142,13 +166,21 @@ class TimezoneTests(SimpleTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_make_aware2(self):
|
def test_make_aware2(self):
|
||||||
self.assertEqual(
|
CEST = datetime.timezone(datetime.timedelta(hours=2), 'CEST')
|
||||||
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), CET),
|
for tz in PARIS_IMPLS:
|
||||||
CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)))
|
with self.subTest(repr(tz)):
|
||||||
|
self.assertEqual(
|
||||||
|
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz),
|
||||||
|
datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST))
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET)
|
timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET)
|
||||||
|
|
||||||
def test_make_aware_pytz(self):
|
if HAS_ZONEINFO:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI)
|
||||||
|
|
||||||
|
def test_make_naive_pytz(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET),
|
timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET),
|
||||||
datetime.datetime(2011, 9, 1, 12, 20, 30))
|
datetime.datetime(2011, 9, 1, 12, 20, 30))
|
||||||
|
@ -160,6 +192,18 @@ class TimezoneTests(SimpleTestCase):
|
||||||
with self.assertRaisesMessage(ValueError, 'make_naive() cannot be applied to a naive datetime'):
|
with self.assertRaisesMessage(ValueError, 'make_naive() cannot be applied to a naive datetime'):
|
||||||
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
|
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
|
||||||
|
|
||||||
|
@needs_zoneinfo
|
||||||
|
def test_make_naive_zoneinfo(self):
|
||||||
|
self.assertEqual(
|
||||||
|
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI),
|
||||||
|
datetime.datetime(2011, 9, 1, 12, 20, 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1, tzinfo=PARIS_ZI), PARIS_ZI),
|
||||||
|
datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1)
|
||||||
|
)
|
||||||
|
|
||||||
def test_make_aware_pytz_ambiguous(self):
|
def test_make_aware_pytz_ambiguous(self):
|
||||||
# 2:30 happens twice, once before DST ends and once after
|
# 2:30 happens twice, once before DST ends and once after
|
||||||
ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
|
ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
|
||||||
|
@ -173,6 +217,21 @@ class TimezoneTests(SimpleTestCase):
|
||||||
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
||||||
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
|
@needs_zoneinfo
|
||||||
|
def test_make_aware_zoneinfo_ambiguous(self):
|
||||||
|
# 2:30 happens twice, once before DST ends and once after
|
||||||
|
ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
|
||||||
|
|
||||||
|
std = timezone.make_aware(ambiguous.replace(fold=1), timezone=PARIS_ZI)
|
||||||
|
dst = timezone.make_aware(ambiguous, timezone=PARIS_ZI)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
std.astimezone(UTC) - dst.astimezone(UTC),
|
||||||
|
datetime.timedelta(hours=1)
|
||||||
|
)
|
||||||
|
self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
def test_make_aware_pytz_non_existent(self):
|
def test_make_aware_pytz_non_existent(self):
|
||||||
# 2:30 never happened due to DST
|
# 2:30 never happened due to DST
|
||||||
non_existent = datetime.datetime(2015, 3, 29, 2, 30)
|
non_existent = datetime.datetime(2015, 3, 29, 2, 30)
|
||||||
|
@ -186,6 +245,21 @@ class TimezoneTests(SimpleTestCase):
|
||||||
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
||||||
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
|
@needs_zoneinfo
|
||||||
|
def test_make_aware_zoneinfo_non_existent(self):
|
||||||
|
# 2:30 never happened due to DST
|
||||||
|
non_existent = datetime.datetime(2015, 3, 29, 2, 30)
|
||||||
|
|
||||||
|
std = timezone.make_aware(non_existent, PARIS_ZI)
|
||||||
|
dst = timezone.make_aware(non_existent.replace(fold=1), PARIS_ZI)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
std.astimezone(UTC) - dst.astimezone(UTC),
|
||||||
|
datetime.timedelta(hours=1)
|
||||||
|
)
|
||||||
|
self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
def test_get_default_timezone(self):
|
def test_get_default_timezone(self):
|
||||||
self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago')
|
self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue