diff options
| author | Stephan Groß | 2013-02-26 11:09:54 +0100 | 
|---|---|---|
| committer | Stephan Groß | 2013-03-01 16:48:20 +0100 | 
| commit | 9157db5da0b5601793d1a9f24e9cb10670a82be2 (patch) | |
| tree | d0c323884c4b5efd05fc40893b25bb18c6cc572d /rest_framework | |
| parent | 282af6057f30b5af4665d687200ee1ebf82fcf00 (diff) | |
| download | django-rest-framework-9157db5da0b5601793d1a9f24e9cb10670a82be2.tar.bz2 | |
Add better date / datetime validation (pull 2)
addition to #631 with update to master + timefield support
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/fields.py | 87 | ||||
| -rw-r--r-- | rest_framework/tests/fields.py | 343 | ||||
| -rw-r--r-- | rest_framework/utils/dates.py | 14 | 
3 files changed, 388 insertions, 56 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 86c3a837..2260c430 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 840ed320..37517642 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 00000000..f094f72d --- /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  | 
