From 87440e73a1adb91a0ae545f2b864c856c81e3c6f Mon Sep 17 00:00:00 2001 From: Dale Hui Date: Mon, 5 Dec 2016 08:28:51 -0800 Subject: [PATCH] Support parsing strict ISO 8601 input format for DateTimeField and TimeField causing microseconds to be ignored --- rest_framework/__init__.py | 1 + rest_framework/fields.py | 18 +++++- rest_framework/utils/humanize_datetime.py | 8 ++- tests/test_fields.py | 74 +++++++++++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 795b84e95..e1d8d4916 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -21,3 +21,4 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' # Default datetime input and output formats ISO_8601 = 'iso-8601' +ISO_8601_STRICT = 'iso-8601-strict' diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 13b5145ba..87cc53518 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -30,7 +30,7 @@ from django.utils.functional import cached_property from django.utils.ipv6 import clean_ipv6_address from django.utils.translation import ugettext_lazy as _ -from rest_framework import ISO_8601 +from rest_framework import ISO_8601, ISO_8601_STRICT from rest_framework.compat import ( get_remote_field, unicode_repr, unicode_to_repr, value_from_object ) @@ -1120,13 +1120,15 @@ class DateTimeField(Field): return self.enforce_timezone(value) for input_format in input_formats: - if input_format.lower() == ISO_8601: + if input_format.lower() in (ISO_8601, ISO_8601_STRICT): try: parsed = parse_datetime(value) except (ValueError, TypeError): pass else: if parsed is not None: + if input_format == ISO_8601_STRICT: + parsed = parsed - datetime.timedelta(microseconds=parsed.microsecond) return self.enforce_timezone(parsed) else: try: @@ -1153,6 +1155,10 @@ class DateTimeField(Field): if value.endswith('+00:00'): value = value[:-6] + 'Z' return value + if output_format.lower() == ISO_8601_STRICT: + value = value - datetime.timedelta(microseconds=value.microsecond) + return value.isoformat() + return value.strftime(output_format) @@ -1243,13 +1249,15 @@ class TimeField(Field): return value for input_format in input_formats: - if input_format.lower() == ISO_8601: + if input_format.lower() in (ISO_8601, ISO_8601_STRICT): try: parsed = parse_time(value) except (ValueError, TypeError): pass else: if parsed is not None: + if input_format.lower() == ISO_8601_STRICT: + return parsed.replace(microsecond=0) return parsed else: try: @@ -1282,6 +1290,10 @@ class TimeField(Field): if output_format.lower() == ISO_8601: return value.isoformat() + if output_format.lower() == ISO_8601_STRICT: + if value.microsecond: + value = value.replace(microsecond=0) + return value.isoformat() return value.strftime(output_format) diff --git a/rest_framework/utils/humanize_datetime.py b/rest_framework/utils/humanize_datetime.py index 649f2abc6..1f0f7bc33 100644 --- a/rest_framework/utils/humanize_datetime.py +++ b/rest_framework/utils/humanize_datetime.py @@ -1,11 +1,14 @@ """ Helper functions that convert strftime formats into more readable representations. """ -from rest_framework import ISO_8601 +from rest_framework import ISO_8601, ISO_8601_STRICT def datetime_formats(formats): format = ', '.join(formats).replace( + ISO_8601_STRICT, + 'YYYY-MM-DDThh:mm[:ss][+HH:MM|-HH:MM|Z]' + ).replace( ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]' ) @@ -18,7 +21,8 @@ def date_formats(formats): def time_formats(formats): - format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') + format = ', '.join(formats).replace(ISO_8601_STRICT, 'hh:mm[:ss]') \ + .replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') return humanize_strptime(format) diff --git a/tests/test_fields.py b/tests/test_fields.py index 92030e3ca..994857d1b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1130,10 +1130,15 @@ class TestDateTimeField(FieldValues): """ valid_inputs = { '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01 13:00:00.123456': datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00:00.123456': datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00:00.123456Z': datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456): datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), # Django 1.4 does not support timezone string parsing. '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()) } @@ -1144,7 +1149,9 @@ class TestDateTimeField(FieldValues): } outputs = { datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00', + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456): '2001-01-01T13:00:00.123456', datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z', + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()): '2001-01-01T13:00:00.123456Z', '2001-01-01T00:00:00': '2001-01-01T00:00:00', six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00', None: None, @@ -1153,6 +1160,41 @@ class TestDateTimeField(FieldValues): field = serializers.DateTimeField(default_timezone=timezone.UTC()) +class TestIso8601StrictDateTimeField(FieldValues): + valid_inputs = { + '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01 13:00:00.123456': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00:00.123456': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + '2001-01-01T13:00:00.123456Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456): datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()), + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()), + # Django 1.4 does not support timezone string parsing. + '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()) + } + invalid_inputs = { + 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss][+HH:MM|-HH:MM|Z].'], + '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss][+HH:MM|-HH:MM|Z].'], + 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, 00, 123456): '2001-01-01T13:00:00', + datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00+00:00', + datetime.datetime(2001, 1, 1, 13, 00, 00, 123456, tzinfo=timezone.UTC()): '2001-01-01T13:00:00+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', + None: None, + '': None, + } + + field = serializers.DateTimeField(format=rest_framework.ISO_8601_STRICT, input_formats=[rest_framework.ISO_8601_STRICT], + default_timezone=timezone.UTC()) + + class TestCustomInputFormatDateTimeField(FieldValues): """ Valid and invalid values for `DateTimeField` with a custom input format. @@ -1210,7 +1252,9 @@ class TestTimeField(FieldValues): """ valid_inputs = { '13:00': datetime.time(13, 00), + '13:00:00.123456': datetime.time(13, 00, 00, 123456), datetime.time(13, 00): datetime.time(13, 00), + datetime.time(13, 00, 00, 123456): datetime.time(13, 00, 00, 123456), } invalid_inputs = { 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'], @@ -1218,14 +1262,44 @@ class TestTimeField(FieldValues): } outputs = { datetime.time(13, 0): '13:00:00', + datetime.time(13, 0, 0, 123456): '13:00:00.123456', datetime.time(0, 0): '00:00:00', + datetime.time(0, 0, 0, 0): '00:00:00', '00:00:00': '00:00:00', + '00:00:00.000000': '00:00:00.000000', None: None, '': None, } field = serializers.TimeField() +class TestIso8601StrictTimeField(FieldValues): + """ + Valid and invalid values for `TimeField`. + """ + valid_inputs = { + '13:00': datetime.time(13, 00), + '13:00:00.123456': datetime.time(13, 00), + datetime.time(13, 00): datetime.time(13, 00), + datetime.time(13, 00, 00, 123456): datetime.time(13, 00, 00, 123456), + } + invalid_inputs = { + 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss].'], + '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss].'], + } + outputs = { + datetime.time(13, 0): '13:00:00', + datetime.time(13, 0, 0, 123456): '13:00:00', + datetime.time(0, 0): '00:00:00', + datetime.time(0, 0, 0, 0): '00:00:00', + '00:00:00': '00:00:00', + '00:00:00.000000': '00:00:00.000000', + None: None, + '': None, + } + field = serializers.TimeField(format=rest_framework.ISO_8601_STRICT, input_formats=[rest_framework.ISO_8601_STRICT]) + + class TestCustomInputFormatTimeField(FieldValues): """ Valid and invalid values for `TimeField` with a custom input format.