From bfff356dd36f7d14d35d8a854cd314e60cf25efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:09:54 +0100 Subject: [PATCH 01/20] Add better date / datetime validation (pull 2) addition to #631 with update to master + timefield support --- docs/api-guide/fields.md | 12 ++ docs/topics/release-notes.md | 3 + rest_framework/fields.py | 87 +++++---- rest_framework/tests/fields.py | 345 +++++++++++++++++++++++++++++++-- rest_framework/utils/dates.py | 14 ++ 5 files changed, 404 insertions(+), 57 deletions(-) create mode 100644 rest_framework/utils/dates.py diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 8c28273b0..a3dc4fe29 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -185,12 +185,20 @@ Corresponds to `django.forms.fields.RegexField` A date representation. +Uses `DATE_INPUT_FORMATS` to validate date. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.DateField` ## DateTimeField A date and time representation. +Uses `DATETIME_INPUT_FORMATS` to validate date_time. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.DateTimeField` When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default. @@ -207,6 +215,10 @@ If you want to override this behavior, you'll need to declare the `DateTimeField A time representation. +Uses `TIME_INPUT_FORMATS` to validate time. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.TimeField` ## IntegerField diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 06dc79a61..31ff68dda 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,9 @@ You can determine your currently installed version using `pip freeze`: * Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfixes for model field validation edge-cases. +* Support `DATE_INPUT_FORMATS` for `DateField` validation +* Support `DATETIME_INPUT_FORMATS` for `DateTimeField` validation +* Support `TIME_INPUT_FORMATS` for `TimeField` validation ### 2.2.1 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 86c3a837a..2260c430b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -19,6 +19,7 @@ from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import parse_time +from rest_framework.utils.dates import get_readable_date_format def is_simple_callable(obj): @@ -447,13 +448,14 @@ class DateField(WritableField): form_field_class = forms.DateField default_error_messages = { - 'invalid': _("'%s' value has an invalid date format. It must be " - "in YYYY-MM-DD format."), - 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) " - "but it is an invalid date."), + 'invalid': _(u"Date has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.DATE_INPUT_FORMATS) + super(DateField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -468,15 +470,16 @@ class DateField(WritableField): if isinstance(value, datetime.date): return value - try: - parsed = parse_date(value) - if parsed is not None: - return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid_date'] % value - raise ValidationError(msg) + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.date() - msg = self.error_messages['invalid'] % value + date_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) @@ -486,16 +489,14 @@ class DateTimeField(WritableField): form_field_class = forms.DateTimeField default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be in " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."), - 'invalid_date': _("'%s' value has the correct format " - "(YYYY-MM-DD) but it is an invalid date."), - 'invalid_datetime': _("'%s' value has the correct format " - "(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " - "but it is an invalid date/time."), + 'invalid': _(u"Datetime has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.DATETIME_INPUT_FORMATS) + super(DateTimeField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -516,23 +517,16 @@ class DateTimeField(WritableField): value = timezone.make_aware(value, default_timezone) return value - try: - parsed = parse_datetime(value) - if parsed is not None: + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid_datetime'] % value - raise ValidationError(msg) - try: - parsed = parse_date(value) - if parsed is not None: - return datetime.datetime(parsed.year, parsed.month, parsed.day) - except (ValueError, TypeError): - msg = self.error_messages['invalid_date'] % value - raise ValidationError(msg) - - msg = self.error_messages['invalid'] % value + datetime_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) @@ -542,11 +536,14 @@ class TimeField(WritableField): form_field_class = forms.TimeField default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be a valid " - "time in the HH:MM[:ss[.uuuuuu]] format."), + 'invalid': _(u"Time has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.TIME_INPUT_FORMATS) + super(TimeField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -554,13 +551,17 @@ class TimeField(WritableField): if isinstance(value, datetime.time): return value - try: - parsed = parse_time(value) - assert parsed is not None - return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid'] % value - raise ValidationError(msg) + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.time() + + time_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) + raise ValidationError(msg) class IntegerField(WritableField): diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 34f616781..d21e247d5 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -3,9 +3,13 @@ General serializer field tests. """ from __future__ import unicode_literals import datetime + +import django from django.db import models from django.test import TestCase from django.core import validators +from django.utils import unittest + from rest_framework import serializers @@ -18,6 +22,21 @@ class CharPrimaryKeyModel(models.Model): id = models.CharField(max_length=20, primary_key=True) +class DateObject(object): + def __init__(self, date): + self.date = date + + +class DateTimeObject(object): + def __init__(self, date_time): + self.date_time = date_time + + +class TimeObject(object): + def __init__(self, time): + self.time = time + + class TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel @@ -28,6 +47,66 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): model = CharPrimaryKeyModel +class DateObjectSerializer(serializers.Serializer): + date = serializers.DateField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date = attrs['date'] + return instance + return DateObject(**attrs) + + +class DateObjectCustomFormatSerializer(serializers.Serializer): + date = serializers.DateField(format=("%Y", "%Y -- %m")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date = attrs['date'] + return instance + return DateObject(**attrs) + + +class DateTimeObjectSerializer(serializers.Serializer): + date_time = serializers.DateTimeField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date_time = attrs['date_time'] + return instance + return DateTimeObject(**attrs) + + +class DateTimeObjectCustomFormatSerializer(serializers.Serializer): + date_time = serializers.DateTimeField(format=("%Y", "%Y %H:%M")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date_time = attrs['date_time'] + return instance + return DateTimeObject(**attrs) + + +class TimeObjectSerializer(serializers.Serializer): + time = serializers.TimeField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.time = attrs['time'] + return instance + return TimeObject(**attrs) + + +class TimeObjectCustomFormatSerializer(serializers.Serializer): + time = serializers.TimeField(format=("%H -- %M", "%H%M%S")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.time = attrs['time'] + return instance + return TimeObject(**attrs) + + class TimeFieldModel(models.Model): clock = models.TimeField() @@ -59,37 +138,275 @@ class BasicFieldTests(TestCase): serializer = CharPrimaryKeyModelSerializer() self.assertEquals(serializer.fields['id'].read_only, False) - def test_TimeField_from_native(self): + +class DateFieldTest(TestCase): + def test_valid_default_date_input_formats(self): + serializer = DateObjectSerializer(data={'date': '1984-07-31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '07/31/1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '07/31/84'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'Jul 31 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'Jul 31, 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'July 31 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'July 31, 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 July 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 July, 1984'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_date_input_formats(self): + serializer = DateObjectCustomFormatSerializer(data={'date': '1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectCustomFormatSerializer(data={'date': '1984 -- 07'}) + self.assertTrue(serializer.is_valid()) + + def test_wrong_default_date_input_format(self): + serializer = DateObjectSerializer(data={'date': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + u'[January through December] DD, YYYY; DD [January through December] YYYY; ' + u'DD [January through December], YYYY']}) + + def test_wrong_custom_date_input_format(self): + serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) + + def test_from_native(self): + f = serializers.DateField() + result = f.from_native('1984-07-31') + + self.assertEqual(datetime.date(1984, 7, 31), result) + + def test_from_native_datetime_date(self): + """ + Make sure from_native() accepts a datetime.date instance. + """ + f = serializers.DateField() + result = f.from_native(datetime.date(1984, 7, 31)) + + self.assertEqual(result, datetime.date(1984, 7, 31)) + + def test_from_native_empty(self): + f = serializers.DateField() + result = f.from_native('') + + self.assertEqual(result, None) + + def test_from_native_invalid_date(self): + f = serializers.DateField() + + try: + f.from_native('1984-42-31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Date has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + u'[January through December] DD, YYYY; DD [January through December] YYYY; ' + u'DD [January through December], YYYY']) + else: + self.fail("ValidationError was not properly raised") + + +class DateTimeFieldTest(TestCase): + def test_valid_default_date_time_input_formats(self): + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84'}) + self.assertTrue(serializer.is_valid()) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_valid_default_date_time_input_formats_for_django_gte_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_date_time_input_formats(self): + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984 04:31'}) + self.assertTrue(serializer.is_valid()) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) + + @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") + def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead:' + u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) + + def test_wrong_custom_date_time_input_format(self): + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) + + def test_from_native(self): + f = serializers.DateTimeField() + result = f.from_native('1984-07-31 04:31') + + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result) + + def test_from_native_datetime_datetime(self): + """ + Make sure from_native() accepts a datetime.date instance. + """ + f = serializers.DateTimeField() + result = f.from_native(datetime.datetime(1984, 7, 31)) + + self.assertEqual(result, datetime.datetime(1984, 7, 31)) + + def test_from_native_empty(self): + f = serializers.DateTimeField() + result = f.from_native('') + + self.assertEqual(result, None) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_from_native_invalid_datetime(self): + f = serializers.DateTimeField() + + try: + f.from_native('1984-42-31 04:31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + else: + self.fail("ValidationError was not properly raised") + + @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") + def test_from_native_invalid_datetime(self): + f = serializers.DateTimeField() + + try: + f.from_native('1984-42-31 04:31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead:' + u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + else: + self.fail("ValidationError was not properly raised") + + +class TimeFieldTest(TestCase): + def test_valid_default_time_input_formats(self): + serializer = TimeObjectSerializer(data={'time': '04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = TimeObjectSerializer(data={'time': '04:31:59'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_time_input_formats(self): + serializer = TimeObjectCustomFormatSerializer(data={'time': '04 -- 31'}) + self.assertTrue(serializer.is_valid()) + + serializer = TimeObjectCustomFormatSerializer(data={'time': '043159'}) + self.assertTrue(serializer.is_valid()) + + def test_wrong_default_time_input_format(self): + serializer = TimeObjectSerializer(data={'time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) + + def test_wrong_custom_time_input_format(self): + serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) + + def test_from_native(self): f = serializers.TimeField() - result = f.from_native('12:34:56.987654') + result = f.from_native('12:34:56') - self.assertEqual(datetime.time(12, 34, 56, 987654), result) + self.assertEqual(datetime.time(12, 34, 56), result) - def test_TimeField_from_native_datetime_time(self): + def test_from_native_datetime_time(self): """ Make sure from_native() accepts a datetime.time instance. """ f = serializers.TimeField() result = f.from_native(datetime.time(12, 34, 56)) + self.assertEqual(result, datetime.time(12, 34, 56)) - def test_TimeField_from_native_empty(self): + def test_from_native_empty(self): f = serializers.TimeField() result = f.from_native('') + self.assertEqual(result, None) - def test_TimeField_from_native_invalid_time(self): + def test_from_native_invalid_time(self): f = serializers.TimeField() try: f.from_native('12:69:12') except validators.ValidationError as e: - self.assertEqual(e.messages, ["'12:69:12' value has an invalid " - "format. It must be a valid time " - "in the HH:MM[:ss[.uuuuuu]] format."]) + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM"]) else: - self.fail("ValidationError was not properly raised") - - def test_TimeFieldModelSerializer(self): - serializer = TimeFieldModelSerializer() - self.assertTrue(isinstance(serializer.fields['clock'], serializers.TimeField)) + self.fail("ValidationError was not properly raised") \ No newline at end of file diff --git a/rest_framework/utils/dates.py b/rest_framework/utils/dates.py new file mode 100644 index 000000000..f094f72db --- /dev/null +++ b/rest_framework/utils/dates.py @@ -0,0 +1,14 @@ +def get_readable_date_format(date_format): + mapping = [("%Y", "YYYY"), + ("%y", "YY"), + ("%m", "MM"), + ("%b", "[Jan through Dec]"), + ("%B", "[January through December]"), + ("%d", "DD"), + ("%H", "HH"), + ("%M", "MM"), + ("%S", "SS"), + ("%f", "uuuuuu")] + for k, v in mapping: + date_format = date_format.replace(k, v) + return date_format \ No newline at end of file From 956a65f5af2bfc19313dcc7c104eed3ff8cc58d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:35:39 +0100 Subject: [PATCH 02/20] Fix for python 3 support - thanks @Linovia --- rest_framework/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2260c430b..d38b726a6 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -448,7 +448,7 @@ class DateField(WritableField): form_field_class = forms.DateField default_error_messages = { - 'invalid': _(u"Date has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Date has wrong format. Use one of these formats instead: %s"), } empty = None @@ -489,7 +489,7 @@ class DateTimeField(WritableField): form_field_class = forms.DateTimeField default_error_messages = { - 'invalid': _(u"Datetime has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), } empty = None @@ -536,7 +536,7 @@ class TimeField(WritableField): form_field_class = forms.TimeField default_error_messages = { - 'invalid': _(u"Time has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Time has wrong format. Use one of these formats instead: %s"), } empty = None From 36d5b24b76af2d98efff9e8f414ec36e2a902994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:42:50 +0100 Subject: [PATCH 03/20] Fix for python 3 support --- rest_framework/tests/fields.py | 72 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index d21e247d5..e76079c9a 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -184,17 +184,17 @@ class DateFieldTest(TestCase): def test_wrong_default_date_input_format(self): serializer = DateObjectSerializer(data={'date': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - u'[January through December] DD, YYYY; DD [January through December] YYYY; ' - u'DD [January through December], YYYY']}) + self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + '[January through December] DD, YYYY; DD [January through December] YYYY; ' + 'DD [January through December], YYYY']}) def test_wrong_custom_date_input_format(self): serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) + self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) def test_from_native(self): f = serializers.DateField() @@ -223,12 +223,12 @@ class DateFieldTest(TestCase): try: f.from_native('1984-42-31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Date has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - u'[January through December] DD, YYYY; DD [January through December] YYYY; ' - u'DD [January through December], YYYY']) + self.assertEqual(e.messages, ['Date has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + '[January through December] DD, YYYY; DD [January through December] YYYY; ' + 'DD [January through December], YYYY']) else: self.fail("ValidationError was not properly raised") @@ -284,25 +284,25 @@ class DateTimeFieldTest(TestCase): def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead:' - u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) def test_wrong_custom_date_time_input_format(self): serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) def test_from_native(self): f = serializers.DateTimeField() @@ -326,31 +326,31 @@ class DateTimeFieldTest(TestCase): self.assertEqual(result, None) @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_from_native_invalid_datetime(self): + def test_from_native_invalid_datetime_for_django_gte_1_4(self): f = serializers.DateTimeField() try: f.from_native('1984-42-31 04:31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) else: self.fail("ValidationError was not properly raised") @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_from_native_invalid_datetime(self): + def test_from_native_invalid_datetime_for_django_lt_1_4(self): f = serializers.DateTimeField() try: f.from_native('1984-42-31 04:31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead:' - u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) else: self.fail("ValidationError was not properly raised") @@ -373,12 +373,12 @@ class TimeFieldTest(TestCase): def test_wrong_default_time_input_format(self): serializer = TimeObjectSerializer(data={'time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) + self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) def test_wrong_custom_time_input_format(self): serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) + self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) def test_from_native(self): f = serializers.TimeField() From 876a58e8bf566b9f05a1f6c921660cf3512ae3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 13:16:00 +0100 Subject: [PATCH 04/20] Add drf settings + output format + testcases --- rest_framework/fields.py | 40 ++- rest_framework/settings.py | 21 ++ rest_framework/tests/fields.py | 527 ++++++++++++++--------------- rest_framework/tests/filterset.py | 13 +- rest_framework/tests/pagination.py | 4 +- rest_framework/tests/serializer.py | 2 +- 6 files changed, 322 insertions(+), 285 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d38b726a6..bb77164a7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -13,12 +13,12 @@ from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import parse_date, parse_datetime + from rest_framework.compat import timezone from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text -from rest_framework.compat import parse_time +from rest_framework.settings import api_settings from rest_framework.utils.dates import get_readable_date_format @@ -453,7 +453,8 @@ class DateField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.DATE_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.DATE_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.DATE_OUTPUT_FORMAT) super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -470,7 +471,7 @@ class DateField(WritableField): if isinstance(value, datetime.date): return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -478,10 +479,15 @@ class DateField(WritableField): else: return parsed.date() - date_input_formats = '; '.join(self.format) + date_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class DateTimeField(WritableField): type_name = 'DateTimeField' @@ -494,7 +500,8 @@ class DateTimeField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.DATETIME_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.DATETIME_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.DATETIME_OUTPUT_FORMAT) super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -517,7 +524,7 @@ class DateTimeField(WritableField): value = timezone.make_aware(value, default_timezone) return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -525,10 +532,15 @@ class DateTimeField(WritableField): else: return parsed - datetime_input_formats = '; '.join(self.format) + datetime_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class TimeField(WritableField): type_name = 'TimeField' @@ -541,7 +553,8 @@ class TimeField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.TIME_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.TIME_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.TIME_OUTPUT_FORMAT) super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -551,7 +564,7 @@ class TimeField(WritableField): if isinstance(value, datetime.time): return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -559,10 +572,15 @@ class TimeField(WritableField): else: return parsed.time() - time_input_formats = '; '.join(self.format) + time_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class IntegerField(WritableField): type_name = 'IntegerField' diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b7aa0bbe0..717496ea5 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -76,6 +76,27 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + + # Input and output formats + 'DATE_INPUT_FORMATS': ( + '%Y-%m-%d', # '1984-07-31' + ), + 'DATE_OUTPUT_FORMAT': None, + + 'DATETIME_INPUT_FORMATS': ( + '%Y-%m-%d', # '1984-07-31' + '%Y-%m-%d %H:%M', # '1984-07-31 04:31' + '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' + '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ), + 'DATETIME_OUTPUT_FORMAT': None, + + 'TIME_INPUT_FORMATS': ( + '%H:%M', # '04:31' + '%H:%M:%S', # '04:31:59' + '%H:%M:%S.%f', # '04:31:59.000200' + ), + 'TIME_OUTPUT_FORMAT': None, } diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index e76079c9a..ddedd6f99 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -4,11 +4,9 @@ General serializer field tests. from __future__ import unicode_literals import datetime -import django from django.db import models from django.test import TestCase from django.core import validators -from django.utils import unittest from rest_framework import serializers @@ -22,21 +20,6 @@ class CharPrimaryKeyModel(models.Model): id = models.CharField(max_length=20, primary_key=True) -class DateObject(object): - def __init__(self, date): - self.date = date - - -class DateTimeObject(object): - def __init__(self, date_time): - self.date_time = date_time - - -class TimeObject(object): - def __init__(self, time): - self.time = time - - class TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel @@ -47,66 +30,6 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): model = CharPrimaryKeyModel -class DateObjectSerializer(serializers.Serializer): - date = serializers.DateField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date = attrs['date'] - return instance - return DateObject(**attrs) - - -class DateObjectCustomFormatSerializer(serializers.Serializer): - date = serializers.DateField(format=("%Y", "%Y -- %m")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date = attrs['date'] - return instance - return DateObject(**attrs) - - -class DateTimeObjectSerializer(serializers.Serializer): - date_time = serializers.DateTimeField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date_time = attrs['date_time'] - return instance - return DateTimeObject(**attrs) - - -class DateTimeObjectCustomFormatSerializer(serializers.Serializer): - date_time = serializers.DateTimeField(format=("%Y", "%Y %H:%M")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date_time = attrs['date_time'] - return instance - return DateTimeObject(**attrs) - - -class TimeObjectSerializer(serializers.Serializer): - time = serializers.TimeField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.time = attrs['time'] - return instance - return TimeObject(**attrs) - - -class TimeObjectCustomFormatSerializer(serializers.Serializer): - time = serializers.TimeField(format=("%H -- %M", "%H%M%S")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.time = attrs['time'] - return instance - return TimeObject(**attrs) - - class TimeFieldModel(models.Model): clock = models.TimeField() @@ -140,273 +63,347 @@ class BasicFieldTests(TestCase): class DateFieldTest(TestCase): - def test_valid_default_date_input_formats(self): - serializer = DateObjectSerializer(data={'date': '1984-07-31'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the DateFieldTest from_native() and to_native() behavior + """ - serializer = DateObjectSerializer(data={'date': '07/31/1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '07/31/84'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'Jul 31 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'Jul 31, 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'July 31 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'July 31, 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 July 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 July, 1984'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_date_input_formats(self): - serializer = DateObjectCustomFormatSerializer(data={'date': '1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectCustomFormatSerializer(data={'date': '1984 -- 07'}) - self.assertTrue(serializer.is_valid()) - - def test_wrong_default_date_input_format(self): - serializer = DateObjectSerializer(data={'date': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - '[January through December] DD, YYYY; DD [January through December] YYYY; ' - 'DD [January through December], YYYY']}) - - def test_wrong_custom_date_input_format(self): - serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.DateField() - result = f.from_native('1984-07-31') + result_1 = f.from_native('1984-07-31') - self.assertEqual(datetime.date(1984, 7, 31), result) + self.assertEqual(datetime.date(1984, 7, 31), result_1) def test_from_native_datetime_date(self): """ Make sure from_native() accepts a datetime.date instance. """ f = serializers.DateField() - result = f.from_native(datetime.date(1984, 7, 31)) + result_1 = f.from_native(datetime.date(1984, 7, 31)) - self.assertEqual(result, datetime.date(1984, 7, 31)) + self.assertEqual(result_1, datetime.date(1984, 7, 31)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.DateField(input_formats=['%Y -- %d']) + result = f.from_native('1984 -- 31') + + self.assertEqual(datetime.date(1984, 1, 31), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.DateField(input_formats=['%Y -- %d']) + + try: + f.from_native('1984-07-31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.DateField() result = f.from_native('') self.assertEqual(result, None) def test_from_native_invalid_date(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid date. + """ f = serializers.DateField() try: - f.from_native('1984-42-31') + f.from_native('1984-13-31') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Date has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - '[January through December] DD, YYYY; DD [January through December] YYYY; ' - 'DD [January through December], YYYY']) + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) else: self.fail("ValidationError was not properly raised") + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ + f = serializers.DateField() + + try: + f.from_native('1984 -- 31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) + else: + self.fail("ValidationError was not properly raised") + + def test_to_native(self): + """ + Make sure to_native() returns isoformat as default. + """ + f = serializers.DateField() + + result_1 = f.to_native(datetime.date(1984, 7, 31)) + + self.assertEqual('1984-07-31', result_1) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.DateField(output_format="%Y - %m.%d") + + result_1 = f.to_native(datetime.date(1984, 7, 31)) + + self.assertEqual('1984 - 07.31', result_1) + class DateTimeFieldTest(TestCase): - def test_valid_default_date_time_input_formats(self): - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the DateTimeField from_native() and to_native() behavior + """ - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84'}) - self.assertTrue(serializer.is_valid()) - - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_valid_default_date_time_input_formats_for_django_gte_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_date_time_input_formats(self): - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984 04:31'}) - self.assertTrue(serializer.is_valid()) - - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) - - @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) - - def test_wrong_custom_date_time_input_format(self): - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.DateTimeField() - result = f.from_native('1984-07-31 04:31') + result_1 = f.from_native('1984-07-31') + result_2 = f.from_native('1984-07-31 04:31') + result_3 = f.from_native('1984-07-31 04:31:59') + result_4 = f.from_native('1984-07-31 04:31:59.000200') - self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result) + 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_from_native_datetime_datetime(self): """ - Make sure from_native() accepts a datetime.date instance. + Make sure from_native() accepts a datetime.datetime instance. """ f = serializers.DateTimeField() - result = f.from_native(datetime.datetime(1984, 7, 31)) + result_1 = f.from_native(datetime.datetime(1984, 7, 31)) + result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) + result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) + result_4 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) - self.assertEqual(result, datetime.datetime(1984, 7, 31)) + self.assertEqual(result_1, datetime.datetime(1984, 7, 31)) + self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31)) + self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59)) + self.assertEqual(result_4, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.DateTimeField(input_formats=['%Y -- %H:%M']) + result = f.from_native('1984 -- 04:59') + + self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.DateTimeField(input_formats=['%Y -- %H:%M']) + + try: + f.from_native('1984-07-31 04:31:59') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- HH:MM"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.DateTimeField() result = f.from_native('') self.assertEqual(result, None) - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_from_native_invalid_datetime_for_django_gte_1_4(self): + def test_from_native_invalid_datetime(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid datetime. + """ f = serializers.DateTimeField() try: - f.from_native('1984-42-31 04:31') + f.from_native('04:61:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " + "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " + "YYYY-MM-DD HH:MM:SS.uuuuuu"]) else: self.fail("ValidationError was not properly raised") - @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_from_native_invalid_datetime_for_django_lt_1_4(self): + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ f = serializers.DateTimeField() try: - f.from_native('1984-42-31 04:31') + f.from_native('04 -- 31') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " + "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " + "YYYY-MM-DD HH:MM:SS.uuuuuu"]) else: self.fail("ValidationError was not properly raised") + def test_to_native(self): + """ + Make sure to_native() returns isoformat as default. + """ + f = serializers.DateTimeField() + + 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) + self.assertEqual('1984-07-31T04:31:59.000200', result_4) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.DateTimeField(output_format="%Y - %H:%M") + + 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 - 00:00', result_1) + self.assertEqual('1984 - 04:31', result_2) + self.assertEqual('1984 - 04:31', result_3) + self.assertEqual('1984 - 04:31', result_4) + class TimeFieldTest(TestCase): - def test_valid_default_time_input_formats(self): - serializer = TimeObjectSerializer(data={'time': '04:31'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the TimeField from_native() and to_native() behavior + """ - serializer = TimeObjectSerializer(data={'time': '04:31:59'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_time_input_formats(self): - serializer = TimeObjectCustomFormatSerializer(data={'time': '04 -- 31'}) - self.assertTrue(serializer.is_valid()) - - serializer = TimeObjectCustomFormatSerializer(data={'time': '043159'}) - self.assertTrue(serializer.is_valid()) - - def test_wrong_default_time_input_format(self): - serializer = TimeObjectSerializer(data={'time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) - - def test_wrong_custom_time_input_format(self): - serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.TimeField() - result = f.from_native('12:34:56') + result_1 = f.from_native('04:31') + result_2 = f.from_native('04:31:59') + result_3 = f.from_native('04:31:59.000200') - self.assertEqual(datetime.time(12, 34, 56), result) + 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_from_native_datetime_time(self): """ Make sure from_native() accepts a datetime.time instance. """ f = serializers.TimeField() - result = f.from_native(datetime.time(12, 34, 56)) + result_1 = f.from_native(datetime.time(4, 31)) + result_2 = f.from_native(datetime.time(4, 31, 59)) + result_3 = f.from_native(datetime.time(4, 31, 59, 200)) - self.assertEqual(result, datetime.time(12, 34, 56)) + self.assertEqual(result_1, datetime.time(4, 31)) + self.assertEqual(result_2, datetime.time(4, 31, 59)) + self.assertEqual(result_3, datetime.time(4, 31, 59, 200)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.TimeField(input_formats=['%H -- %M']) + result = f.from_native('04 -- 31') + + self.assertEqual(datetime.time(4, 31), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.TimeField(input_formats=['%H -- %M']) + + try: + f.from_native('04:31:59') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH -- MM"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.TimeField() result = f.from_native('') self.assertEqual(result, None) def test_from_native_invalid_time(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid time. + """ f = serializers.TimeField() try: - f.from_native('12:69:12') + f.from_native('04:61:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM"]) + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " + "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) else: - self.fail("ValidationError was not properly raised") \ No newline at end of file + self.fail("ValidationError was not properly raised") + + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ + f = serializers.TimeField() + + try: + f.from_native('04 -- 31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " + "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + else: + self.fail("ValidationError was not properly raised") + + def test_to_native(self): + """ + Make sure to_native() returns isoformat 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('04:31:00', result_1) + self.assertEqual('04:31:59', result_2) + self.assertEqual('04:31:59.000200', result_3) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.TimeField(output_format="%H - %S [%f]") + 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 - 00 [000000]', result_1) + self.assertEqual('04 - 59 [000000]', result_2) + self.assertEqual('04 - 59 [000200]', result_3) \ No newline at end of file diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index daea6e53d..16f9a48ec 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -65,8 +65,8 @@ class IntegrationTestFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - for obj in self.objects.all() + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + for obj in self.objects.all() ] @unittest.skipUnless(django_filters, 'django-filters not installed') @@ -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.assertEquals(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if f['date'] == search_date] + expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() == search_date] self.assertEquals(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.assertEquals(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if f['date'] > search_date] + expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() > search_date] self.assertEquals(response.data, expected_data) # Tests that the text filter set with 'icontains' in the filter class works. @@ -142,8 +142,9 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if f['date'] > search_date and - f['decimal'] < search_decimal] + expected_data = [f for f in self.data if + datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() > search_date and + f['decimal'] < search_decimal] self.assertEquals(response.data, expected_data) @unittest.skipUnless(django_filters, 'django-filters not installed') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index b85ce1448..c1efd4f56 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -112,8 +112,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - for obj in self.objects.all() + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + for obj in self.objects.all() ] self.view = FilterFieldsRootView.as_view() diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 671494b5d..1c99375fc 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -107,7 +107,7 @@ class BasicTests(TestCase): self.expected = { 'email': 'tom@example.com', 'content': 'Happy new year!', - 'created': datetime.datetime(2012, 1, 1), + 'created': '2012-01-01T00:00:00', 'sub_comment': 'And Merry Christmas!' } self.person_data = {'name': 'dwight', 'age': 35} From 7e702439eb4a82fe2ef45911fbb176e95d5f0bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 15:03:27 +0100 Subject: [PATCH 05/20] Add docs update - part 1 --- docs/api-guide/fields.md | 24 ++++++++++++++++++------ docs/api-guide/settings.md | 35 +++++++++++++++++++++++++++++++++++ docs/topics/release-notes.md | 4 +--- rest_framework/fields.py | 18 +++++++++--------- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index a3dc4fe29..898de12da 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -185,18 +185,20 @@ Corresponds to `django.forms.fields.RegexField` A date representation. -Uses `DATE_INPUT_FORMATS` to validate date. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.DateField` +**Signature:** `DateField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `DATE_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `DATE_OUTPUT_FORMAT` + ## DateTimeField A date and time representation. -Uses `DATETIME_INPUT_FORMATS` to validate date_time. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.DateTimeField` @@ -211,16 +213,26 @@ If you want to override this behavior, you'll need to declare the `DateTimeField class Meta: model = Comment +**Signature:** `DateTimeField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `DATETIME_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `DATETIME_OUTPUT_FORMAT` + ## TimeField A time representation. -Uses `TIME_INPUT_FORMATS` to validate time. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.TimeField` +**Signature:** `TimeField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `TIME_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `TIME_OUTPUT_FORMAT` + ## IntegerField An integer representation. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index e103fbab4..9080cacb0 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -174,4 +174,39 @@ The name of a parameter in the URL conf that may be used to provide a format suf Default: `'format'` +## DATE_INPUT_FORMATS + +Default: + + ( + '%Y-%m-%d', # '1984-07-31' + ) + +## DATE_OUTPUT_FORMAT + +## DATETIME_INPUT_FORMATS + +Default: + + ( + '%Y-%m-%d', # '1984-07-31' + '%Y-%m-%d %H:%M', # '1984-07-31 04:31' + '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' + '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ) + +## DATETIME_OUTPUT_FORMAT + +## TIME_INPUT_FORMATS + +Default: + + ( + '%H:%M', # '04:31' + '%H:%M:%S', # '04:31:59' + '%H:%M:%S.%f', # '04:31:59.000200' + ) + +## TIME_OUTPUT_FORMAT + [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 31ff68dda..f5f4dc550 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,9 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfixes for model field validation edge-cases. -* Support `DATE_INPUT_FORMATS` for `DateField` validation -* Support `DATETIME_INPUT_FORMATS` for `DateTimeField` validation -* Support `TIME_INPUT_FORMATS` for `TimeField` validation +* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField` ### 2.2.1 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb77164a7..3eaa532ac 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -452,9 +452,9 @@ class DateField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.DATE_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.DATE_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.DATE_INPUT_FORMATS + self.output_format = output_format or api_settings.DATE_OUTPUT_FORMAT super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -499,9 +499,9 @@ class DateTimeField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.DATETIME_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.DATETIME_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.DATETIME_INPUT_FORMATS + self.output_format = output_format or api_settings.DATETIME_OUTPUT_FORMAT super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -552,9 +552,9 @@ class TimeField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.TIME_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.TIME_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.TIME_INPUT_FORMATS + self.output_format = output_format or api_settings.TIME_OUTPUT_FORMAT super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): From 29a54b5a773c0bc854a57393364437efd2c7a172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 16:24:25 +0100 Subject: [PATCH 06/20] Add new ISO8601 setting + integration --- rest_framework/__init__.py | 3 ++ rest_framework/fields.py | 83 +++++++++++++++++++++++----------- rest_framework/settings.py | 20 ++++---- rest_framework/tests/fields.py | 38 +++++++--------- 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 29f3d7bc0..d26bb6bf8 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -4,3 +4,6 @@ VERSION = __version__ # synonym # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' + +# Default input and output format +ISO8601 = 'iso-8601' \ No newline at end of file diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3eaa532ac..c3e83c5e9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,9 +11,11 @@ from django.core.exceptions import ValidationError from django.conf import settings from django import forms from django.forms import widgets +from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ +from rest_framework import ISO8601 from rest_framework.compat import timezone from rest_framework.compat import BytesIO from rest_framework.compat import six @@ -472,21 +474,30 @@ class DateField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_date(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed.date() + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.date() - date_input_formats = '; '.join(self.input_formats) + date_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD') msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class DateTimeField(WritableField): @@ -525,21 +536,30 @@ class DateTimeField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_datetime(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed - datetime_input_formats = '; '.join(self.input_formats) + datetime_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]') msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class TimeField(WritableField): @@ -565,21 +585,30 @@ class TimeField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_time(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed.time() + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.time() - time_input_formats = '; '.join(self.input_formats) + time_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'HH:MM[:ss[.uuuuuu]]') msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class IntegerField(WritableField): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 717496ea5..02d751e19 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,8 +18,11 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals + from django.conf import settings from django.utils import importlib + +from rest_framework import ISO8601 from rest_framework.compat import six @@ -79,24 +82,19 @@ DEFAULTS = { # Input and output formats 'DATE_INPUT_FORMATS': ( - '%Y-%m-%d', # '1984-07-31' + ISO8601, ), - 'DATE_OUTPUT_FORMAT': None, + 'DATE_OUTPUT_FORMAT': ISO8601, 'DATETIME_INPUT_FORMATS': ( - '%Y-%m-%d', # '1984-07-31' - '%Y-%m-%d %H:%M', # '1984-07-31 04:31' - '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' - '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ISO8601, ), - 'DATETIME_OUTPUT_FORMAT': None, + 'DATETIME_OUTPUT_FORMAT': ISO8601, 'TIME_INPUT_FORMATS': ( - '%H:%M', # '04:31' - '%H:%M:%S', # '04:31:59' - '%H:%M:%S.%f', # '04:31:59.000200' + ISO8601, ), - 'TIME_OUTPUT_FORMAT': None, + 'TIME_OUTPUT_FORMAT': ISO8601, } diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index ddedd6f99..3f8a6730e 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -173,30 +173,26 @@ class DateTimeFieldTest(TestCase): Make sure from_native() accepts default iso input formats. """ f = serializers.DateTimeField() - result_1 = f.from_native('1984-07-31') - result_2 = f.from_native('1984-07-31 04:31') - result_3 = f.from_native('1984-07-31 04:31:59') - result_4 = f.from_native('1984-07-31 04:31:59.000200') + result_1 = f.from_native('1984-07-31 04:31') + result_2 = f.from_native('1984-07-31 04:31:59') + result_3 = f.from_native('1984-07-31 04:31:59.000200') - 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) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3) def test_from_native_datetime_datetime(self): """ Make sure from_native() accepts a datetime.datetime instance. """ f = serializers.DateTimeField() - result_1 = f.from_native(datetime.datetime(1984, 7, 31)) - result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) - result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) - result_4 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) + result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) + result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) - self.assertEqual(result_1, datetime.datetime(1984, 7, 31)) - self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31)) - self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59)) - self.assertEqual(result_4, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31)) + self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59)) + self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) def test_from_native_custom_format(self): """ @@ -239,8 +235,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " - "YYYY-MM-DD HH:MM:SS.uuuuuu"]) + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) else: self.fail("ValidationError was not properly raised") @@ -254,8 +249,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " - "YYYY-MM-DD HH:MM:SS.uuuuuu"]) + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) else: self.fail("ValidationError was not properly raised") @@ -364,7 +358,7 @@ class TimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + "HH:MM[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") @@ -378,7 +372,7 @@ class TimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + "HH:MM[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") From d20089c8d18e04dec03c3836b47b4436945d0a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 16:35:27 +0100 Subject: [PATCH 07/20] Update docs --- docs/api-guide/settings.md | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 9080cacb0..04569f44e 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -176,37 +176,26 @@ Default: `'format'` ## DATE_INPUT_FORMATS -Default: - - ( - '%Y-%m-%d', # '1984-07-31' - ) +Default: `ISO8601` ## DATE_OUTPUT_FORMAT +Default: `ISO8601` + ## DATETIME_INPUT_FORMATS -Default: - - ( - '%Y-%m-%d', # '1984-07-31' - '%Y-%m-%d %H:%M', # '1984-07-31 04:31' - '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' - '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' - ) +Default: `ISO8601` ## DATETIME_OUTPUT_FORMAT +Default: `ISO8601` + ## TIME_INPUT_FORMATS -Default: - - ( - '%H:%M', # '04:31' - '%H:%M:%S', # '04:31:59' - '%H:%M:%S.%f', # '04:31:59.000200' - ) +Default: `ISO8601` ## TIME_OUTPUT_FORMAT +Default: `ISO8601` + [cite]: http://www.python.org/dev/peps/pep-0020/ From 9157db5da0b5601793d1a9f24e9cb10670a82be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:09:54 +0100 Subject: [PATCH 08/20] Add better date / datetime validation (pull 2) addition to #631 with update to master + timefield support --- docs/api-guide/fields.md | 12 ++ docs/topics/release-notes.md | 3 + rest_framework/fields.py | 87 ++++----- rest_framework/tests/fields.py | 343 +++++++++++++++++++++++++++++++-- rest_framework/utils/dates.py | 14 ++ 5 files changed, 403 insertions(+), 56 deletions(-) create mode 100644 rest_framework/utils/dates.py diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d7f9197f1..abacc1b82 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -185,12 +185,20 @@ Corresponds to `django.forms.fields.RegexField` A date representation. +Uses `DATE_INPUT_FORMATS` to validate date. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.DateField` ## DateTimeField A date and time representation. +Uses `DATETIME_INPUT_FORMATS` to validate date_time. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.DateTimeField` When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default. @@ -207,6 +215,10 @@ If you want to override this behavior, you'll need to declare the `DateTimeField A time representation. +Uses `TIME_INPUT_FORMATS` to validate time. + +Optionally takes `format` as parameter to replace the matching pattern. + Corresponds to `django.db.models.fields.TimeField` ## IntegerField diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 43499c9a9..f60382acf 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -45,6 +45,9 @@ You can determine your currently installed version using `pip freeze`: * Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view. * Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfixes for model field validation edge-cases. +* Support `DATE_INPUT_FORMATS` for `DateField` validation +* Support `DATETIME_INPUT_FORMATS` for `DateTimeField` validation +* Support `TIME_INPUT_FORMATS` for `TimeField` validation ### 2.2.1 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 86c3a837a..2260c430b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -19,6 +19,7 @@ from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import parse_time +from rest_framework.utils.dates import get_readable_date_format def is_simple_callable(obj): @@ -447,13 +448,14 @@ class DateField(WritableField): form_field_class = forms.DateField default_error_messages = { - 'invalid': _("'%s' value has an invalid date format. It must be " - "in YYYY-MM-DD format."), - 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) " - "but it is an invalid date."), + 'invalid': _(u"Date has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.DATE_INPUT_FORMATS) + super(DateField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -468,15 +470,16 @@ class DateField(WritableField): if isinstance(value, datetime.date): return value - try: - parsed = parse_date(value) - if parsed is not None: - return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid_date'] % value - raise ValidationError(msg) + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.date() - msg = self.error_messages['invalid'] % value + date_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) @@ -486,16 +489,14 @@ class DateTimeField(WritableField): form_field_class = forms.DateTimeField default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be in " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."), - 'invalid_date': _("'%s' value has the correct format " - "(YYYY-MM-DD) but it is an invalid date."), - 'invalid_datetime': _("'%s' value has the correct format " - "(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " - "but it is an invalid date/time."), + 'invalid': _(u"Datetime has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.DATETIME_INPUT_FORMATS) + super(DateTimeField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -516,23 +517,16 @@ class DateTimeField(WritableField): value = timezone.make_aware(value, default_timezone) return value - try: - parsed = parse_datetime(value) - if parsed is not None: + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid_datetime'] % value - raise ValidationError(msg) - try: - parsed = parse_date(value) - if parsed is not None: - return datetime.datetime(parsed.year, parsed.month, parsed.day) - except (ValueError, TypeError): - msg = self.error_messages['invalid_date'] % value - raise ValidationError(msg) - - msg = self.error_messages['invalid'] % value + datetime_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) @@ -542,11 +536,14 @@ class TimeField(WritableField): form_field_class = forms.TimeField default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be a valid " - "time in the HH:MM[:ss[.uuuuuu]] format."), + 'invalid': _(u"Time has wrong format. Use one of these formats instead: %s"), } empty = None + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', settings.TIME_INPUT_FORMATS) + super(TimeField, self).__init__(*args, **kwargs) + def from_native(self, value): if value in validators.EMPTY_VALUES: return None @@ -554,13 +551,17 @@ class TimeField(WritableField): if isinstance(value, datetime.time): return value - try: - parsed = parse_time(value) - assert parsed is not None - return parsed - except (ValueError, TypeError): - msg = self.error_messages['invalid'] % value - raise ValidationError(msg) + for format in self.format: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.time() + + time_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) + raise ValidationError(msg) class IntegerField(WritableField): diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 840ed3200..375176428 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -3,9 +3,13 @@ General serializer field tests. """ from __future__ import unicode_literals import datetime + +import django from django.db import models from django.test import TestCase from django.core import validators +from django.utils import unittest + from rest_framework import serializers @@ -18,6 +22,21 @@ class CharPrimaryKeyModel(models.Model): id = models.CharField(max_length=20, primary_key=True) +class DateObject(object): + def __init__(self, date): + self.date = date + + +class DateTimeObject(object): + def __init__(self, date_time): + self.date_time = date_time + + +class TimeObject(object): + def __init__(self, time): + self.time = time + + class TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel @@ -28,6 +47,66 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): model = CharPrimaryKeyModel +class DateObjectSerializer(serializers.Serializer): + date = serializers.DateField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date = attrs['date'] + return instance + return DateObject(**attrs) + + +class DateObjectCustomFormatSerializer(serializers.Serializer): + date = serializers.DateField(format=("%Y", "%Y -- %m")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date = attrs['date'] + return instance + return DateObject(**attrs) + + +class DateTimeObjectSerializer(serializers.Serializer): + date_time = serializers.DateTimeField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date_time = attrs['date_time'] + return instance + return DateTimeObject(**attrs) + + +class DateTimeObjectCustomFormatSerializer(serializers.Serializer): + date_time = serializers.DateTimeField(format=("%Y", "%Y %H:%M")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.date_time = attrs['date_time'] + return instance + return DateTimeObject(**attrs) + + +class TimeObjectSerializer(serializers.Serializer): + time = serializers.TimeField() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.time = attrs['time'] + return instance + return TimeObject(**attrs) + + +class TimeObjectCustomFormatSerializer(serializers.Serializer): + time = serializers.TimeField(format=("%H -- %M", "%H%M%S")) + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.time = attrs['time'] + return instance + return TimeObject(**attrs) + + class TimeFieldModel(models.Model): clock = models.TimeField() @@ -59,37 +138,275 @@ class BasicFieldTests(TestCase): serializer = CharPrimaryKeyModelSerializer() self.assertEqual(serializer.fields['id'].read_only, False) - def test_TimeField_from_native(self): + +class DateFieldTest(TestCase): + def test_valid_default_date_input_formats(self): + serializer = DateObjectSerializer(data={'date': '1984-07-31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '07/31/1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '07/31/84'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'Jul 31 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'Jul 31, 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'July 31 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': 'July 31, 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 July 1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectSerializer(data={'date': '31 July, 1984'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_date_input_formats(self): + serializer = DateObjectCustomFormatSerializer(data={'date': '1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateObjectCustomFormatSerializer(data={'date': '1984 -- 07'}) + self.assertTrue(serializer.is_valid()) + + def test_wrong_default_date_input_format(self): + serializer = DateObjectSerializer(data={'date': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + u'[January through December] DD, YYYY; DD [January through December] YYYY; ' + u'DD [January through December], YYYY']}) + + def test_wrong_custom_date_input_format(self): + serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) + + def test_from_native(self): + f = serializers.DateField() + result = f.from_native('1984-07-31') + + self.assertEqual(datetime.date(1984, 7, 31), result) + + def test_from_native_datetime_date(self): + """ + Make sure from_native() accepts a datetime.date instance. + """ + f = serializers.DateField() + result = f.from_native(datetime.date(1984, 7, 31)) + + self.assertEqual(result, datetime.date(1984, 7, 31)) + + def test_from_native_empty(self): + f = serializers.DateField() + result = f.from_native('') + + self.assertEqual(result, None) + + def test_from_native_invalid_date(self): + f = serializers.DateField() + + try: + f.from_native('1984-42-31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Date has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + u'[January through December] DD, YYYY; DD [January through December] YYYY; ' + u'DD [January through December], YYYY']) + else: + self.fail("ValidationError was not properly raised") + + +class DateTimeFieldTest(TestCase): + def test_valid_default_date_time_input_formats(self): + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84'}) + self.assertTrue(serializer.is_valid()) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_valid_default_date_time_input_formats_for_django_gte_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59.123456'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_date_time_input_formats(self): + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984'}) + self.assertTrue(serializer.is_valid()) + + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984 04:31'}) + self.assertTrue(serializer.is_valid()) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) + + @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") + def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): + serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead:' + u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) + + def test_wrong_custom_date_time_input_format(self): + serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) + + def test_from_native(self): + f = serializers.DateTimeField() + result = f.from_native('1984-07-31 04:31') + + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result) + + def test_from_native_datetime_datetime(self): + """ + Make sure from_native() accepts a datetime.date instance. + """ + f = serializers.DateTimeField() + result = f.from_native(datetime.datetime(1984, 7, 31)) + + self.assertEqual(result, datetime.datetime(1984, 7, 31)) + + def test_from_native_empty(self): + f = serializers.DateTimeField() + result = f.from_native('') + + self.assertEqual(result, None) + + @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") + def test_from_native_invalid_datetime(self): + f = serializers.DateTimeField() + + try: + f.from_native('1984-42-31 04:31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead: ' + u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + else: + self.fail("ValidationError was not properly raised") + + @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") + def test_from_native_invalid_datetime(self): + f = serializers.DateTimeField() + + try: + f.from_native('1984-42-31 04:31') + except validators.ValidationError as e: + self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead:' + u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + else: + self.fail("ValidationError was not properly raised") + + +class TimeFieldTest(TestCase): + def test_valid_default_time_input_formats(self): + serializer = TimeObjectSerializer(data={'time': '04:31'}) + self.assertTrue(serializer.is_valid()) + + serializer = TimeObjectSerializer(data={'time': '04:31:59'}) + self.assertTrue(serializer.is_valid()) + + def test_valid_custom_time_input_formats(self): + serializer = TimeObjectCustomFormatSerializer(data={'time': '04 -- 31'}) + self.assertTrue(serializer.is_valid()) + + serializer = TimeObjectCustomFormatSerializer(data={'time': '043159'}) + self.assertTrue(serializer.is_valid()) + + def test_wrong_default_time_input_format(self): + serializer = TimeObjectSerializer(data={'time': 'something wrong'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) + + def test_wrong_custom_time_input_format(self): + serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) + + def test_from_native(self): f = serializers.TimeField() - result = f.from_native('12:34:56.987654') + result = f.from_native('12:34:56') - self.assertEqual(datetime.time(12, 34, 56, 987654), result) + self.assertEqual(datetime.time(12, 34, 56), result) - def test_TimeField_from_native_datetime_time(self): + def test_from_native_datetime_time(self): """ Make sure from_native() accepts a datetime.time instance. """ f = serializers.TimeField() result = f.from_native(datetime.time(12, 34, 56)) + self.assertEqual(result, datetime.time(12, 34, 56)) - def test_TimeField_from_native_empty(self): + def test_from_native_empty(self): f = serializers.TimeField() result = f.from_native('') + self.assertEqual(result, None) - def test_TimeField_from_native_invalid_time(self): + def test_from_native_invalid_time(self): f = serializers.TimeField() try: f.from_native('12:69:12') except validators.ValidationError as e: - self.assertEqual(e.messages, ["'12:69:12' value has an invalid " - "format. It must be a valid time " - "in the HH:MM[:ss[.uuuuuu]] format."]) + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM"]) else: self.fail("ValidationError was not properly raised") - - def test_TimeFieldModelSerializer(self): - serializer = TimeFieldModelSerializer() - self.assertTrue(isinstance(serializer.fields['clock'], serializers.TimeField)) diff --git a/rest_framework/utils/dates.py b/rest_framework/utils/dates.py new file mode 100644 index 000000000..f094f72db --- /dev/null +++ b/rest_framework/utils/dates.py @@ -0,0 +1,14 @@ +def get_readable_date_format(date_format): + mapping = [("%Y", "YYYY"), + ("%y", "YY"), + ("%m", "MM"), + ("%b", "[Jan through Dec]"), + ("%B", "[January through December]"), + ("%d", "DD"), + ("%H", "HH"), + ("%M", "MM"), + ("%S", "SS"), + ("%f", "uuuuuu")] + for k, v in mapping: + date_format = date_format.replace(k, v) + return date_format \ No newline at end of file From b2165cc76a97ee09c0ff109df2140e2950549a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:35:39 +0100 Subject: [PATCH 09/20] Fix for python 3 support - thanks @Linovia --- rest_framework/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2260c430b..d38b726a6 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -448,7 +448,7 @@ class DateField(WritableField): form_field_class = forms.DateField default_error_messages = { - 'invalid': _(u"Date has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Date has wrong format. Use one of these formats instead: %s"), } empty = None @@ -489,7 +489,7 @@ class DateTimeField(WritableField): form_field_class = forms.DateTimeField default_error_messages = { - 'invalid': _(u"Datetime has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), } empty = None @@ -536,7 +536,7 @@ class TimeField(WritableField): form_field_class = forms.TimeField default_error_messages = { - 'invalid': _(u"Time has wrong format. Use one of these formats instead: %s"), + 'invalid': _("Time has wrong format. Use one of these formats instead: %s"), } empty = None From ef5752f8b5de3f75310d654f3da649ad4a9a6db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 26 Feb 2013 11:42:50 +0100 Subject: [PATCH 10/20] Fix for python 3 support --- rest_framework/tests/fields.py | 72 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 375176428..a34290964 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -184,17 +184,17 @@ class DateFieldTest(TestCase): def test_wrong_default_date_input_format(self): serializer = DateObjectSerializer(data={'date': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - u'[January through December] DD, YYYY; DD [January through December] YYYY; ' - u'DD [January through December], YYYY']}) + self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + '[January through December] DD, YYYY; DD [January through December] YYYY; ' + 'DD [January through December], YYYY']}) def test_wrong_custom_date_input_format(self): serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': [u'Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) + self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) def test_from_native(self): f = serializers.DateField() @@ -223,12 +223,12 @@ class DateFieldTest(TestCase): try: f.from_native('1984-42-31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Date has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - u'[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - u'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - u'[January through December] DD, YYYY; DD [January through December] YYYY; ' - u'DD [January through December], YYYY']) + self.assertEqual(e.messages, ['Date has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' + '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' + 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' + '[January through December] DD, YYYY; DD [January through December] YYYY; ' + 'DD [January through December], YYYY']) else: self.fail("ValidationError was not properly raised") @@ -284,25 +284,25 @@ class DateTimeFieldTest(TestCase): def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead:' - u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) def test_wrong_custom_date_time_input_format(self): serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': [u'Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) + self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) def test_from_native(self): f = serializers.DateTimeField() @@ -326,31 +326,31 @@ class DateTimeFieldTest(TestCase): self.assertEqual(result, None) @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_from_native_invalid_datetime(self): + def test_from_native_invalid_datetime_for_django_gte_1_4(self): f = serializers.DateTimeField() try: f.from_native('1984-42-31 04:31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead: ' - u'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - u'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - u'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - u'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' + 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' + 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' + 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) else: self.fail("ValidationError was not properly raised") @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_from_native_invalid_datetime(self): + def test_from_native_invalid_datetime_for_django_lt_1_4(self): f = serializers.DateTimeField() try: f.from_native('1984-42-31 04:31') except validators.ValidationError as e: - self.assertEqual(e.messages, [u'Datetime has wrong format. Use one of these formats instead:' - u' YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - u'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - u'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' + 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' + 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' + 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) else: self.fail("ValidationError was not properly raised") @@ -373,12 +373,12 @@ class TimeFieldTest(TestCase): def test_wrong_default_time_input_format(self): serializer = TimeObjectSerializer(data={'time': 'something wrong'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) + self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) def test_wrong_custom_time_input_format(self): serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': [u'Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) + self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) def test_from_native(self): f = serializers.TimeField() From f208d8d2bbe2f418caa51199070f703fba544d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 13:16:00 +0100 Subject: [PATCH 11/20] Add drf settings + output format + testcases --- rest_framework/fields.py | 40 ++- rest_framework/settings.py | 21 ++ rest_framework/tests/fields.py | 527 ++++++++++++++--------------- rest_framework/tests/filterset.py | 13 +- rest_framework/tests/pagination.py | 4 +- rest_framework/tests/serializer.py | 2 +- 6 files changed, 322 insertions(+), 285 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d38b726a6..bb77164a7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -13,12 +13,12 @@ from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import parse_date, parse_datetime + from rest_framework.compat import timezone from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text -from rest_framework.compat import parse_time +from rest_framework.settings import api_settings from rest_framework.utils.dates import get_readable_date_format @@ -453,7 +453,8 @@ class DateField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.DATE_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.DATE_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.DATE_OUTPUT_FORMAT) super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -470,7 +471,7 @@ class DateField(WritableField): if isinstance(value, datetime.date): return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -478,10 +479,15 @@ class DateField(WritableField): else: return parsed.date() - date_input_formats = '; '.join(self.format) + date_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class DateTimeField(WritableField): type_name = 'DateTimeField' @@ -494,7 +500,8 @@ class DateTimeField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.DATETIME_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.DATETIME_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.DATETIME_OUTPUT_FORMAT) super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -517,7 +524,7 @@ class DateTimeField(WritableField): value = timezone.make_aware(value, default_timezone) return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -525,10 +532,15 @@ class DateTimeField(WritableField): else: return parsed - datetime_input_formats = '; '.join(self.format) + datetime_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class TimeField(WritableField): type_name = 'TimeField' @@ -541,7 +553,8 @@ class TimeField(WritableField): empty = None def __init__(self, *args, **kwargs): - self.format = kwargs.pop('format', settings.TIME_INPUT_FORMATS) + self.input_formats = kwargs.pop('input_formats', api_settings.TIME_INPUT_FORMATS) + self.output_format = kwargs.pop('output_format', api_settings.TIME_OUTPUT_FORMAT) super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -551,7 +564,7 @@ class TimeField(WritableField): if isinstance(value, datetime.time): return value - for format in self.format: + for format in self.input_formats: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): @@ -559,10 +572,15 @@ class TimeField(WritableField): else: return parsed.time() - time_input_formats = '; '.join(self.format) + time_input_formats = '; '.join(self.input_formats) msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) raise ValidationError(msg) + def to_native(self, value): + if self.output_format is not None: + return value.strftime(self.output_format) + return value.isoformat() + class IntegerField(WritableField): type_name = 'IntegerField' diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b7aa0bbe0..717496ea5 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -76,6 +76,27 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + + # Input and output formats + 'DATE_INPUT_FORMATS': ( + '%Y-%m-%d', # '1984-07-31' + ), + 'DATE_OUTPUT_FORMAT': None, + + 'DATETIME_INPUT_FORMATS': ( + '%Y-%m-%d', # '1984-07-31' + '%Y-%m-%d %H:%M', # '1984-07-31 04:31' + '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' + '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ), + 'DATETIME_OUTPUT_FORMAT': None, + + 'TIME_INPUT_FORMATS': ( + '%H:%M', # '04:31' + '%H:%M:%S', # '04:31:59' + '%H:%M:%S.%f', # '04:31:59.000200' + ), + 'TIME_OUTPUT_FORMAT': None, } diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index a34290964..6630e0b21 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -4,11 +4,9 @@ General serializer field tests. from __future__ import unicode_literals import datetime -import django from django.db import models from django.test import TestCase from django.core import validators -from django.utils import unittest from rest_framework import serializers @@ -22,21 +20,6 @@ class CharPrimaryKeyModel(models.Model): id = models.CharField(max_length=20, primary_key=True) -class DateObject(object): - def __init__(self, date): - self.date = date - - -class DateTimeObject(object): - def __init__(self, date_time): - self.date_time = date_time - - -class TimeObject(object): - def __init__(self, time): - self.time = time - - class TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel @@ -47,66 +30,6 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): model = CharPrimaryKeyModel -class DateObjectSerializer(serializers.Serializer): - date = serializers.DateField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date = attrs['date'] - return instance - return DateObject(**attrs) - - -class DateObjectCustomFormatSerializer(serializers.Serializer): - date = serializers.DateField(format=("%Y", "%Y -- %m")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date = attrs['date'] - return instance - return DateObject(**attrs) - - -class DateTimeObjectSerializer(serializers.Serializer): - date_time = serializers.DateTimeField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date_time = attrs['date_time'] - return instance - return DateTimeObject(**attrs) - - -class DateTimeObjectCustomFormatSerializer(serializers.Serializer): - date_time = serializers.DateTimeField(format=("%Y", "%Y %H:%M")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.date_time = attrs['date_time'] - return instance - return DateTimeObject(**attrs) - - -class TimeObjectSerializer(serializers.Serializer): - time = serializers.TimeField() - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.time = attrs['time'] - return instance - return TimeObject(**attrs) - - -class TimeObjectCustomFormatSerializer(serializers.Serializer): - time = serializers.TimeField(format=("%H -- %M", "%H%M%S")) - - def restore_object(self, attrs, instance=None): - if instance is not None: - instance.time = attrs['time'] - return instance - return TimeObject(**attrs) - - class TimeFieldModel(models.Model): clock = models.TimeField() @@ -136,277 +59,351 @@ class BasicFieldTests(TestCase): PK fields other than AutoField fields should not be read_only by default. """ serializer = CharPrimaryKeyModelSerializer() - self.assertEqual(serializer.fields['id'].read_only, False) + self.assertEquals(serializer.fields['id'].read_only, False) class DateFieldTest(TestCase): - def test_valid_default_date_input_formats(self): - serializer = DateObjectSerializer(data={'date': '1984-07-31'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the DateFieldTest from_native() and to_native() behavior + """ - serializer = DateObjectSerializer(data={'date': '07/31/1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '07/31/84'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'Jul 31 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'Jul 31, 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 Jul 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'July 31 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': 'July 31, 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 July 1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectSerializer(data={'date': '31 July, 1984'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_date_input_formats(self): - serializer = DateObjectCustomFormatSerializer(data={'date': '1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateObjectCustomFormatSerializer(data={'date': '1984 -- 07'}) - self.assertTrue(serializer.is_valid()) - - def test_wrong_default_date_input_format(self): - serializer = DateObjectSerializer(data={'date': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - '[January through December] DD, YYYY; DD [January through December] YYYY; ' - 'DD [January through December], YYYY']}) - - def test_wrong_custom_date_input_format(self): - serializer = DateObjectCustomFormatSerializer(data={'date': '07/31/1984'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date': ['Date has wrong format. Use one of these formats instead: YYYY; YYYY -- MM']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.DateField() - result = f.from_native('1984-07-31') + result_1 = f.from_native('1984-07-31') - self.assertEqual(datetime.date(1984, 7, 31), result) + self.assertEqual(datetime.date(1984, 7, 31), result_1) def test_from_native_datetime_date(self): """ Make sure from_native() accepts a datetime.date instance. """ f = serializers.DateField() - result = f.from_native(datetime.date(1984, 7, 31)) + result_1 = f.from_native(datetime.date(1984, 7, 31)) - self.assertEqual(result, datetime.date(1984, 7, 31)) + self.assertEqual(result_1, datetime.date(1984, 7, 31)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.DateField(input_formats=['%Y -- %d']) + result = f.from_native('1984 -- 31') + + self.assertEqual(datetime.date(1984, 1, 31), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.DateField(input_formats=['%Y -- %d']) + + try: + f.from_native('1984-07-31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.DateField() result = f.from_native('') self.assertEqual(result, None) def test_from_native_invalid_date(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid date. + """ f = serializers.DateField() try: - f.from_native('1984-42-31') + f.from_native('1984-13-31') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Date has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD; MM/DD/YYYY; MM/DD/YY; [Jan through Dec] DD YYYY; ' - '[Jan through Dec] DD, YYYY; DD [Jan through Dec] YYYY; ' - 'DD [Jan through Dec], YYYY; [January through December] DD YYYY; ' - '[January through December] DD, YYYY; DD [January through December] YYYY; ' - 'DD [January through December], YYYY']) + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) else: self.fail("ValidationError was not properly raised") + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ + f = serializers.DateField() + + try: + f.from_native('1984 -- 31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) + else: + self.fail("ValidationError was not properly raised") + + def test_to_native(self): + """ + Make sure to_native() returns isoformat as default. + """ + f = serializers.DateField() + + result_1 = f.to_native(datetime.date(1984, 7, 31)) + + self.assertEqual('1984-07-31', result_1) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.DateField(output_format="%Y - %m.%d") + + result_1 = f.to_native(datetime.date(1984, 7, 31)) + + self.assertEqual('1984 - 07.31', result_1) + class DateTimeFieldTest(TestCase): - def test_valid_default_date_time_input_formats(self): - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the DateTimeField from_native() and to_native() behavior + """ - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84'}) - self.assertTrue(serializer.is_valid()) - - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_valid_default_date_time_input_formats_for_django_gte_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': '1984-07-31 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/1984 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectSerializer(data={'date_time': '07/31/84 04:31:59.123456'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_date_time_input_formats(self): - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984'}) - self.assertTrue(serializer.is_valid()) - - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '1984 04:31'}) - self.assertTrue(serializer.is_valid()) - - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_wrong_default_date_time_input_format_for_django_gte_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']}) - - @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_wrong_default_date_time_input_format_for_django_lt_1_4(self): - serializer = DateTimeObjectSerializer(data={'date_time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']}) - - def test_wrong_custom_date_time_input_format(self): - serializer = DateTimeObjectCustomFormatSerializer(data={'date_time': '07/31/84 04:31'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'date_time': ['Datetime has wrong format. Use one of these formats instead: YYYY; YYYY HH:MM']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.DateTimeField() - result = f.from_native('1984-07-31 04:31') + result_1 = f.from_native('1984-07-31') + result_2 = f.from_native('1984-07-31 04:31') + result_3 = f.from_native('1984-07-31 04:31:59') + result_4 = f.from_native('1984-07-31 04:31:59.000200') - self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result) + 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_from_native_datetime_datetime(self): """ - Make sure from_native() accepts a datetime.date instance. + Make sure from_native() accepts a datetime.datetime instance. """ f = serializers.DateTimeField() - result = f.from_native(datetime.datetime(1984, 7, 31)) + result_1 = f.from_native(datetime.datetime(1984, 7, 31)) + result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) + result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) + result_4 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) - self.assertEqual(result, datetime.datetime(1984, 7, 31)) + self.assertEqual(result_1, datetime.datetime(1984, 7, 31)) + self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31)) + self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59)) + self.assertEqual(result_4, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.DateTimeField(input_formats=['%Y -- %H:%M']) + result = f.from_native('1984 -- 04:59') + + self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.DateTimeField(input_formats=['%Y -- %H:%M']) + + try: + f.from_native('1984-07-31 04:31:59') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- HH:MM"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.DateTimeField() result = f.from_native('') self.assertEqual(result, None) - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_from_native_invalid_datetime_for_django_gte_1_4(self): + def test_from_native_invalid_datetime(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid datetime. + """ f = serializers.DateTimeField() try: - f.from_native('1984-42-31 04:31') + f.from_native('04:61:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM:SS.uuuuuu; YYYY-MM-DD HH:MM; ' - 'YYYY-MM-DD; MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM:SS.uuuuuu; ' - 'MM/DD/YYYY HH:MM; MM/DD/YYYY; MM/DD/YY HH:MM:SS; ' - 'MM/DD/YY HH:MM:SS.uuuuuu; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " + "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " + "YYYY-MM-DD HH:MM:SS.uuuuuu"]) else: self.fail("ValidationError was not properly raised") - @unittest.skipUnless(django.VERSION < (1, 4), "django >= 1.4 have microseconds in default settings") - def test_from_native_invalid_datetime_for_django_lt_1_4(self): + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ f = serializers.DateTimeField() try: - f.from_native('1984-42-31 04:31') + f.from_native('04 -- 31') except validators.ValidationError as e: - self.assertEqual(e.messages, ['Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DD HH:MM:SS; YYYY-MM-DD HH:MM; YYYY-MM-DD; ' - 'MM/DD/YYYY HH:MM:SS; MM/DD/YYYY HH:MM; MM/DD/YYYY; ' - 'MM/DD/YY HH:MM:SS; MM/DD/YY HH:MM; MM/DD/YY']) + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " + "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " + "YYYY-MM-DD HH:MM:SS.uuuuuu"]) else: self.fail("ValidationError was not properly raised") + def test_to_native(self): + """ + Make sure to_native() returns isoformat as default. + """ + f = serializers.DateTimeField() + + 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) + self.assertEqual('1984-07-31T04:31:59.000200', result_4) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.DateTimeField(output_format="%Y - %H:%M") + + 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 - 00:00', result_1) + self.assertEqual('1984 - 04:31', result_2) + self.assertEqual('1984 - 04:31', result_3) + self.assertEqual('1984 - 04:31', result_4) + class TimeFieldTest(TestCase): - def test_valid_default_time_input_formats(self): - serializer = TimeObjectSerializer(data={'time': '04:31'}) - self.assertTrue(serializer.is_valid()) + """ + Tests for the TimeField from_native() and to_native() behavior + """ - serializer = TimeObjectSerializer(data={'time': '04:31:59'}) - self.assertTrue(serializer.is_valid()) - - def test_valid_custom_time_input_formats(self): - serializer = TimeObjectCustomFormatSerializer(data={'time': '04 -- 31'}) - self.assertTrue(serializer.is_valid()) - - serializer = TimeObjectCustomFormatSerializer(data={'time': '043159'}) - self.assertTrue(serializer.is_valid()) - - def test_wrong_default_time_input_format(self): - serializer = TimeObjectSerializer(data={'time': 'something wrong'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM']}) - - def test_wrong_custom_time_input_format(self): - serializer = TimeObjectCustomFormatSerializer(data={'time': '04:31'}) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'time': ['Time has wrong format. Use one of these formats instead: HH -- MM; HHMMSS']}) - - def test_from_native(self): + def test_from_native_string(self): + """ + Make sure from_native() accepts default iso input formats. + """ f = serializers.TimeField() - result = f.from_native('12:34:56') + result_1 = f.from_native('04:31') + result_2 = f.from_native('04:31:59') + result_3 = f.from_native('04:31:59.000200') - self.assertEqual(datetime.time(12, 34, 56), result) + 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_from_native_datetime_time(self): """ Make sure from_native() accepts a datetime.time instance. """ f = serializers.TimeField() - result = f.from_native(datetime.time(12, 34, 56)) + result_1 = f.from_native(datetime.time(4, 31)) + result_2 = f.from_native(datetime.time(4, 31, 59)) + result_3 = f.from_native(datetime.time(4, 31, 59, 200)) - self.assertEqual(result, datetime.time(12, 34, 56)) + self.assertEqual(result_1, datetime.time(4, 31)) + self.assertEqual(result_2, datetime.time(4, 31, 59)) + self.assertEqual(result_3, datetime.time(4, 31, 59, 200)) + + def test_from_native_custom_format(self): + """ + Make sure from_native() accepts custom input formats. + """ + f = serializers.TimeField(input_formats=['%H -- %M']) + result = f.from_native('04 -- 31') + + self.assertEqual(datetime.time(4, 31), result) + + def test_from_native_invalid_default_on_custom_format(self): + """ + Make sure from_native() don't accept default formats if custom format is preset + """ + f = serializers.TimeField(input_formats=['%H -- %M']) + + try: + f.from_native('04:31:59') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH -- MM"]) + else: + self.fail("ValidationError was not properly raised") def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ f = serializers.TimeField() result = f.from_native('') self.assertEqual(result, None) def test_from_native_invalid_time(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid time. + """ f = serializers.TimeField() try: - f.from_native('12:69:12') + f.from_native('04:61:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH:MM:SS; HH:MM"]) + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " + "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) else: self.fail("ValidationError was not properly raised") + + def test_from_native_invalid_format(self): + """ + Make sure from_native() raises a ValidationError on passing an invalid format. + """ + f = serializers.TimeField() + + try: + f.from_native('04 -- 31') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " + "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + else: + self.fail("ValidationError was not properly raised") + + def test_to_native(self): + """ + Make sure to_native() returns isoformat 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('04:31:00', result_1) + self.assertEqual('04:31:59', result_2) + self.assertEqual('04:31:59.000200', result_3) + + def test_to_native_custom_format(self): + """ + Make sure to_native() returns correct custom format. + """ + f = serializers.TimeField(output_format="%H - %S [%f]") + 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 - 00 [000000]', result_1) + self.assertEqual('04 - 59 [000000]', result_2) + self.assertEqual('04 - 59 [000200]', result_3) \ No newline at end of file diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 8c13947c9..fe92e0bcf 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -65,8 +65,8 @@ class IntegrationTestFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - for obj in self.objects.all() + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + for obj in self.objects.all() ] @unittest.skipUnless(django_filters, 'django-filters not installed') @@ -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 f['date'] == search_date] + expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').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 f['date'] > search_date] + expected_data = [f for f in self.data if datetime.datetime.strptime(f['date'], '%Y-%m-%d').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,9 @@ 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 f['date'] > search_date and - f['decimal'] < search_decimal] + expected_data = [f for f in self.data if + datetime.datetime.strptime(f['date'], '%Y-%m-%d').date() > search_date and + f['decimal'] < search_decimal] self.assertEqual(response.data, expected_data) @unittest.skipUnless(django_filters, 'django-filters not installed') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 6b9970a6c..472ffcddd 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -112,8 +112,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.objects = FilterableItem.objects self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - for obj in self.objects.all() + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} + for obj in self.objects.all() ] self.view = FilterFieldsRootView.as_view() diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index d0300f9ec..510650173 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': datetime.datetime(2012, 1, 1), + 'created': '2012-01-01T00:00:00', 'sub_comment': 'And Merry Christmas!' } self.person_data = {'name': 'dwight', 'age': 35} From a9d36d4726fc8eea02184b089ee6ed1d02e4c75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 15:03:27 +0100 Subject: [PATCH 12/20] Add docs update - part 1 --- docs/api-guide/fields.md | 24 ++++++++++++++++++------ docs/api-guide/settings.md | 35 +++++++++++++++++++++++++++++++++++ docs/topics/release-notes.md | 4 +--- rest_framework/fields.py | 18 +++++++++--------- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index abacc1b82..c1f3c051e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -185,18 +185,20 @@ Corresponds to `django.forms.fields.RegexField` A date representation. -Uses `DATE_INPUT_FORMATS` to validate date. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.DateField` +**Signature:** `DateField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `DATE_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `DATE_OUTPUT_FORMAT` + ## DateTimeField A date and time representation. -Uses `DATETIME_INPUT_FORMATS` to validate date_time. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.DateTimeField` @@ -211,16 +213,26 @@ If you want to override this behavior, you'll need to declare the `DateTimeField class Meta: model = Comment +**Signature:** `DateTimeField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `DATETIME_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `DATETIME_OUTPUT_FORMAT` + ## TimeField A time representation. -Uses `TIME_INPUT_FORMATS` to validate time. - Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.TimeField` +**Signature:** `TimeField(input_formats=None, output_format=False)` + + - `input_formats` designates which input formats are supported. This will override the `TIME_INPUT_FORMATS` + + - `output_format` designates which output format will be used. This will override the `TIME_OUTPUT_FORMAT` + ## IntegerField An integer representation. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index e103fbab4..9080cacb0 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -174,4 +174,39 @@ The name of a parameter in the URL conf that may be used to provide a format suf Default: `'format'` +## DATE_INPUT_FORMATS + +Default: + + ( + '%Y-%m-%d', # '1984-07-31' + ) + +## DATE_OUTPUT_FORMAT + +## DATETIME_INPUT_FORMATS + +Default: + + ( + '%Y-%m-%d', # '1984-07-31' + '%Y-%m-%d %H:%M', # '1984-07-31 04:31' + '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' + '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ) + +## DATETIME_OUTPUT_FORMAT + +## TIME_INPUT_FORMATS + +Default: + + ( + '%H:%M', # '04:31' + '%H:%M:%S', # '04:31:59' + '%H:%M:%S.%f', # '04:31:59.000200' + ) + +## TIME_OUTPUT_FORMAT + [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f60382acf..6b9e4e219 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -45,9 +45,7 @@ You can determine your currently installed version using `pip freeze`: * Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view. * Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfixes for model field validation edge-cases. -* Support `DATE_INPUT_FORMATS` for `DateField` validation -* Support `DATETIME_INPUT_FORMATS` for `DateTimeField` validation -* Support `TIME_INPUT_FORMATS` for `TimeField` validation +* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField` ### 2.2.1 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb77164a7..3eaa532ac 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -452,9 +452,9 @@ class DateField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.DATE_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.DATE_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.DATE_INPUT_FORMATS + self.output_format = output_format or api_settings.DATE_OUTPUT_FORMAT super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -499,9 +499,9 @@ class DateTimeField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.DATETIME_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.DATETIME_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.DATETIME_INPUT_FORMATS + self.output_format = output_format or api_settings.DATETIME_OUTPUT_FORMAT super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -552,9 +552,9 @@ class TimeField(WritableField): } empty = None - def __init__(self, *args, **kwargs): - self.input_formats = kwargs.pop('input_formats', api_settings.TIME_INPUT_FORMATS) - self.output_format = kwargs.pop('output_format', api_settings.TIME_OUTPUT_FORMAT) + def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + self.input_formats = input_formats or api_settings.TIME_INPUT_FORMATS + self.output_format = output_format or api_settings.TIME_OUTPUT_FORMAT super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): From 9c964cf37b6936e907d250306e15c3f116591e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 16:24:25 +0100 Subject: [PATCH 13/20] Add new ISO8601 setting + integration --- rest_framework/__init__.py | 3 ++ rest_framework/fields.py | 83 +++++++++++++++++++++++----------- rest_framework/settings.py | 20 ++++---- rest_framework/tests/fields.py | 38 +++++++--------- 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 29f3d7bc0..d26bb6bf8 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -4,3 +4,6 @@ VERSION = __version__ # synonym # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' + +# Default input and output format +ISO8601 = 'iso-8601' \ No newline at end of file diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3eaa532ac..c3e83c5e9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,9 +11,11 @@ from django.core.exceptions import ValidationError from django.conf import settings from django import forms from django.forms import widgets +from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ +from rest_framework import ISO8601 from rest_framework.compat import timezone from rest_framework.compat import BytesIO from rest_framework.compat import six @@ -472,21 +474,30 @@ class DateField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_date(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed.date() + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.date() - date_input_formats = '; '.join(self.input_formats) + date_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD') msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class DateTimeField(WritableField): @@ -525,21 +536,30 @@ class DateTimeField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_datetime(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed - datetime_input_formats = '; '.join(self.input_formats) + datetime_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]') msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class TimeField(WritableField): @@ -565,21 +585,30 @@ class TimeField(WritableField): return value for format in self.input_formats: - try: - parsed = datetime.datetime.strptime(value, format) - except (ValueError, TypeError): - pass + if format.lower() == ISO8601: + try: + parsed = parse_time(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed else: - return parsed.time() + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.time() - time_input_formats = '; '.join(self.input_formats) + time_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'HH:MM[:ss[.uuuuuu]]') msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format is not None: - return value.strftime(self.output_format) - return value.isoformat() + if self.output_format.lower() == ISO8601: + return value.isoformat() + return value.strftime(self.output_format) class IntegerField(WritableField): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 717496ea5..02d751e19 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,8 +18,11 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals + from django.conf import settings from django.utils import importlib + +from rest_framework import ISO8601 from rest_framework.compat import six @@ -79,24 +82,19 @@ DEFAULTS = { # Input and output formats 'DATE_INPUT_FORMATS': ( - '%Y-%m-%d', # '1984-07-31' + ISO8601, ), - 'DATE_OUTPUT_FORMAT': None, + 'DATE_OUTPUT_FORMAT': ISO8601, 'DATETIME_INPUT_FORMATS': ( - '%Y-%m-%d', # '1984-07-31' - '%Y-%m-%d %H:%M', # '1984-07-31 04:31' - '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' - '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' + ISO8601, ), - 'DATETIME_OUTPUT_FORMAT': None, + 'DATETIME_OUTPUT_FORMAT': ISO8601, 'TIME_INPUT_FORMATS': ( - '%H:%M', # '04:31' - '%H:%M:%S', # '04:31:59' - '%H:%M:%S.%f', # '04:31:59.000200' + ISO8601, ), - 'TIME_OUTPUT_FORMAT': None, + 'TIME_OUTPUT_FORMAT': ISO8601, } diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 6630e0b21..3b84ab1c5 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -173,30 +173,26 @@ class DateTimeFieldTest(TestCase): Make sure from_native() accepts default iso input formats. """ f = serializers.DateTimeField() - result_1 = f.from_native('1984-07-31') - result_2 = f.from_native('1984-07-31 04:31') - result_3 = f.from_native('1984-07-31 04:31:59') - result_4 = f.from_native('1984-07-31 04:31:59.000200') + result_1 = f.from_native('1984-07-31 04:31') + result_2 = f.from_native('1984-07-31 04:31:59') + result_3 = f.from_native('1984-07-31 04:31:59.000200') - 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) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2) + self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3) def test_from_native_datetime_datetime(self): """ Make sure from_native() accepts a datetime.datetime instance. """ f = serializers.DateTimeField() - result_1 = f.from_native(datetime.datetime(1984, 7, 31)) - result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) - result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) - result_4 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31)) + result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59)) + result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) - self.assertEqual(result_1, datetime.datetime(1984, 7, 31)) - self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31)) - self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59)) - self.assertEqual(result_4, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) + self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31)) + self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59)) + self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200)) def test_from_native_custom_format(self): """ @@ -239,8 +235,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " - "YYYY-MM-DD HH:MM:SS.uuuuuu"]) + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) else: self.fail("ValidationError was not properly raised") @@ -254,8 +249,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD; YYYY-MM-DD HH:MM; YYYY-MM-DD HH:MM:SS; " - "YYYY-MM-DD HH:MM:SS.uuuuuu"]) + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) else: self.fail("ValidationError was not properly raised") @@ -364,7 +358,7 @@ class TimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + "HH:MM[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") @@ -378,7 +372,7 @@ class TimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM; HH:MM:SS; HH:MM:SS.uuuuuu"]) + "HH:MM[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") From 4a2788a7be6541811977fbe7ae31af41bf9545a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 16:35:27 +0100 Subject: [PATCH 14/20] Update docs --- docs/api-guide/settings.md | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 9080cacb0..04569f44e 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -176,37 +176,26 @@ Default: `'format'` ## DATE_INPUT_FORMATS -Default: - - ( - '%Y-%m-%d', # '1984-07-31' - ) +Default: `ISO8601` ## DATE_OUTPUT_FORMAT +Default: `ISO8601` + ## DATETIME_INPUT_FORMATS -Default: - - ( - '%Y-%m-%d', # '1984-07-31' - '%Y-%m-%d %H:%M', # '1984-07-31 04:31' - '%Y-%m-%d %H:%M:%S', # '1984-07-31 04:31:59' - '%Y-%m-%d %H:%M:%S.%f', # '1984-07-31 04:31:59.000200' - ) +Default: `ISO8601` ## DATETIME_OUTPUT_FORMAT +Default: `ISO8601` + ## TIME_INPUT_FORMATS -Default: - - ( - '%H:%M', # '04:31' - '%H:%M:%S', # '04:31:59' - '%H:%M:%S.%f', # '04:31:59.000200' - ) +Default: `ISO8601` ## TIME_OUTPUT_FORMAT +Default: `ISO8601` + [cite]: http://www.python.org/dev/peps/pep-0020/ From 12905449a5591cef2b2fc94d68cc273dc6df0463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 16:59:47 +0100 Subject: [PATCH 15/20] Add format class attributes --- rest_framework/fields.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c3e83c5e9..21ab92ed0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -453,10 +453,12 @@ class DateField(WritableField): 'invalid': _("Date has wrong format. Use one of these formats instead: %s"), } empty = None + input_formats = api_settings.DATE_INPUT_FORMATS + output_format = api_settings.DATE_OUTPUT_FORMAT def __init__(self, input_formats=None, output_format=None, *args, **kwargs): - self.input_formats = input_formats or api_settings.DATE_INPUT_FORMATS - self.output_format = output_format or api_settings.DATE_OUTPUT_FORMAT + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.output_format = output_format if output_format is not None else self.output_format super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -509,10 +511,12 @@ class DateTimeField(WritableField): 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), } empty = None + input_formats = api_settings.DATETIME_INPUT_FORMATS + output_format = api_settings.DATETIME_OUTPUT_FORMAT def __init__(self, input_formats=None, output_format=None, *args, **kwargs): - self.input_formats = input_formats or api_settings.DATETIME_INPUT_FORMATS - self.output_format = output_format or api_settings.DATETIME_OUTPUT_FORMAT + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.output_format = output_format if output_format is not None else self.output_format super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -571,10 +575,12 @@ class TimeField(WritableField): 'invalid': _("Time has wrong format. Use one of these formats instead: %s"), } empty = None + input_formats = api_settings.TIME_INPUT_FORMATS + output_format = api_settings.TIME_OUTPUT_FORMAT def __init__(self, input_formats=None, output_format=None, *args, **kwargs): - self.input_formats = input_formats or api_settings.TIME_INPUT_FORMATS - self.output_format = output_format or api_settings.TIME_OUTPUT_FORMAT + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.output_format = output_format if output_format is not None else self.output_format super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): From 681ad6f5370c88c15ccde3ecd61e735cde514e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 17:04:30 +0100 Subject: [PATCH 16/20] Add none testcases to date, datetime, time --- rest_framework/tests/fields.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 3b84ab1c5..17ef2f2c6 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -116,6 +116,15 @@ class DateFieldTest(TestCase): self.assertEqual(result, None) + def test_from_native_none(self): + """ + Make sure from_native() returns None on None param. + """ + f = serializers.DateField() + result = f.from_native(None) + + self.assertEqual(result, None) + def test_from_native_invalid_date(self): """ Make sure from_native() raises a ValidationError on passing an invalid date. @@ -225,6 +234,15 @@ class DateTimeFieldTest(TestCase): self.assertEqual(result, None) + def test_from_native_none(self): + """ + Make sure from_native() returns None on None param. + """ + f = serializers.DateTimeField() + result = f.from_native(None) + + self.assertEqual(result, None) + def test_from_native_invalid_datetime(self): """ Make sure from_native() raises a ValidationError on passing an invalid datetime. @@ -348,6 +366,15 @@ class TimeFieldTest(TestCase): self.assertEqual(result, None) + def test_from_native_none(self): + """ + Make sure from_native() returns None on None param. + """ + f = serializers.TimeField() + result = f.from_native(None) + + self.assertEqual(result, None) + def test_from_native_invalid_time(self): """ Make sure from_native() raises a ValidationError on passing an invalid time. From 5e5cd6f7f7357d68d5c10500ec0379e49d679202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 1 Mar 2013 17:15:39 +0100 Subject: [PATCH 17/20] Fix for django 1.3 compatibility --- rest_framework/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 21ab92ed0..87e0161f5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,12 +11,11 @@ from django.core.exceptions import ValidationError from django.conf import settings from django import forms from django.forms import widgets -from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO8601 -from rest_framework.compat import timezone +from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text From 4f7b028a0a983b79ebca63b2ba48ce97e7a06175 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 5 Mar 2013 20:57:35 +0000 Subject: [PATCH 18/20] Updating docs --- docs/api-guide/fields.md | 45 +++++++++++++++++++------------------- docs/api-guide/settings.md | 36 ++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index c1f3c051e..68e619a01 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -181,26 +181,10 @@ Corresponds to `django.forms.fields.RegexField` **Signature:** `RegexField(regex, max_length=None, min_length=None)` -## DateField - -A date representation. - -Optionally takes `format` as parameter to replace the matching pattern. - -Corresponds to `django.db.models.fields.DateField` - -**Signature:** `DateField(input_formats=None, output_format=False)` - - - `input_formats` designates which input formats are supported. This will override the `DATE_INPUT_FORMATS` - - - `output_format` designates which output format will be used. This will override the `DATE_OUTPUT_FORMAT` - ## DateTimeField A date and time representation. -Optionally takes `format` as parameter to replace the matching pattern. - Corresponds to `django.db.models.fields.DateTimeField` When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default. @@ -213,11 +197,25 @@ If you want to override this behavior, you'll need to declare the `DateTimeField class Meta: model = Comment -**Signature:** `DateTimeField(input_formats=None, output_format=False)` +**Signature:** `DateTimeField(format=None, input_formats=None)` - - `input_formats` designates which input formats are supported. This will override the `DATETIME_INPUT_FORMATS` +* `format` - A string representing the output format. If not specified, the `DATETIME_FORMAT` setting will be used, which defaults to `'iso8601'`. +* `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 `['iso8601']`. - - `output_format` designates which output format will be used. This will override the `DATETIME_OUTPUT_FORMAT` +DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'is8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000'`) + +## DateField + +A date representation. + +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 `'iso8601'`. +* `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 `['iso8601']`. + +Date format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'is8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`) ## TimeField @@ -227,11 +225,12 @@ Optionally takes `format` as parameter to replace the matching pattern. Corresponds to `django.db.models.fields.TimeField` -**Signature:** `TimeField(input_formats=None, output_format=False)` +**Signature:** `TimeField(format=None, input_formats=None)` - - `input_formats` designates which input formats are supported. This will override the `TIME_INPUT_FORMATS` +* `format` - A string representing the output format. If not specified, the `TIME_FORMAT` setting will be used, which defaults to `'iso8601'`. +* `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 `['iso8601']`. - - `output_format` designates which output format will be used. This will override the `TIME_OUTPUT_FORMAT` +Time format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'is8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`) ## IntegerField @@ -276,3 +275,5 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS +[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior +[iso8601]: http://www.w3.org/TR/NOTE-datetime diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 04569f44e..827302259 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -174,28 +174,40 @@ The name of a parameter in the URL conf that may be used to provide a format suf Default: `'format'` -## DATE_INPUT_FORMATS +## DATETIME_FORMAT -Default: `ISO8601` +A format string that should be used by default for `DateTimeField` serializer fields. -## DATE_OUTPUT_FORMAT - -Default: `ISO8601` +Default: `'iso8601'` ## DATETIME_INPUT_FORMATS -Default: `ISO8601` +A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields. -## DATETIME_OUTPUT_FORMAT +Default: `['iso8601']` -Default: `ISO8601` +## DATE_FORMAT + +A format string that should be used by default for `DateField` serializer fields. + +Default: `'iso8601'` + +## DATE_INPUT_FORMATS + +A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields. + +Default: `['iso8601']` + +## TIME_FORMAT + +A format string that should be used by default for `TimeField` serializer fields. + +Default: `'iso8601'` ## TIME_INPUT_FORMATS -Default: `ISO8601` +A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields. -## TIME_OUTPUT_FORMAT - -Default: `ISO8601` +Default: `['iso8601']` [cite]: http://www.python.org/dev/peps/pep-0020/ From 1106596c80218569a56ff5ea04d759e3d0c541dd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Mar 2013 12:19:39 +0000 Subject: [PATCH 19/20] Clean ups to datetime formatting --- docs/api-guide/fields.md | 20 ++--- docs/api-guide/settings.md | 142 ++++++++++++++++++++------------- docs/topics/release-notes.md | 2 +- rest_framework/__init__.py | 2 +- rest_framework/fields.py | 88 ++++++++++++++------ rest_framework/settings.py | 14 ++-- rest_framework/tests/fields.py | 26 +++--- rest_framework/utils/dates.py | 14 ---- tox.ini | 2 +- 9 files changed, 182 insertions(+), 128 deletions(-) delete mode 100644 rest_framework/utils/dates.py diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 68e619a01..9a745cf19 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -2,7 +2,7 @@ # Serializer fields -> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it -- normalizing it to a consistent format. +> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. > > — [Django documentation][cite] @@ -199,10 +199,10 @@ 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 `'iso8601'`. -* `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 `['iso8601']`. +* `format` - A string representing the output format. If not specified, the `DATETIME_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `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 `'is8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000'`) +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'`) ## DateField @@ -212,10 +212,10 @@ 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 `'iso8601'`. -* `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 `['iso8601']`. +* `format` - A string representing the output format. If not specified, the `DATE_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `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 `'is8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`) +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'`) ## TimeField @@ -227,10 +227,10 @@ 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 `'iso8601'`. -* `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 `['iso8601']`. +* `format` - A string representing the output format. If not specified, the `TIME_FORMAT` setting will be used, which defaults to `'iso-8601'`. +* `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 `'is8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`) +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'`) ## IntegerField diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 827302259..116386969 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -34,7 +34,11 @@ The `api_settings` object will check for any user-defined settings, and otherwis # API Reference -## DEFAULT_RENDERER_CLASSES +## API policy settings + +*The following settings control the basic API policies, and are applied to every `APIView` class based view, or `@api_view` function based view.* + +#### DEFAULT_RENDERER_CLASSES A list or tuple of renderer classes, that determines the default set of renderers that may be used when returning a `Response` object. @@ -45,7 +49,7 @@ Default: 'rest_framework.renderers.BrowsableAPIRenderer', ) -## DEFAULT_PARSER_CLASSES +#### DEFAULT_PARSER_CLASSES A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property. @@ -57,7 +61,7 @@ Default: 'rest_framework.parsers.MultiPartParser' ) -## DEFAULT_AUTHENTICATION_CLASSES +#### DEFAULT_AUTHENTICATION_CLASSES A list or tuple of authentication classes, that determines the default set of authenticators used when accessing the `request.user` or `request.auth` properties. @@ -68,7 +72,7 @@ Default: 'rest_framework.authentication.BasicAuthentication' ) -## DEFAULT_PERMISSION_CLASSES +#### DEFAULT_PERMISSION_CLASSES A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. @@ -78,59 +82,77 @@ Default: 'rest_framework.permissions.AllowAny', ) -## DEFAULT_THROTTLE_CLASSES +#### DEFAULT_THROTTLE_CLASSES A list or tuple of throttle classes, that determines the default set of throttles checked at the start of a view. Default: `()` -## DEFAULT_CONTENT_NEGOTIATION_CLASS +#### DEFAULT_CONTENT_NEGOTIATION_CLASS A content negotiation class, that determines how a renderer is selected for the response, given an incoming request. Default: `'rest_framework.negotiation.DefaultContentNegotiation'` -## DEFAULT_MODEL_SERIALIZER_CLASS +--- + +## Generic view settings + +*The following settings control the behavior of the generic class based views.* + +#### DEFAULT_MODEL_SERIALIZER_CLASS A class that determines the default type of model serializer that should be used by a generic view if `model` is specified, but `serializer_class` is not provided. Default: `'rest_framework.serializers.ModelSerializer'` -## DEFAULT_PAGINATION_SERIALIZER_CLASS +#### DEFAULT_PAGINATION_SERIALIZER_CLASS A class the determines the default serialization style for paginated responses. Default: `rest_framework.pagination.PaginationSerializer` -## FILTER_BACKEND +#### FILTER_BACKEND The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled. -## PAGINATE_BY +#### PAGINATE_BY The default page size to use for pagination. If set to `None`, pagination is disabled by default. Default: `None` -## PAGINATE_BY_PARAM +#### PAGINATE_BY_PARAM The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size. Default: `None` -## UNAUTHENTICATED_USER +--- + +## Authentication settings + +*The following settings control the behavior of unauthenticated requests.* + +#### UNAUTHENTICATED_USER The class that should be used to initialize `request.user` for unauthenticated requests. Default: `django.contrib.auth.models.AnonymousUser` -## UNAUTHENTICATED_TOKEN +#### UNAUTHENTICATED_TOKEN The class that should be used to initialize `request.auth` for unauthenticated requests. Default: `None` -## FORM_METHOD_OVERRIDE +--- + +## Browser overrides + +*The following settings provide URL or form-based overrides of the default browser behavior.* + +#### FORM_METHOD_OVERRIDE The name of a form field that may be used to override the HTTP method of the form. @@ -138,7 +160,7 @@ If the value of this setting is `None` then form method overloading will be disa Default: `'_method'` -## FORM_CONTENT_OVERRIDE +#### FORM_CONTENT_OVERRIDE The name of a form field that may be used to override the content of the form payload. Must be used together with `FORM_CONTENTTYPE_OVERRIDE`. @@ -146,7 +168,7 @@ If either setting is `None` then form content overloading will be disabled. Default: `'_content'` -## FORM_CONTENTTYPE_OVERRIDE +#### FORM_CONTENTTYPE_OVERRIDE The name of a form field that may be used to override the content type of the form payload. Must be used together with `FORM_CONTENT_OVERRIDE`. @@ -154,7 +176,7 @@ If either setting is `None` then form content overloading will be disabled. Default: `'_content_type'` -## URL_ACCEPT_OVERRIDE +#### URL_ACCEPT_OVERRIDE The name of a URL parameter that may be used to override the HTTP `Accept` header. @@ -162,52 +184,62 @@ If the value of this setting is `None` then URL accept overloading will be disab Default: `'accept'` -## URL_FORMAT_OVERRIDE +#### URL_FORMAT_OVERRIDE The name of a URL parameter that may be used to override the default `Accept` header based content negotiation. Default: `'format'` -## FORMAT_SUFFIX_KWARG +--- + +## Date/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. + +Default: `'iso-8601'` + +#### DATETIME_INPUT_FORMATS + +A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields. + +Default: `['iso-8601']` + +#### DATE_FORMAT + +A format string that should be used by default for rendering the output of `DateField` serializer fields. + +Default: `'iso-8601'` + +#### DATE_INPUT_FORMATS + +A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields. + +Default: `['iso-8601']` + +#### TIME_FORMAT + +A format string that should be used by default for rendering the output of `TimeField` serializer fields. + +Default: `'iso-8601'` + +#### TIME_INPUT_FORMATS + +A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields. + +Default: `['iso-8601']` + +--- + +## Miscellaneous settings + +#### FORMAT_SUFFIX_KWARG The name of a parameter in the URL conf that may be used to provide a format suffix. Default: `'format'` -## DATETIME_FORMAT - -A format string that should be used by default for `DateTimeField` serializer fields. - -Default: `'iso8601'` - -## DATETIME_INPUT_FORMATS - -A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields. - -Default: `['iso8601']` - -## DATE_FORMAT - -A format string that should be used by default for `DateField` serializer fields. - -Default: `'iso8601'` - -## DATE_INPUT_FORMATS - -A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields. - -Default: `['iso8601']` - -## TIME_FORMAT - -A format string that should be used by default for `TimeField` serializer fields. - -Default: `'iso8601'` - -## TIME_INPUT_FORMATS - -A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields. - -Default: `['iso8601']` - [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 81ed0c847..df66cb35e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,7 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master -* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField` +* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField`. * Cleanup: Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view. * Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfixes for model field validation edge-cases. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index d26bb6bf8..14ab7ff02 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -6,4 +6,4 @@ VERSION = __version__ # synonym HTTP_HEADER_ENCODING = 'iso-8859-1' # Default input and output format -ISO8601 = 'iso-8601' \ No newline at end of file +ISO_8601 = 'iso-8601' diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e65f0307a..fe555ee51 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -14,13 +14,12 @@ from django.forms import widgets from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ -from rest_framework import ISO8601 +from rest_framework import ISO_8601 from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.settings import api_settings -from rest_framework.utils.dates import get_readable_date_format def is_simple_callable(obj): @@ -52,6 +51,46 @@ def get_component(obj, attr_name): return val +def readable_datetime_formats(formats): + format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') + return humanize_strptime(format) + + +def readable_date_formats(formats): + format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]') + return humanize_strptime(format) + + +def readable_time_formats(formats): + format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') + return humanize_strptime(format) + + +def humanize_strptime(format_string): + # Note that we're missing some of the locale specific mappings that + # don't really make sense. + mapping = { + "%Y": "YYYY", + "%y": "YY", + "%m": "MM", + "%b": "[Jan-Dec]", + "%B": "[January-December]", + "%d": "DD", + "%H": "hh", + "%I": "hh", # Requires '%p' to differentiate from '%H'. + "%M": "mm", + "%S": "ss", + "%f": "uuuuuu", + "%a": "[Mon-Sun]", + "%A": "[Monday-Sunday]", + "%p": "[AM|PM]", + "%z": "[+HHMM|-HHMM]" + } + for key, val in mapping.items(): + format_string = format_string.replace(key, val) + return format_string + + class Field(object): read_only = True creation_counter = 0 @@ -453,11 +492,11 @@ class DateField(WritableField): } empty = None input_formats = api_settings.DATE_INPUT_FORMATS - output_format = api_settings.DATE_OUTPUT_FORMAT + format = api_settings.DATE_FORMAT - def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + 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 - self.output_format = output_format if output_format is not None else self.output_format + self.format = format if format is not None else self.format super(DateField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -475,7 +514,7 @@ class DateField(WritableField): return value for format in self.input_formats: - if format.lower() == ISO8601: + if format.lower() == ISO_8601: try: parsed = parse_date(value) except (ValueError, TypeError): @@ -491,16 +530,15 @@ class DateField(WritableField): else: return parsed.date() - date_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD') - msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) + msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats) raise ValidationError(msg) def to_native(self, value): if isinstance(value, datetime.datetime): value = value.date() - if self.output_format.lower() == ISO8601: + if self.format.lower() == ISO_8601: return value.isoformat() - return value.strftime(self.output_format) + return value.strftime(self.format) class DateTimeField(WritableField): @@ -513,11 +551,11 @@ class DateTimeField(WritableField): } empty = None input_formats = api_settings.DATETIME_INPUT_FORMATS - output_format = api_settings.DATETIME_OUTPUT_FORMAT + format = api_settings.DATETIME_FORMAT - def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + 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 - self.output_format = output_format if output_format is not None else self.output_format + self.format = format if format is not None else self.format super(DateTimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -541,7 +579,7 @@ class DateTimeField(WritableField): return value for format in self.input_formats: - if format.lower() == ISO8601: + if format.lower() == ISO_8601: try: parsed = parse_datetime(value) except (ValueError, TypeError): @@ -557,14 +595,13 @@ class DateTimeField(WritableField): else: return parsed - datetime_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]') - msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) + msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats) raise ValidationError(msg) def to_native(self, value): - if self.output_format.lower() == ISO8601: + if self.format.lower() == ISO_8601: return value.isoformat() - return value.strftime(self.output_format) + return value.strftime(self.format) class TimeField(WritableField): @@ -577,11 +614,11 @@ class TimeField(WritableField): } empty = None input_formats = api_settings.TIME_INPUT_FORMATS - output_format = api_settings.TIME_OUTPUT_FORMAT + format = api_settings.TIME_FORMAT - def __init__(self, input_formats=None, output_format=None, *args, **kwargs): + 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 - self.output_format = output_format if output_format is not None else self.output_format + self.format = format if format is not None else self.format super(TimeField, self).__init__(*args, **kwargs) def from_native(self, value): @@ -592,7 +629,7 @@ class TimeField(WritableField): return value for format in self.input_formats: - if format.lower() == ISO8601: + if format.lower() == ISO_8601: try: parsed = parse_time(value) except (ValueError, TypeError): @@ -608,16 +645,15 @@ class TimeField(WritableField): else: return parsed.time() - time_input_formats = '; '.join(self.input_formats).replace(ISO8601, 'HH:MM[:ss[.uuuuuu]]') - msg = self.error_messages['invalid'] % get_readable_date_format(time_input_formats) + msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats) raise ValidationError(msg) def to_native(self, value): if isinstance(value, datetime.datetime): value = value.time() - if self.output_format.lower() == ISO8601: + if self.format.lower() == ISO_8601: return value.isoformat() - return value.strftime(self.output_format) + return value.strftime(self.format) class IntegerField(WritableField): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 02d751e19..eede0c5a0 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -22,7 +22,7 @@ from __future__ import unicode_literals from django.conf import settings from django.utils import importlib -from rest_framework import ISO8601 +from rest_framework import ISO_8601 from rest_framework.compat import six @@ -82,19 +82,19 @@ DEFAULTS = { # Input and output formats 'DATE_INPUT_FORMATS': ( - ISO8601, + ISO_8601, ), - 'DATE_OUTPUT_FORMAT': ISO8601, + 'DATE_FORMAT': ISO_8601, 'DATETIME_INPUT_FORMATS': ( - ISO8601, + ISO_8601, ), - 'DATETIME_OUTPUT_FORMAT': ISO8601, + 'DATETIME_FORMAT': ISO_8601, 'TIME_INPUT_FORMATS': ( - ISO8601, + ISO_8601, ), - 'TIME_OUTPUT_FORMAT': ISO8601, + 'TIME_FORMAT': ISO_8601, } diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 17ef2f2c6..28f18ed89 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -59,7 +59,7 @@ class BasicFieldTests(TestCase): PK fields other than AutoField fields should not be read_only by default. """ serializer = CharPrimaryKeyModelSerializer() - self.assertEquals(serializer.fields['id'].read_only, False) + self.assertEqual(serializer.fields['id'].read_only, False) class DateFieldTest(TestCase): @@ -134,7 +134,7 @@ class DateFieldTest(TestCase): try: f.from_native('1984-13-31') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"]) else: self.fail("ValidationError was not properly raised") @@ -147,7 +147,7 @@ class DateFieldTest(TestCase): try: f.from_native('1984 -- 31') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD"]) + self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"]) else: self.fail("ValidationError was not properly raised") @@ -165,7 +165,7 @@ class DateFieldTest(TestCase): """ Make sure to_native() returns correct custom format. """ - f = serializers.DateField(output_format="%Y - %m.%d") + f = serializers.DateField(format="%Y - %m.%d") result_1 = f.to_native(datetime.date(1984, 7, 31)) @@ -221,7 +221,7 @@ class DateTimeFieldTest(TestCase): try: f.from_native('1984-07-31 04:31:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- HH:MM"]) + self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"]) else: self.fail("ValidationError was not properly raised") @@ -253,7 +253,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) else: self.fail("ValidationError was not properly raised") @@ -267,7 +267,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) else: self.fail("ValidationError was not properly raised") @@ -291,7 +291,7 @@ class DateTimeFieldTest(TestCase): """ Make sure to_native() returns correct custom format. """ - f = serializers.DateTimeField(output_format="%Y - %H:%M") + f = serializers.DateTimeField(format="%Y - %H:%M") result_1 = f.to_native(datetime.datetime(1984, 7, 31)) result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31)) @@ -353,7 +353,7 @@ class TimeFieldTest(TestCase): try: f.from_native('04:31:59') except validators.ValidationError as e: - self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: HH -- MM"]) + self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"]) else: self.fail("ValidationError was not properly raised") @@ -385,7 +385,7 @@ class TimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM[:ss[.uuuuuu]]"]) + "hh:mm[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") @@ -399,7 +399,7 @@ class TimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: " - "HH:MM[:ss[.uuuuuu]]"]) + "hh:mm[:ss[.uuuuuu]]"]) else: self.fail("ValidationError was not properly raised") @@ -420,11 +420,11 @@ class TimeFieldTest(TestCase): """ Make sure to_native() returns correct custom format. """ - f = serializers.TimeField(output_format="%H - %S [%f]") + f = serializers.TimeField(format="%H - %S [%f]") 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 - 00 [000000]', result_1) self.assertEqual('04 - 59 [000000]', result_2) - self.assertEqual('04 - 59 [000200]', result_3) \ No newline at end of file + self.assertEqual('04 - 59 [000200]', result_3) diff --git a/rest_framework/utils/dates.py b/rest_framework/utils/dates.py deleted file mode 100644 index f094f72db..000000000 --- a/rest_framework/utils/dates.py +++ /dev/null @@ -1,14 +0,0 @@ -def get_readable_date_format(date_format): - mapping = [("%Y", "YYYY"), - ("%y", "YY"), - ("%m", "MM"), - ("%b", "[Jan through Dec]"), - ("%B", "[January through December]"), - ("%d", "DD"), - ("%H", "HH"), - ("%M", "MM"), - ("%S", "SS"), - ("%f", "uuuuuu")] - for k, v in mapping: - date_format = date_format.replace(k, v) - return date_format \ No newline at end of file diff --git a/tox.ini b/tox.ini index 58d308aca..1fa395738 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.7-django1.4,py2.7-django1.3,py2.6-django1.5,py2.6-django1.4,py2.6-django1.3 +envlist = py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 [testenv] commands = {envpython} rest_framework/runtests/runtests.py From d260f1ec15d5aa3085fa74118382bcf2fd752dca Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 6 Mar 2013 12:19:49 +0000 Subject: [PATCH 20/20] Add `tox` note. --- docs/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.md b/docs/index.md index b2c04735a..faad6afec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -133,6 +133,10 @@ Run the tests: ./rest_framework/runtests/runtests.py +To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`: + + tox + ## Support For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. @@ -218,6 +222,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [release-notes]: topics/release-notes.md [credits]: topics/credits.md +[tox]: http://testrun.org/tox/latest/ + [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [stack-overflow]: http://stackoverflow.com/ [django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework