diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b527b016b..27a9c2ef3 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -299,7 +299,7 @@ Corresponds to `django.db.models.fields.DateTimeField`. #### `DateTimeField` format strings. -Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`) +Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special strings `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used (eg `'2013-01-29T12:34:56.000000Z'`), and `'iso-8601-strict'`, which indicates that [ISO 8601][iso8601] style datetimes without fraction seconds should be used (eg `'2013-01-29T12:34:56Z'`) When a value of `None` is used for the format `datetime` objects will be returned by `to_representation` and the final output representation will determined by the renderer class. @@ -345,7 +345,7 @@ Corresponds to `django.db.models.fields.TimeField` #### `TimeField` format strings -Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`) +Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special strings `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used (eg `'12:34:56.000000'`), and `'iso-8601-strict'`, which indicates that [ISO 8601][iso8601] style times without fractional seconds should be used (eg `'12:34:56'`). ## DurationField diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index aaedd463e..9105a691c 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -286,7 +286,7 @@ Default: `'format'` A format string that should be used by default for rendering the output of `DateTimeField` serializer fields. If `None`, then `DateTimeField` serializer fields will return Python `datetime` objects, and the datetime encoding will be determined by the renderer. -May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string. +May be any of `None`, `'iso-8601'`, `'iso-8601-strict'`, or a Python [strftime format][strftime] string. Default: `'iso-8601'` @@ -294,7 +294,7 @@ Default: `'iso-8601'` A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields. -May be a list including the string `'iso-8601'` or Python [strftime format][strftime] strings. +May be a list including the string `'iso-8601'`, `'iso-8601-strict'` or Python [strftime format][strftime] strings. Default: `['iso-8601']` @@ -318,7 +318,7 @@ Default: `['iso-8601']` A format string that should be used by default for rendering the output of `TimeField` serializer fields. If `None`, then `TimeField` serializer fields will return Python `time` objects, and the time encoding will be determined by the renderer. -May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string. +May be any of `None`, `'iso-8601'`, `'iso-8601-strict'` or a Python [strftime format][strftime] string. Default: `'iso-8601'` @@ -326,7 +326,7 @@ Default: `'iso-8601'` A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields. -May be a list including the string `'iso-8601'` or Python [strftime format][strftime] strings. +May be a list including the string `'iso-8601'`, `'iso-8601-strict'` or Python [strftime format][strftime] strings. Default: `['iso-8601']` 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 af6247c5e..f1393d258 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 069ba879d..3ec5fa2ab 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.