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:
Carlton Gibson 2017-09-20 12:15:15 +02:00 committed by GitHub
parent 89daaf6276
commit 7d6d043531
4 changed files with 70 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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.