aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorStephan Groß2013-02-26 11:09:54 +0100
committerStephan Groß2013-03-01 16:48:20 +0100
commit9157db5da0b5601793d1a9f24e9cb10670a82be2 (patch)
treed0c323884c4b5efd05fc40893b25bb18c6cc572d /rest_framework
parent282af6057f30b5af4665d687200ee1ebf82fcf00 (diff)
downloaddjango-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.py87
-rw-r--r--rest_framework/tests/fields.py343
-rw-r--r--rest_framework/utils/dates.py14
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