Skip to content

Fix DateTimeField TZ handling #5435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/topics/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like an erroneous ")" here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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

Expand Down Expand Up @@ -1426,3 +1430,6 @@ For older release notes, [please see the version 2.x documentation][old-release-
<!-- 3.6.5 -->
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376
[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 change: 1 addition & 0 deletions requirements/requirements-optionals.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Optional packages which may be used with REST framework.
pytz==2017.2
markdown==2.6.4
django-guardian==1.4.8
django-filter==1.0.4
Expand Down
7 changes: 5 additions & 2 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,9 @@ def enforce_timezone(self, value):
"""
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:
return timezone.make_aware(value, field_timezone)
except InvalidTimeError:
Expand All @@ -1135,7 +1137,7 @@ def enforce_timezone(self, value):
return value

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):
input_formats = getattr(self, 'input_formats', api_settings.DATETIME_INPUT_FORMATS)
Expand Down Expand Up @@ -1174,6 +1176,7 @@ def to_representation(self, value):
return value

if output_format.lower() == ISO_8601:
value = self.enforce_timezone(value)
value = value.isoformat()
if value.endswith('+00:00'):
value = value[:-6] + 'Z'
Expand Down
60 changes: 57 additions & 3 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
from django.http import QueryDict
from django.test import TestCase, override_settings
from django.utils import six
from django.utils.timezone import utc
from django.utils.timezone import activate, deactivate, utc

import rest_framework
from rest_framework import compat, serializers
from rest_framework.fields import is_simple_callable

try:
import pytz
except ImportError:
pytz = None

try:
import typings
except ImportError:
Expand Down Expand Up @@ -1168,7 +1173,7 @@ class TestDateTimeField(FieldValues):
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
}
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',
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
Expand Down Expand Up @@ -1230,10 +1235,59 @@ class TestNaiveDateTimeField(FieldValues):
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
}
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)


@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):
"""
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
Expand Down