Update date / datetime validation due to github comments

This commit is contained in:
Stephan Groß 2013-01-30 10:59:38 +01:00
parent 57fcbda730
commit a405f90403
5 changed files with 215 additions and 154 deletions

View File

@ -189,6 +189,8 @@ Corresponds to `django.db.models.fields.DateField`
Uses `DATE_INPUT_FORMATS` to validate date. Uses `DATE_INPUT_FORMATS` to validate date.
Optionally takes `format` as parameter to replace the matching pattern.
## DateTimeField ## DateTimeField
A date and time representation. A date and time representation.
@ -197,6 +199,8 @@ Corresponds to `django.db.models.fields.DateTimeField`
Uses `DATETIME_INPUT_FORMATS` to validate date_time. 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. 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: If you want to override this behavior, you'll need to declare the `DateTimeField` explicitly on the serializer. For example:

View File

@ -13,8 +13,8 @@ from django import forms
from django.forms import widgets from django.forms import widgets
from django.utils.encoding import is_protected_type, smart_unicode from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _ 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 timezone
from rest_framework.utils.dates import get_readable_date_format
def is_simple_callable(obj): def is_simple_callable(obj):
@ -429,6 +429,10 @@ class DateField(WritableField):
} }
empty = None 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): def from_native(self, value):
if value in validators.EMPTY_VALUES: if value in validators.EMPTY_VALUES:
return None return None
@ -443,7 +447,7 @@ class DateField(WritableField):
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
return value return value
for format in settings.DATE_INPUT_FORMATS: for format in self.format:
try: try:
parsed = datetime.datetime.strptime(value, format) parsed = datetime.datetime.strptime(value, format)
except ValueError: except ValueError:
@ -451,12 +455,8 @@ class DateField(WritableField):
else: else:
return parsed.date() return parsed.date()
formats = '; '.join(settings.DATE_INPUT_FORMATS) date_input_formats = '; '.join(self.format)
mapping = [("%Y", "YYYY"), ("%y", "YY"), ("%m", "MM"), ("%b", "[Jan through Dec]"), msg = self.error_messages['invalid'] % get_readable_date_format(date_input_formats)
("%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
raise ValidationError(msg) raise ValidationError(msg)
@ -470,6 +470,10 @@ class DateTimeField(WritableField):
} }
empty = None 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): def from_native(self, value):
if value in validators.EMPTY_VALUES: if value in validators.EMPTY_VALUES:
return None return None
@ -490,7 +494,7 @@ class DateTimeField(WritableField):
value = timezone.make_aware(value, default_timezone) value = timezone.make_aware(value, default_timezone)
return value return value
for format in settings.DATETIME_INPUT_FORMATS: for format in self.format:
try: try:
parsed = datetime.datetime.strptime(value, format) parsed = datetime.datetime.strptime(value, format)
except ValueError: except ValueError:
@ -498,12 +502,8 @@ class DateTimeField(WritableField):
else: else:
return parsed return parsed
formats = '; '.join(settings.DATETIME_INPUT_FORMATS) datetime_input_formats = '; '.join(self.format)
mapping = [("%Y", "YYYY"), ("%y", "YY"), ("%m", "MM"), ("%d", "DD"), msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats)
("%H", "HH"), ("%M", "MM"), ("%S", "SS"), ("%f", "uuuuuu")]
for k, v in mapping:
formats = formats.replace(k, v)
msg = self.error_messages['invalid'] % formats
raise ValidationError(msg) raise ValidationError(msg)

View File

@ -2,8 +2,10 @@
General serializer field tests. General serializer field tests.
""" """
import django
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from rest_framework import serializers from rest_framework import serializers
@ -16,6 +18,16 @@ class CharPrimaryKeyModel(models.Model):
id = models.CharField(max_length=20, primary_key=True) 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 TimestampedModelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = TimestampedModel model = TimestampedModel
@ -26,6 +38,46 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
model = CharPrimaryKeyModel 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): class ReadOnlyFieldTests(TestCase):
def test_auto_now_fields_read_only(self): def test_auto_now_fields_read_only(self):
""" """
@ -47,3 +99,133 @@ class ReadOnlyFieldTests(TestCase):
""" """
serializer = CharPrimaryKeyModelSerializer() serializer = CharPrimaryKeyModelSerializer()
self.assertEquals(serializer.fields['id'].read_only, False) 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']})

View File

@ -1,8 +1,6 @@
import datetime import datetime
import pickle import pickle
import django
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
@ -43,36 +41,6 @@ class CommentSerializer(serializers.Serializer):
return instance 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): class BookSerializer(serializers.ModelSerializer):
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'}) 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()) 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): class MetadataTests(TestCase):
def test_empty(self): def test_empty(self):
serializer = CommentSerializer() serializer = CommentSerializer()

View File

@ -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