From a405f90403a845deeb2641aa6a9c8c6486553ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 30 Jan 2013 10:59:38 +0100 Subject: [PATCH] Update date / datetime validation due to github comments --- docs/api-guide/fields.md | 4 + rest_framework/fields.py | 30 ++--- rest_framework/tests/fields.py | 182 +++++++++++++++++++++++++++++ rest_framework/tests/serializer.py | 139 ---------------------- rest_framework/utils/dates.py | 14 +++ 5 files changed, 215 insertions(+), 154 deletions(-) create mode 100644 rest_framework/utils/dates.py diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 7be311b82..852277b07 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -189,6 +189,8 @@ Corresponds to `django.db.models.fields.DateField` Uses `DATE_INPUT_FORMATS` to validate date. +Optionally takes `format` as parameter to replace the matching pattern. + ## DateTimeField A date and time representation. @@ -197,6 +199,8 @@ Corresponds to `django.db.models.fields.DateTimeField` Uses `DATETIME_INPUT_FORMATS` to validate date_time. +Optionally takes `format` as parameter to replace the matching pattern. + 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. If you want to override this behavior, you'll need to declare the `DateTimeField` explicitly on the serializer. For example: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6d930338a..8b4367773 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -13,8 +13,8 @@ from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type, smart_unicode 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.utils.dates import get_readable_date_format def is_simple_callable(obj): @@ -429,6 +429,10 @@ class DateField(WritableField): } 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 @@ -443,7 +447,7 @@ class DateField(WritableField): if isinstance(value, datetime.date): return value - for format in settings.DATE_INPUT_FORMATS: + for format in self.format: try: parsed = datetime.datetime.strptime(value, format) except ValueError: @@ -451,12 +455,8 @@ class DateField(WritableField): else: return parsed.date() - formats = '; '.join(settings.DATE_INPUT_FORMATS) - mapping = [("%Y", "YYYY"), ("%y", "YY"), ("%m", "MM"), ("%b", "[Jan through Dec]"), - ("%B", "[January through December]"), ("%d", "DD"), ("%H", "HH"), ("%M", "MM"), ("%S", "SS")] - for k, v in mapping: - formats = formats.replace(k, v) - msg = self.error_messages['invalid'] % formats + date_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats) raise ValidationError(msg) @@ -470,6 +470,10 @@ class DateTimeField(WritableField): } 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 @@ -490,7 +494,7 @@ class DateTimeField(WritableField): value = timezone.make_aware(value, default_timezone) return value - for format in settings.DATETIME_INPUT_FORMATS: + for format in self.format: try: parsed = datetime.datetime.strptime(value, format) except ValueError: @@ -498,12 +502,8 @@ class DateTimeField(WritableField): else: return parsed - formats = '; '.join(settings.DATETIME_INPUT_FORMATS) - mapping = [("%Y", "YYYY"), ("%y", "YY"), ("%m", "MM"), ("%d", "DD"), - ("%H", "HH"), ("%M", "MM"), ("%S", "SS"), ("%f", "uuuuuu")] - for k, v in mapping: - formats = formats.replace(k, v) - msg = self.error_messages['invalid'] % formats + datetime_input_formats = '; '.join(self.format) + msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats) raise ValidationError(msg) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 8068272d4..b2017c39f 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -2,8 +2,10 @@ General serializer field tests. """ +import django from django.db import models from django.test import TestCase +from django.utils import unittest from rest_framework import serializers @@ -16,6 +18,16 @@ 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 TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel @@ -26,6 +38,46 @@ 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 ReadOnlyFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ @@ -47,3 +99,133 @@ class ReadOnlyFieldTests(TestCase): """ serializer = CharPrimaryKeyModelSerializer() self.assertEquals(serializer.fields['id'].read_only, False) + + +class DateValidationTest(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']}) + + +class DateTimeValidationTest(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']}) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 6d05bc381..48b4f1ab9 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,8 +1,6 @@ import datetime import pickle -import django from django.test import TestCase -from django.utils import unittest from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, @@ -43,36 +41,6 @@ class CommentSerializer(serializers.Serializer): return instance -class DateObject(object): - def __init__(self, date): - self.date = date - - -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 DateTimeObject(object): - def __init__(self, date_time): - self.date_time = date_time - - -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 BookSerializer(serializers.ModelSerializer): isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'}) @@ -468,113 +436,6 @@ class RegexValidationTest(TestCase): self.assertTrue(serializer.is_valid()) -class DateValidationTest(TestCase): - def test_valid_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_wrong_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']}) - - -class DateTimeValidationTest(TestCase): - def test_valid_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_date_time_input_formats_2(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()) - - @unittest.skipUnless(django.VERSION >= (1, 4), "django < 1.4 don't have microseconds in default settings") - def test_wrong_date_time_input_format(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_date_time_input_format_2(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']}) - - class MetadataTests(TestCase): def test_empty(self): serializer = CommentSerializer() 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