From 1ee54fb85ccc71ab689515dc33f9e99a4d872472 Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 15 Mar 2017 23:45:41 -0400 Subject: [PATCH 1/4] Added test for DateTimeField validation when server has timezone with DST and input is a native time in a DST shift interval. Added pytz to requirements-testing.txt to reproduce the case. --- requirements/requirements-testing.txt | 1 + tests/test_fields.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index b9e168442..ea2022512 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -2,3 +2,4 @@ pytest==3.0.5 pytest-django==3.1.2 pytest-cov==2.4.0 +pytz==2016.10 diff --git a/tests/test_fields.py b/tests/test_fields.py index 16221d4cc..971e7f0e1 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,7 +9,7 @@ import pytest 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 utc, pytz import rest_framework from rest_framework import serializers @@ -1205,6 +1205,23 @@ class TestNaiveDateTimeField(FieldValues): field = serializers.DateTimeField(default_timezone=None) +class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): + """ + Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST. + Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and + from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017. + """ + valid_inputs = {} + invalid_inputs = { + '2017-03-12T02:30:00': [ + 'Datetime can not be converted to server timezone due to NonExistentTimeError.'], + '2017-11-05T01:30:00': [ + 'Datetime can not be converted to server timezone due to AmbiguousTimeError.'] + } + outputs = {} + field = serializers.DateTimeField(default_timezone=pytz.timezone('America/New_York')) + + class TestTimeField(FieldValues): """ Valid and invalid values for `TimeField`. From d4726dab81412af779cb7fcdc7c29fc07edc4bea Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Sat, 18 Mar 2017 22:43:08 -0400 Subject: [PATCH 2/4] Fix bug for not existent or ambiguous datetime during native to aware conversion in timezone with DST. Ref: #4986 --- rest_framework/fields.py | 6 +++++- tests/test_fields.py | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5a881c772..fa6ff0bf8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1085,6 +1085,7 @@ class DateTimeField(Field): default_error_messages = { 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), 'date': _('Expected a datetime but got a date.'), + 'make_aware': _('Datetime can not be represented in timezone "{timezone}".') } datetime_parser = datetime.datetime.strptime @@ -1105,7 +1106,10 @@ class DateTimeField(Field): field_timezone = getattr(self, 'timezone', self.default_timezone()) if (field_timezone is not None) and not timezone.is_aware(value): - return timezone.make_aware(value, field_timezone) + try: + return timezone.make_aware(value, field_timezone) + except Exception: + self.fail('make_aware', timezone=field_timezone) elif (field_timezone is None) and timezone.is_aware(value): return timezone.make_naive(value, utc) return value diff --git a/tests/test_fields.py b/tests/test_fields.py index 971e7f0e1..da4a05091 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,7 +9,7 @@ import pytest from django.http import QueryDict from django.test import TestCase, override_settings from django.utils import six -from django.utils.timezone import utc, pytz +from django.utils.timezone import pytz, utc import rest_framework from rest_framework import serializers @@ -1213,10 +1213,8 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): """ valid_inputs = {} invalid_inputs = { - '2017-03-12T02:30:00': [ - 'Datetime can not be converted to server timezone due to NonExistentTimeError.'], - '2017-11-05T01:30:00': [ - 'Datetime can not be converted to server timezone due to AmbiguousTimeError.'] + '2017-03-12T02:30:00': ['Datetime can not be represented in timezone "America/New_York".'], + '2017-11-05T01:30:00': ['Datetime can not be represented in timezone "America/New_York".'] } outputs = {} field = serializers.DateTimeField(default_timezone=pytz.timezone('America/New_York')) From e4a1bd140b3c96dd6724b413a337f639b5a949ef Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Mon, 20 Mar 2017 18:47:25 -0400 Subject: [PATCH 3/4] Update error message. Ref: #4986 --- rest_framework/fields.py | 2 +- tests/test_fields.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fa6ff0bf8..6dd40acd9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1085,7 +1085,7 @@ class DateTimeField(Field): default_error_messages = { 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), 'date': _('Expected a datetime but got a date.'), - 'make_aware': _('Datetime can not be represented in timezone "{timezone}".') + 'make_aware': _('Invalid datetime for the timezone "{timezone}".') } datetime_parser = datetime.datetime.strptime diff --git a/tests/test_fields.py b/tests/test_fields.py index da4a05091..457e368cc 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1213,8 +1213,8 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): """ valid_inputs = {} invalid_inputs = { - '2017-03-12T02:30:00': ['Datetime can not be represented in timezone "America/New_York".'], - '2017-11-05T01:30:00': ['Datetime can not be represented in timezone "America/New_York".'] + '2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'], + '2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".'] } outputs = {} field = serializers.DateTimeField(default_timezone=pytz.timezone('America/New_York')) From b0a0c30bfe4643e6824a3ad4d428a9742997aabe Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 22 Mar 2017 00:01:07 -0400 Subject: [PATCH 4/4] Added pytz exception in compat module. Mock pytz.timezone localize in tests. Ref: #4986 --- requirements/requirements-testing.txt | 1 - rest_framework/compat.py | 9 +++++++++ rest_framework/fields.py | 5 +++-- tests/test_fields.py | 15 ++++++++++++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index ea2022512..b9e168442 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -2,4 +2,3 @@ pytest==3.0.5 pytest-django==3.1.2 pytest-cov==2.4.0 -pytz==2016.10 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 45ac49841..168bccf83 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -275,6 +275,14 @@ except ImportError: def pygments_css(style): return None + +try: + import pytz + from pytz.exceptions import InvalidTimeError +except ImportError: + InvalidTimeError = Exception + + # `separators` argument to `json.dumps()` differs between 2.x and 3.x # See: http://bugs.python.org/issue22767 if six.PY3: @@ -339,6 +347,7 @@ def set_many(instance, field, value): field = getattr(instance, field) field.set(value) + def include(module, namespace=None, app_name=None): from django.conf.urls import include if django.VERSION < (1,9): diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6dd40acd9..7ee3f1016 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -33,7 +33,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 from rest_framework.compat import ( - get_remote_field, unicode_repr, unicode_to_repr, value_from_object + InvalidTimeError, get_remote_field, unicode_repr, unicode_to_repr, + value_from_object ) from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings @@ -1108,7 +1109,7 @@ class DateTimeField(Field): if (field_timezone is not None) and not timezone.is_aware(value): try: return timezone.make_aware(value, field_timezone) - except Exception: + except InvalidTimeError: self.fail('make_aware', timezone=field_timezone) elif (field_timezone is None) and timezone.is_aware(value): return timezone.make_naive(value, utc) diff --git a/tests/test_fields.py b/tests/test_fields.py index 457e368cc..968c41d3f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,10 +9,10 @@ import pytest from django.http import QueryDict from django.test import TestCase, override_settings from django.utils import six -from django.utils.timezone import pytz, utc +from django.utils.timezone import utc import rest_framework -from rest_framework import serializers +from rest_framework import compat, serializers from rest_framework.fields import is_simple_callable try: @@ -1217,7 +1217,16 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): '2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".'] } outputs = {} - field = serializers.DateTimeField(default_timezone=pytz.timezone('America/New_York')) + + class MockTimezone: + @staticmethod + def localize(value, is_dst): + raise compat.InvalidTimeError() + + def __str__(self): + return 'America/New_York' + + field = serializers.DateTimeField(default_timezone=MockTimezone()) class TestTimeField(FieldValues):