Commit bfff356d by Stephan Groß

Add better date / datetime validation (pull 2)

addition to #631 with update to master + timefield support
parent 8da83f0d
...@@ -185,12 +185,20 @@ Corresponds to `django.forms.fields.RegexField` ...@@ -185,12 +185,20 @@ Corresponds to `django.forms.fields.RegexField`
A date representation. 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` Corresponds to `django.db.models.fields.DateField`
## DateTimeField ## DateTimeField
A date and time representation. 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` 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. 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 ...@@ -207,6 +215,10 @@ If you want to override this behavior, you'll need to declare the `DateTimeField
A time representation. 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` Corresponds to `django.db.models.fields.TimeField`
## IntegerField ## IntegerField
......
...@@ -44,6 +44,9 @@ You can determine your currently installed version using `pip freeze`: ...@@ -44,6 +44,9 @@ You can determine your currently installed version using `pip freeze`:
* Bugfix for serializer data being uncacheable with pickle protocol 0. * Bugfix for serializer data being uncacheable with pickle protocol 0.
* Bugfixes for model field validation edge-cases. * 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 ### 2.2.1
......
...@@ -19,6 +19,7 @@ from rest_framework.compat import BytesIO ...@@ -19,6 +19,7 @@ from rest_framework.compat import BytesIO
from rest_framework.compat import six from rest_framework.compat import six
from rest_framework.compat import smart_text from rest_framework.compat import smart_text
from rest_framework.compat import parse_time from rest_framework.compat import parse_time
from rest_framework.utils.dates import get_readable_date_format
def is_simple_callable(obj): def is_simple_callable(obj):
...@@ -447,13 +448,14 @@ class DateField(WritableField): ...@@ -447,13 +448,14 @@ class DateField(WritableField):
form_field_class = forms.DateField form_field_class = forms.DateField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value has an invalid date format. It must be " 'invalid': _(u"Date has wrong format. Use one of these formats instead: %s"),
"in YYYY-MM-DD format."),
'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) "
"but it is an invalid date."),
} }
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
...@@ -468,15 +470,16 @@ class DateField(WritableField): ...@@ -468,15 +470,16 @@ class DateField(WritableField):
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
return value return value
for format in self.format:
try: try:
parsed = parse_date(value) parsed = datetime.datetime.strptime(value, format)
if parsed is not None:
return parsed
except (ValueError, TypeError): except (ValueError, TypeError):
msg = self.error_messages['invalid_date'] % value pass
raise ValidationError(msg) 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) raise ValidationError(msg)
...@@ -486,16 +489,14 @@ class DateTimeField(WritableField): ...@@ -486,16 +489,14 @@ class DateTimeField(WritableField):
form_field_class = forms.DateTimeField form_field_class = forms.DateTimeField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value has an invalid format. It must be in " 'invalid': _(u"Datetime has wrong format. Use one of these formats instead: %s"),
"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."),
} }
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
...@@ -516,23 +517,16 @@ class DateTimeField(WritableField): ...@@ -516,23 +517,16 @@ class DateTimeField(WritableField):
value = timezone.make_aware(value, default_timezone) value = timezone.make_aware(value, default_timezone)
return value return value
for format in self.format:
try: try:
parsed = parse_datetime(value) parsed = datetime.datetime.strptime(value, format)
if parsed is not None:
return parsed
except (ValueError, TypeError): except (ValueError, TypeError):
msg = self.error_messages['invalid_datetime'] % value pass
raise ValidationError(msg) else:
return parsed
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) raise ValidationError(msg)
...@@ -542,11 +536,14 @@ class TimeField(WritableField): ...@@ -542,11 +536,14 @@ class TimeField(WritableField):
form_field_class = forms.TimeField form_field_class = forms.TimeField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value has an invalid format. It must be a valid " 'invalid': _(u"Time has wrong format. Use one of these formats instead: %s"),
"time in the HH:MM[:ss[.uuuuuu]] format."),
} }
empty = None 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): def from_native(self, value):
if value in validators.EMPTY_VALUES: if value in validators.EMPTY_VALUES:
return None return None
...@@ -554,12 +551,16 @@ class TimeField(WritableField): ...@@ -554,12 +551,16 @@ class TimeField(WritableField):
if isinstance(value, datetime.time): if isinstance(value, datetime.time):
return value return value
for format in self.format:
try: try:
parsed = parse_time(value) parsed = datetime.datetime.strptime(value, format)
assert parsed is not None
return parsed
except (ValueError, TypeError): except (ValueError, TypeError):
msg = self.error_messages['invalid'] % value 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) raise ValidationError(msg)
......
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment