mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
Fix DateTimeField TZ handling (#5435)
* Add failing TZ tests for DateTimeField - tests "current" timezone activation - tests output for non-UTC timezones * Update DateTimeField TZ aware/naive test output * Fix DateTimeField TZ handling * Add Release Note for BC change
This commit is contained in:
parent
89daaf6276
commit
7d6d043531
|
@ -44,7 +44,11 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
* Fix `DjangoModelPermissions` to ensure user authentication before calling the view's `get_queryset()` method. As a side effect, this changes the order of the HTTP method permissions and authentication checks, and 405 responses will only be returned when authenticated. If you want to replicate the old behavior, see the PR for details. [#5376][gh5376]
|
* Fix `DjangoModelPermissions` to ensure user authentication before calling the view's `get_queryset()` method. As a side effect, this changes the order of the HTTP method permissions and authentication checks, and 405 responses will only be returned when authenticated. If you want to replicate the old behavior, see the PR for details. [#5376][gh5376]
|
||||||
* Deprecated `exclude_from_schema` on `APIView` and `api_view` decorator. Set `schema = None` or `@schema(None)` as appropriate. [#5422][gh5422]
|
* Deprecated `exclude_from_schema` on `APIView` and `api_view` decorator. Set `schema = None` or `@schema(None)` as appropriate. [#5422][gh5422]
|
||||||
|
* Timezone-aware `DateTimeField`s now respect active or default) `timezone` during serialization, instead of always using UTC.
|
||||||
|
|
||||||
|
Resolves inconsistency whereby instances were serialised with supplied datetime for `create` but UTC for `retrieve`. [#3732][gh3732]
|
||||||
|
|
||||||
|
**Possible backwards compatibility break** if you were relying on datetime strings being UTC. Have client interpret datetimes or [set default or active timezone (docs)][djangodocs-set-timezone] to UTC if needed.
|
||||||
|
|
||||||
### 3.6.4
|
### 3.6.4
|
||||||
|
|
||||||
|
@ -1426,3 +1430,6 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
||||||
<!-- 3.6.5 -->
|
<!-- 3.6.5 -->
|
||||||
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376
|
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376
|
||||||
[gh5422]: https://github.com/encode/django-rest-framework/issues/5422
|
[gh5422]: https://github.com/encode/django-rest-framework/issues/5422
|
||||||
|
[gh5408]: https://github.com/encode/django-rest-framework/issues/5408
|
||||||
|
[gh3732]: https://github.com/encode/django-rest-framework/issues/3732
|
||||||
|
[djangodocs-set-timezone]: https://docs.djangoproject.com/en/1.11/topics/i18n/timezones/#default-time-zone-and-current-time-zone
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# Optional packages which may be used with REST framework.
|
# Optional packages which may be used with REST framework.
|
||||||
|
pytz==2017.2
|
||||||
markdown==2.6.4
|
markdown==2.6.4
|
||||||
django-guardian==1.4.8
|
django-guardian==1.4.8
|
||||||
django-filter==1.0.4
|
django-filter==1.0.4
|
||||||
|
|
|
@ -1125,7 +1125,9 @@ class DateTimeField(Field):
|
||||||
"""
|
"""
|
||||||
field_timezone = getattr(self, 'timezone', self.default_timezone())
|
field_timezone = getattr(self, 'timezone', self.default_timezone())
|
||||||
|
|
||||||
if (field_timezone is not None) and not timezone.is_aware(value):
|
if field_timezone is not None:
|
||||||
|
if timezone.is_aware(value):
|
||||||
|
return value.astimezone(field_timezone)
|
||||||
try:
|
try:
|
||||||
return timezone.make_aware(value, field_timezone)
|
return timezone.make_aware(value, field_timezone)
|
||||||
except InvalidTimeError:
|
except InvalidTimeError:
|
||||||
|
@ -1135,7 +1137,7 @@ class DateTimeField(Field):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def default_timezone(self):
|
def default_timezone(self):
|
||||||
return timezone.get_default_timezone() if settings.USE_TZ else None
|
return timezone.get_current_timezone() if settings.USE_TZ else None
|
||||||
|
|
||||||
def to_internal_value(self, value):
|
def to_internal_value(self, value):
|
||||||
input_formats = getattr(self, 'input_formats', api_settings.DATETIME_INPUT_FORMATS)
|
input_formats = getattr(self, 'input_formats', api_settings.DATETIME_INPUT_FORMATS)
|
||||||
|
@ -1174,6 +1176,7 @@ class DateTimeField(Field):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if output_format.lower() == ISO_8601:
|
if output_format.lower() == ISO_8601:
|
||||||
|
value = self.enforce_timezone(value)
|
||||||
value = value.isoformat()
|
value = value.isoformat()
|
||||||
if value.endswith('+00:00'):
|
if value.endswith('+00:00'):
|
||||||
value = value[:-6] + 'Z'
|
value = value[:-6] + 'Z'
|
||||||
|
|
|
@ -10,12 +10,17 @@ import pytest
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.timezone import utc
|
from django.utils.timezone import activate, deactivate, utc
|
||||||
|
|
||||||
import rest_framework
|
import rest_framework
|
||||||
from rest_framework import compat, serializers
|
from rest_framework import compat, serializers
|
||||||
from rest_framework.fields import is_simple_callable
|
from rest_framework.fields import is_simple_callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pytz
|
||||||
|
except ImportError:
|
||||||
|
pytz = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import typings
|
import typings
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -1168,7 +1173,7 @@ class TestDateTimeField(FieldValues):
|
||||||
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
|
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
|
||||||
}
|
}
|
||||||
outputs = {
|
outputs = {
|
||||||
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00Z',
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z',
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z',
|
||||||
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
|
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
|
||||||
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
|
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
|
||||||
|
@ -1230,10 +1235,59 @@ class TestNaiveDateTimeField(FieldValues):
|
||||||
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
|
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
|
||||||
}
|
}
|
||||||
invalid_inputs = {}
|
invalid_inputs = {}
|
||||||
outputs = {}
|
outputs = {
|
||||||
|
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
||||||
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00',
|
||||||
|
}
|
||||||
field = serializers.DateTimeField(default_timezone=None)
|
field = serializers.DateTimeField(default_timezone=None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
|
||||||
|
class TestTZWithDateTimeField(FieldValues):
|
||||||
|
"""
|
||||||
|
Valid and invalid values for `DateTimeField` when not using UTC as the timezone.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
# use class setup method, as class-level attribute will still be evaluated even if test is skipped
|
||||||
|
kolkata = pytz.timezone('Asia/Kolkata')
|
||||||
|
|
||||||
|
cls.valid_inputs = {
|
||||||
|
'2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
||||||
|
'2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
||||||
|
datetime.datetime(2016, 12, 19, 10): kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
||||||
|
}
|
||||||
|
cls.invalid_inputs = {}
|
||||||
|
cls.outputs = {
|
||||||
|
datetime.datetime(2016, 12, 19, 10): '2016-12-19T10:00:00+05:30',
|
||||||
|
datetime.datetime(2016, 12, 19, 4, 30, tzinfo=utc): '2016-12-19T10:00:00+05:30',
|
||||||
|
}
|
||||||
|
cls.field = serializers.DateTimeField(default_timezone=kolkata)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
|
||||||
|
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
|
||||||
|
class TestDefaultTZDateTimeField(TestCase):
|
||||||
|
"""
|
||||||
|
Test the current/default timezone handling in `DateTimeField`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
cls.field = serializers.DateTimeField()
|
||||||
|
cls.kolkata = pytz.timezone('Asia/Kolkata')
|
||||||
|
|
||||||
|
def test_default_timezone(self):
|
||||||
|
assert self.field.default_timezone() == utc
|
||||||
|
|
||||||
|
def test_current_timezone(self):
|
||||||
|
assert self.field.default_timezone() == utc
|
||||||
|
activate(self.kolkata)
|
||||||
|
assert self.field.default_timezone() == self.kolkata
|
||||||
|
deactivate()
|
||||||
|
assert self.field.default_timezone() == utc
|
||||||
|
|
||||||
|
|
||||||
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user