Skip to content

Fix timezone handling for DateTimeField #5408

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

Closed
wants to merge 3 commits into from
Closed
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
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 @@ -1165,7 +1170,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',
Copy link
Member Author

Choose a reason for hiding this comment

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

Side note - changed the datetime output from unaware to aware/UTC datetime, since the field itself is datetime-aware.

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 @@ -1227,10 +1232,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