From 20fd738c856dfa21a0d2d251b92459d6641c782c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Mar 2013 13:05:59 +0000 Subject: [PATCH 1/2] iso formated datetime aware fields with +0000 offset should use 'Z' suffix instead --- rest_framework/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4b6931ad4..a0f52f506 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -609,7 +609,10 @@ class DateTimeField(WritableField): return None if self.format.lower() == ISO_8601: - return value.isoformat() + ret = value.isoformat() + if ret.endswith('+00:00'): + ret = ret[:-6] + 'Z' + return ret return value.strftime(self.format) From 8adde506e865005a96cdeff996ec4b5b9bb73a8f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Mar 2013 08:41:54 +0000 Subject: [PATCH 2/2] Default date/time fields now return python date/time objects again by default --- docs/api-guide/fields.md | 6 ++--- docs/api-guide/settings.md | 27 ++++++++++++++----- rest_framework/fields.py | 18 ++++++------- rest_framework/tests/fields.py | 43 ++++++++++++++++++++++++++++-- rest_framework/tests/filterset.py | 9 +++---- rest_framework/tests/pagination.py | 2 +- rest_framework/tests/serializer.py | 2 +- 7 files changed, 79 insertions(+), 28 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 9a745cf19..63bd5b1aa 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -199,7 +199,7 @@ If you want to override this behavior, you'll need to declare the `DateTimeField **Signature:** `DateTimeField(format=None, input_formats=None)` -* `format` - A string representing the output format. If not specified, the `DATETIME_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy 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.000000'`) @@ -212,7 +212,7 @@ Corresponds to `django.db.models.fields.DateField` **Signature:** `DateField(format=None, input_formats=None)` -* `format` - A string representing the output format. If not specified, the `DATE_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. Date format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`) @@ -227,7 +227,7 @@ Corresponds to `django.db.models.fields.TimeField` **Signature:** `TimeField(format=None, input_formats=None)` -* `format` - A string representing the output format. If not specified, the `TIME_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. Time format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 116386969..c0d8d9eea 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -192,44 +192,56 @@ Default: `'format'` --- -## Date/Time formatting +## Date and time formatting *The following settings are used to control how date and time representations may be parsed and rendered.* #### DATETIME_FORMAT -A format string that should be used by default for rendering the output of `DateTimeField` serializer fields. +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. -Default: `'iso-8601'` +May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string. + +Default: `None'` #### DATETIME_INPUT_FORMATS 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. + Default: `['iso-8601']` #### DATE_FORMAT -A format string that should be used by default for rendering the output of `DateField` serializer fields. +A format string that should be used by default for rendering the output of `DateField` serializer fields. If `None`, then `DateField` serializer fields will return python `date` objects, and the date encoding will be determined by the renderer. -Default: `'iso-8601'` +May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string. + +Default: `None` #### DATE_INPUT_FORMATS A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields. +May be a list including the string `'iso-8601'` or python [strftime format][strftime] strings. + Default: `['iso-8601']` #### TIME_FORMAT -A format string that should be used by default for rendering the output of `TimeField` serializer fields. +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. -Default: `'iso-8601'` +May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string. + +Default: `None` #### TIME_INPUT_FORMATS 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. + Default: `['iso-8601']` --- @@ -243,3 +255,4 @@ The name of a parameter in the URL conf that may be used to provide a format suf Default: `'format'` [cite]: http://www.python.org/dev/peps/pep-0020/ +[strftime]: http://docs.python.org/2/library/time.html#time.strftime \ No newline at end of file diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a0f52f506..f3496b53e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -494,7 +494,7 @@ class DateField(WritableField): } empty = None input_formats = api_settings.DATE_INPUT_FORMATS - format = api_settings.DATE_FORMAT + format = None def __init__(self, input_formats=None, format=None, *args, **kwargs): self.input_formats = input_formats if input_formats is not None else self.input_formats @@ -536,8 +536,8 @@ class DateField(WritableField): raise ValidationError(msg) def to_native(self, value): - if value is None: - return None + if value is None or self.format is None: + return value if isinstance(value, datetime.datetime): value = value.date() @@ -557,7 +557,7 @@ class DateTimeField(WritableField): } empty = None input_formats = api_settings.DATETIME_INPUT_FORMATS - format = api_settings.DATETIME_FORMAT + format = None def __init__(self, input_formats=None, format=None, *args, **kwargs): self.input_formats = input_formats if input_formats is not None else self.input_formats @@ -605,8 +605,8 @@ class DateTimeField(WritableField): raise ValidationError(msg) def to_native(self, value): - if value is None: - return None + if value is None or self.format is None: + return value if self.format.lower() == ISO_8601: ret = value.isoformat() @@ -626,7 +626,7 @@ class TimeField(WritableField): } empty = None input_formats = api_settings.TIME_INPUT_FORMATS - format = api_settings.TIME_FORMAT + format = None def __init__(self, input_formats=None, format=None, *args, **kwargs): self.input_formats = input_formats if input_formats is not None else self.input_formats @@ -661,8 +661,8 @@ class TimeField(WritableField): raise ValidationError(msg) def to_native(self, value): - if value is None: - return None + if value is None or self.format is None: + return value if isinstance(value, datetime.datetime): value = value.time() diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index fd6de7797..19c663d82 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -153,12 +153,22 @@ class DateFieldTest(TestCase): def test_to_native(self): """ - Make sure to_native() returns isoformat as default. + Make sure to_native() returns datetime as default. """ f = serializers.DateField() result_1 = f.to_native(datetime.date(1984, 7, 31)) + self.assertEqual(datetime.date(1984, 7, 31), result_1) + + def test_to_native_iso(self): + """ + Make sure to_native() with 'iso-8601' returns iso formated date. + """ + f = serializers.DateField(format='iso-8601') + + result_1 = f.to_native(datetime.date(1984, 7, 31)) + self.assertEqual('1984-07-31', result_1) def test_to_native_custom_format(self): @@ -289,6 +299,22 @@ class DateTimeFieldTest(TestCase): result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + self.assertEqual(datetime.datetime(1984, 7, 31), result_1) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4) + + def test_to_native_iso(self): + """ + Make sure to_native() with format=iso-8601 returns iso formatted datetime. + """ + f = serializers.DateTimeField(format='iso-8601') + + result_1 = f.to_native(datetime.datetime(1984, 7, 31)) + result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31)) + result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) + result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + self.assertEqual('1984-07-31T00:00:00', result_1) self.assertEqual('1984-07-31T04:31:00', result_2) self.assertEqual('1984-07-31T04:31:59', result_3) @@ -419,13 +445,26 @@ class TimeFieldTest(TestCase): def test_to_native(self): """ - Make sure to_native() returns isoformat as default. + Make sure to_native() returns time object as default. """ f = serializers.TimeField() result_1 = f.to_native(datetime.time(4, 31)) result_2 = f.to_native(datetime.time(4, 31, 59)) result_3 = f.to_native(datetime.time(4, 31, 59, 200)) + self.assertEqual(datetime.time(4, 31), result_1) + self.assertEqual(datetime.time(4, 31, 59), result_2) + self.assertEqual(datetime.time(4, 31, 59, 200), result_3) + + def test_to_native_iso(self): + """ + Make sure to_native() with format='iso-8601' returns iso formatted time. + """ + f = serializers.TimeField(format='iso-8601') + result_1 = f.to_native(datetime.time(4, 31)) + result_2 = f.to_native(datetime.time(4, 31, 59)) + result_3 = f.to_native(datetime.time(4, 31, 59, 200)) + self.assertEqual('04:31:00', result_1) self.assertEqual('04:31:59', result_2) self.assertEqual('04:31:59.000200', result_3) diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index fe92e0bcf..238da56e5 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -65,7 +65,7 @@ class IntegrationTestFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} for obj in self.objects.all() ] @@ -95,7 +95,7 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() == search_date] + expected_data = [f for f in self.data if f['date'] == search_date] self.assertEqual(response.data, expected_data) @unittest.skipUnless(django_filters, 'django-filters not installed') @@ -125,7 +125,7 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() > search_date] + expected_data = [f for f in self.data if f['date'] > search_date] self.assertEqual(response.data, expected_data) # Tests that the text filter set with 'icontains' in the filter class works. @@ -142,8 +142,7 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if - datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() > search_date and + expected_data = [f for f in self.data if f['date'] > search_date and f['decimal'] < search_decimal] self.assertEqual(response.data, expected_data) diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 1a2d68a68..d2c9b0513 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -102,7 +102,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} for obj in self.objects.all() ] diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index beb372c2b..d0799b85d 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -112,7 +112,7 @@ class BasicTests(TestCase): self.expected = { 'email': 'tom@example.com', 'content': 'Happy new year!', - 'created': '2012-01-01T00:00:00', + 'created': datetime.datetime(2012, 1, 1), 'sub_comment': 'And Merry Christmas!' } self.person_data = {'name': 'dwight', 'age': 35}