Skip to content

Commit 7d6d043

Browse files
author
Carlton Gibson
authored
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
1 parent 89daaf6 commit 7d6d043

File tree

4 files changed

+70
-5
lines changed

4 files changed

+70
-5
lines changed

docs/topics/release-notes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ You can determine your currently installed version using `pip freeze`:
4444

4545
* 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]
4646
* Deprecated `exclude_from_schema` on `APIView` and `api_view` decorator. Set `schema = None` or `@schema(None)` as appropriate. [#5422][gh5422]
47+
* Timezone-aware `DateTimeField`s now respect active or default) `timezone` during serialization, instead of always using UTC.
4748

49+
Resolves inconsistency whereby instances were serialised with supplied datetime for `create` but UTC for `retrieve`. [#3732][gh3732]
50+
51+
**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.
4852

4953
### 3.6.4
5054

@@ -1426,3 +1430,6 @@ For older release notes, [please see the version 2.x documentation][old-release-
14261430
<!-- 3.6.5 -->
14271431
[gh5376]: https://github.com/encode/django-rest-framework/issues/5376
14281432
[gh5422]: https://github.com/encode/django-rest-framework/issues/5422
1433+
[gh5408]: https://github.com/encode/django-rest-framework/issues/5408
1434+
[gh3732]: https://github.com/encode/django-rest-framework/issues/3732
1435+
[djangodocs-set-timezone]: https://docs.djangoproject.com/en/1.11/topics/i18n/timezones/#default-time-zone-and-current-time-zone

requirements/requirements-optionals.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Optional packages which may be used with REST framework.
2+
pytz==2017.2
23
markdown==2.6.4
34
django-guardian==1.4.8
45
django-filter==1.0.4

rest_framework/fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,7 +1125,9 @@ def enforce_timezone(self, value):
11251125
"""
11261126
field_timezone = getattr(self, 'timezone', self.default_timezone())
11271127

1128-
if (field_timezone is not None) and not timezone.is_aware(value):
1128+
if field_timezone is not None:
1129+
if timezone.is_aware(value):
1130+
return value.astimezone(field_timezone)
11291131
try:
11301132
return timezone.make_aware(value, field_timezone)
11311133
except InvalidTimeError:
@@ -1135,7 +1137,7 @@ def enforce_timezone(self, value):
11351137
return value
11361138

11371139
def default_timezone(self):
1138-
return timezone.get_default_timezone() if settings.USE_TZ else None
1140+
return timezone.get_current_timezone() if settings.USE_TZ else None
11391141

11401142
def to_internal_value(self, value):
11411143
input_formats = getattr(self, 'input_formats', api_settings.DATETIME_INPUT_FORMATS)
@@ -1174,6 +1176,7 @@ def to_representation(self, value):
11741176
return value
11751177

11761178
if output_format.lower() == ISO_8601:
1179+
value = self.enforce_timezone(value)
11771180
value = value.isoformat()
11781181
if value.endswith('+00:00'):
11791182
value = value[:-6] + 'Z'

tests/test_fields.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
from django.http import QueryDict
1111
from django.test import TestCase, override_settings
1212
from django.utils import six
13-
from django.utils.timezone import utc
13+
from django.utils.timezone import activate, deactivate, utc
1414

1515
import rest_framework
1616
from rest_framework import compat, serializers
1717
from rest_framework.fields import is_simple_callable
1818

19+
try:
20+
import pytz
21+
except ImportError:
22+
pytz = None
23+
1924
try:
2025
import typings
2126
except ImportError:
@@ -1168,7 +1173,7 @@ class TestDateTimeField(FieldValues):
11681173
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
11691174
}
11701175
outputs = {
1171-
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
1176+
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00Z',
11721177
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z',
11731178
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
11741179
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
@@ -1230,10 +1235,59 @@ class TestNaiveDateTimeField(FieldValues):
12301235
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
12311236
}
12321237
invalid_inputs = {}
1233-
outputs = {}
1238+
outputs = {
1239+
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
1240+
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00',
1241+
}
12341242
field = serializers.DateTimeField(default_timezone=None)
12351243

12361244

1245+
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
1246+
class TestTZWithDateTimeField(FieldValues):
1247+
"""
1248+
Valid and invalid values for `DateTimeField` when not using UTC as the timezone.
1249+
"""
1250+
@classmethod
1251+
def setup_class(cls):
1252+
# use class setup method, as class-level attribute will still be evaluated even if test is skipped
1253+
kolkata = pytz.timezone('Asia/Kolkata')
1254+
1255+
cls.valid_inputs = {
1256+
'2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1257+
'2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1258+
datetime.datetime(2016, 12, 19, 10): kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1259+
}
1260+
cls.invalid_inputs = {}
1261+
cls.outputs = {
1262+
datetime.datetime(2016, 12, 19, 10): '2016-12-19T10:00:00+05:30',
1263+
datetime.datetime(2016, 12, 19, 4, 30, tzinfo=utc): '2016-12-19T10:00:00+05:30',
1264+
}
1265+
cls.field = serializers.DateTimeField(default_timezone=kolkata)
1266+
1267+
1268+
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
1269+
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
1270+
class TestDefaultTZDateTimeField(TestCase):
1271+
"""
1272+
Test the current/default timezone handling in `DateTimeField`.
1273+
"""
1274+
1275+
@classmethod
1276+
def setup_class(cls):
1277+
cls.field = serializers.DateTimeField()
1278+
cls.kolkata = pytz.timezone('Asia/Kolkata')
1279+
1280+
def test_default_timezone(self):
1281+
assert self.field.default_timezone() == utc
1282+
1283+
def test_current_timezone(self):
1284+
assert self.field.default_timezone() == utc
1285+
activate(self.kolkata)
1286+
assert self.field.default_timezone() == self.kolkata
1287+
deactivate()
1288+
assert self.field.default_timezone() == utc
1289+
1290+
12371291
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
12381292
"""
12391293
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.

0 commit comments

Comments
 (0)