Commit 9157db5d by Stephan Groß

Add better date / datetime validation (pull 2)

addition to #631 with update to master + timefield support
parent 282af605
...@@ -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
......
...@@ -45,6 +45,9 @@ You can determine your currently installed version using `pip freeze`: ...@@ -45,6 +45,9 @@ You can determine your currently installed version using `pip freeze`:
* Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view. * Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view.
* 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
try: for format in self.format:
parsed = parse_date(value) try:
if parsed is not None: parsed = datetime.datetime.strptime(value, format)
return parsed except (ValueError, TypeError):
except (ValueError, TypeError): pass
msg = self.error_messages['invalid_date'] % value else:
raise ValidationError(msg) 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
try: for format in self.format:
parsed = parse_datetime(value) try:
if parsed is not None: parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
return parsed return parsed
except (ValueError, TypeError):
msg = self.error_messages['invalid_datetime'] % value
raise ValidationError(msg)
try: datetime_input_formats = '; '.join(self.format)
parsed = parse_date(value) msg = self.error_messages['invalid'] % get_readable_date_format(datetime_input_formats)
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
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,13 +551,17 @@ class TimeField(WritableField): ...@@ -554,13 +551,17 @@ class TimeField(WritableField):
if isinstance(value, datetime.time): if isinstance(value, datetime.time):
return value return value
try: for format in self.format:
parsed = parse_time(value) try:
assert parsed is not None parsed = datetime.datetime.strptime(value, format)
return parsed except (ValueError, TypeError):
except (ValueError, TypeError): pass
msg = self.error_messages['invalid'] % value else:
raise ValidationError(msg) 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): class IntegerField(WritableField):
......
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