Fixed #19997 -- Added custom EMPTY_VALUES to form fields
Thanks Loic Bistuer for the report and the patch.
This commit is contained in:
parent
25ce177e66
commit
4cccb85e29
1
AUTHORS
1
AUTHORS
|
@ -98,6 +98,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Natalia Bidart <nataliabidart@gmail.com>
|
Natalia Bidart <nataliabidart@gmail.com>
|
||||||
Mark Biggers <biggers@utsl.com>
|
Mark Biggers <biggers@utsl.com>
|
||||||
Paul Bissex <http://e-scribe.com/>
|
Paul Bissex <http://e-scribe.com/>
|
||||||
|
Loic Bistuer <loic.bistuer@sixmedia.com>
|
||||||
Simon Blanchard
|
Simon Blanchard
|
||||||
Craig Blaszczyk <masterjakul@gmail.com>
|
Craig Blaszczyk <masterjakul@gmail.com>
|
||||||
David Blewett <david@dawninglight.net>
|
David Blewett <david@dawninglight.net>
|
||||||
|
|
|
@ -53,6 +53,7 @@ class Field(object):
|
||||||
'required': _('This field is required.'),
|
'required': _('This field is required.'),
|
||||||
'invalid': _('Enter a valid value.'),
|
'invalid': _('Enter a valid value.'),
|
||||||
}
|
}
|
||||||
|
empty_values = list(validators.EMPTY_VALUES)
|
||||||
|
|
||||||
# Tracks each time a Field instance is created. Used to retain order.
|
# Tracks each time a Field instance is created. Used to retain order.
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
|
@ -125,11 +126,11 @@ class Field(object):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if value in validators.EMPTY_VALUES and self.required:
|
if value in self.empty_values and self.required:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
|
|
||||||
def run_validators(self, value):
|
def run_validators(self, value):
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return
|
return
|
||||||
errors = []
|
errors = []
|
||||||
for v in self.validators:
|
for v in self.validators:
|
||||||
|
@ -210,7 +211,7 @@ class CharField(Field):
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"Returns a Unicode object."
|
"Returns a Unicode object."
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return ''
|
return ''
|
||||||
return smart_text(value)
|
return smart_text(value)
|
||||||
|
|
||||||
|
@ -244,7 +245,7 @@ class IntegerField(Field):
|
||||||
of int(). Returns None for empty values.
|
of int(). Returns None for empty values.
|
||||||
"""
|
"""
|
||||||
value = super(IntegerField, self).to_python(value)
|
value = super(IntegerField, self).to_python(value)
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if self.localize:
|
if self.localize:
|
||||||
value = formats.sanitize_separators(value)
|
value = formats.sanitize_separators(value)
|
||||||
|
@ -275,7 +276,7 @@ class FloatField(IntegerField):
|
||||||
of float(). Returns None for empty values.
|
of float(). Returns None for empty values.
|
||||||
"""
|
"""
|
||||||
value = super(IntegerField, self).to_python(value)
|
value = super(IntegerField, self).to_python(value)
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if self.localize:
|
if self.localize:
|
||||||
value = formats.sanitize_separators(value)
|
value = formats.sanitize_separators(value)
|
||||||
|
@ -311,7 +312,7 @@ class DecimalField(IntegerField):
|
||||||
than max_digits in the number, and no more than decimal_places digits
|
than max_digits in the number, and no more than decimal_places digits
|
||||||
after the decimal point.
|
after the decimal point.
|
||||||
"""
|
"""
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if self.localize:
|
if self.localize:
|
||||||
value = formats.sanitize_separators(value)
|
value = formats.sanitize_separators(value)
|
||||||
|
@ -324,7 +325,7 @@ class DecimalField(IntegerField):
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
super(DecimalField, self).validate(value)
|
super(DecimalField, self).validate(value)
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return
|
return
|
||||||
# Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
|
# Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
|
||||||
# since it is never equal to itself. However, NaN is the only value that
|
# since it is never equal to itself. However, NaN is the only value that
|
||||||
|
@ -401,7 +402,7 @@ class DateField(BaseTemporalField):
|
||||||
Validates that the input can be converted to a date. Returns a Python
|
Validates that the input can be converted to a date. Returns a Python
|
||||||
datetime.date object.
|
datetime.date object.
|
||||||
"""
|
"""
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, datetime.datetime):
|
if isinstance(value, datetime.datetime):
|
||||||
return value.date()
|
return value.date()
|
||||||
|
@ -425,7 +426,7 @@ class TimeField(BaseTemporalField):
|
||||||
Validates that the input can be converted to a time. Returns a Python
|
Validates that the input can be converted to a time. Returns a Python
|
||||||
datetime.time object.
|
datetime.time object.
|
||||||
"""
|
"""
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, datetime.time):
|
if isinstance(value, datetime.time):
|
||||||
return value
|
return value
|
||||||
|
@ -451,7 +452,7 @@ class DateTimeField(BaseTemporalField):
|
||||||
Validates that the input can be converted to a datetime. Returns a
|
Validates that the input can be converted to a datetime. Returns a
|
||||||
Python datetime.datetime object.
|
Python datetime.datetime object.
|
||||||
"""
|
"""
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, datetime.datetime):
|
if isinstance(value, datetime.datetime):
|
||||||
return from_current_timezone(value)
|
return from_current_timezone(value)
|
||||||
|
@ -463,7 +464,7 @@ class DateTimeField(BaseTemporalField):
|
||||||
# components: date and time.
|
# components: date and time.
|
||||||
if len(value) != 2:
|
if len(value) != 2:
|
||||||
raise ValidationError(self.error_messages['invalid'])
|
raise ValidationError(self.error_messages['invalid'])
|
||||||
if value[0] in validators.EMPTY_VALUES and value[1] in validators.EMPTY_VALUES:
|
if value[0] in self.empty_values and value[1] in self.empty_values:
|
||||||
return None
|
return None
|
||||||
value = '%s %s' % tuple(value)
|
value = '%s %s' % tuple(value)
|
||||||
result = super(DateTimeField, self).to_python(value)
|
result = super(DateTimeField, self).to_python(value)
|
||||||
|
@ -531,7 +532,7 @@ class FileField(Field):
|
||||||
super(FileField, self).__init__(*args, **kwargs)
|
super(FileField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_python(self, data):
|
def to_python(self, data):
|
||||||
if data in validators.EMPTY_VALUES:
|
if data in self.empty_values:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# UploadedFile objects should have name and size attributes.
|
# UploadedFile objects should have name and size attributes.
|
||||||
|
@ -562,7 +563,7 @@ class FileField(Field):
|
||||||
return False
|
return False
|
||||||
# If the field is required, clearing is not possible (the widget
|
# If the field is required, clearing is not possible (the widget
|
||||||
# shouldn't return False data in that case anyway). False is not
|
# shouldn't return False data in that case anyway). False is not
|
||||||
# in validators.EMPTY_VALUES; if a False value makes it this far
|
# in self.empty_value; if a False value makes it this far
|
||||||
# it should be validated from here on out as None (so it will be
|
# it should be validated from here on out as None (so it will be
|
||||||
# caught by the required check).
|
# caught by the required check).
|
||||||
data = None
|
data = None
|
||||||
|
@ -763,7 +764,7 @@ class ChoiceField(Field):
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"Returns a Unicode object."
|
"Returns a Unicode object."
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return ''
|
return ''
|
||||||
return smart_text(value)
|
return smart_text(value)
|
||||||
|
|
||||||
|
@ -801,7 +802,7 @@ class TypedChoiceField(ChoiceField):
|
||||||
"""
|
"""
|
||||||
value = super(TypedChoiceField, self).to_python(value)
|
value = super(TypedChoiceField, self).to_python(value)
|
||||||
super(TypedChoiceField, self).validate(value)
|
super(TypedChoiceField, self).validate(value)
|
||||||
if value == self.empty_value or value in validators.EMPTY_VALUES:
|
if value == self.empty_value or value in self.empty_values:
|
||||||
return self.empty_value
|
return self.empty_value
|
||||||
try:
|
try:
|
||||||
value = self.coerce(value)
|
value = self.coerce(value)
|
||||||
|
@ -864,7 +865,7 @@ class TypedMultipleChoiceField(MultipleChoiceField):
|
||||||
"""
|
"""
|
||||||
value = super(TypedMultipleChoiceField, self).to_python(value)
|
value = super(TypedMultipleChoiceField, self).to_python(value)
|
||||||
super(TypedMultipleChoiceField, self).validate(value)
|
super(TypedMultipleChoiceField, self).validate(value)
|
||||||
if value == self.empty_value or value in validators.EMPTY_VALUES:
|
if value == self.empty_value or value in self.empty_values:
|
||||||
return self.empty_value
|
return self.empty_value
|
||||||
new_value = []
|
new_value = []
|
||||||
for choice in value:
|
for choice in value:
|
||||||
|
@ -945,7 +946,7 @@ class MultiValueField(Field):
|
||||||
clean_data = []
|
clean_data = []
|
||||||
errors = ErrorList()
|
errors = ErrorList()
|
||||||
if not value or isinstance(value, (list, tuple)):
|
if not value or isinstance(value, (list, tuple)):
|
||||||
if not value or not [v for v in value if v not in validators.EMPTY_VALUES]:
|
if not value or not [v for v in value if v not in self.empty_values]:
|
||||||
if self.required:
|
if self.required:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
else:
|
else:
|
||||||
|
@ -957,7 +958,7 @@ class MultiValueField(Field):
|
||||||
field_value = value[i]
|
field_value = value[i]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
field_value = None
|
field_value = None
|
||||||
if self.required and field_value in validators.EMPTY_VALUES:
|
if self.required and field_value in self.empty_values:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
try:
|
try:
|
||||||
clean_data.append(field.clean(field_value))
|
clean_data.append(field.clean(field_value))
|
||||||
|
@ -1071,9 +1072,9 @@ class SplitDateTimeField(MultiValueField):
|
||||||
if data_list:
|
if data_list:
|
||||||
# Raise a validation error if time or date is empty
|
# Raise a validation error if time or date is empty
|
||||||
# (possible if SplitDateTimeField has required=False).
|
# (possible if SplitDateTimeField has required=False).
|
||||||
if data_list[0] in validators.EMPTY_VALUES:
|
if data_list[0] in self.empty_values:
|
||||||
raise ValidationError(self.error_messages['invalid_date'])
|
raise ValidationError(self.error_messages['invalid_date'])
|
||||||
if data_list[1] in validators.EMPTY_VALUES:
|
if data_list[1] in self.empty_values:
|
||||||
raise ValidationError(self.error_messages['invalid_time'])
|
raise ValidationError(self.error_messages['invalid_time'])
|
||||||
result = datetime.datetime.combine(*data_list)
|
result = datetime.datetime.combine(*data_list)
|
||||||
return from_current_timezone(result)
|
return from_current_timezone(result)
|
||||||
|
@ -1087,7 +1088,7 @@ class IPAddressField(CharField):
|
||||||
default_validators = [validators.validate_ipv4_address]
|
default_validators = [validators.validate_ipv4_address]
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value in EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return ''
|
return ''
|
||||||
return value.strip()
|
return value.strip()
|
||||||
|
|
||||||
|
@ -1103,7 +1104,7 @@ class GenericIPAddressField(CharField):
|
||||||
super(GenericIPAddressField, self).__init__(*args, **kwargs)
|
super(GenericIPAddressField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value in validators.EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return ''
|
return ''
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if value and ':' in value:
|
if value and ':' in value:
|
||||||
|
|
|
@ -6,7 +6,6 @@ and database field objects.
|
||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
|
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
|
||||||
from django.core.validators import EMPTY_VALUES
|
|
||||||
from django.forms.fields import Field, ChoiceField
|
from django.forms.fields import Field, ChoiceField
|
||||||
from django.forms.forms import BaseForm, get_declared_fields
|
from django.forms.forms import BaseForm, get_declared_fields
|
||||||
from django.forms.formsets import BaseFormSet, formset_factory
|
from django.forms.formsets import BaseFormSet, formset_factory
|
||||||
|
@ -301,7 +300,7 @@ class BaseModelForm(BaseForm):
|
||||||
else:
|
else:
|
||||||
form_field = self.fields[field]
|
form_field = self.fields[field]
|
||||||
field_value = self.cleaned_data.get(field, None)
|
field_value = self.cleaned_data.get(field, None)
|
||||||
if not f.blank and not form_field.required and field_value in EMPTY_VALUES:
|
if not f.blank and not form_field.required and field_value in form_field.empty_values:
|
||||||
exclude.append(f.name)
|
exclude.append(f.name)
|
||||||
return exclude
|
return exclude
|
||||||
|
|
||||||
|
@ -880,7 +879,7 @@ class InlineForeignKeyField(Field):
|
||||||
super(InlineForeignKeyField, self).__init__(*args, **kwargs)
|
super(InlineForeignKeyField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
if value in EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
if self.pk_field:
|
if self.pk_field:
|
||||||
return None
|
return None
|
||||||
# if there is no value act as we did before.
|
# if there is no value act as we did before.
|
||||||
|
@ -1000,7 +999,7 @@ class ModelChoiceField(ChoiceField):
|
||||||
return super(ModelChoiceField, self).prepare_value(value)
|
return super(ModelChoiceField, self).prepare_value(value)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value in EMPTY_VALUES:
|
if value in self.empty_values:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
key = self.to_field_name or 'pk'
|
key = self.to_field_name or 'pk'
|
||||||
|
|
|
@ -27,7 +27,6 @@ from django.core.management.color import no_style
|
||||||
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
|
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
|
||||||
WSGIServerException)
|
WSGIServerException)
|
||||||
from django.core.urlresolvers import clear_url_caches
|
from django.core.urlresolvers import clear_url_caches
|
||||||
from django.core.validators import EMPTY_VALUES
|
|
||||||
from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction
|
from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction
|
||||||
from django.forms.fields import CharField
|
from django.forms.fields import CharField
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
|
@ -322,7 +321,7 @@ class SimpleTestCase(ut2.TestCase):
|
||||||
raised error messages.
|
raised error messages.
|
||||||
field_args: the args passed to instantiate the field
|
field_args: the args passed to instantiate the field
|
||||||
field_kwargs: the kwargs passed to instantiate the field
|
field_kwargs: the kwargs passed to instantiate the field
|
||||||
empty_value: the expected clean output for inputs in EMPTY_VALUES
|
empty_value: the expected clean output for inputs in empty_values
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if field_args is None:
|
if field_args is None:
|
||||||
|
@ -347,7 +346,7 @@ class SimpleTestCase(ut2.TestCase):
|
||||||
self.assertEqual(context_manager.exception.messages, errors)
|
self.assertEqual(context_manager.exception.messages, errors)
|
||||||
# test required inputs
|
# test required inputs
|
||||||
error_required = [force_text(required.error_messages['required'])]
|
error_required = [force_text(required.error_messages['required'])]
|
||||||
for e in EMPTY_VALUES:
|
for e in required.empty_values:
|
||||||
with self.assertRaises(ValidationError) as context_manager:
|
with self.assertRaises(ValidationError) as context_manager:
|
||||||
required.clean(e)
|
required.clean(e)
|
||||||
self.assertEqual(context_manager.exception.messages,
|
self.assertEqual(context_manager.exception.messages,
|
||||||
|
|
|
@ -1480,7 +1480,7 @@ your test suite.
|
||||||
error messages.
|
error messages.
|
||||||
:param field_args: the args passed to instantiate the field.
|
:param field_args: the args passed to instantiate the field.
|
||||||
:param field_kwargs: the kwargs passed to instantiate the field.
|
:param field_kwargs: the kwargs passed to instantiate the field.
|
||||||
:param empty_value: the expected clean output for inputs in ``EMPTY_VALUES``.
|
:param empty_value: the expected clean output for inputs in ``empty_values``.
|
||||||
|
|
||||||
For example, the following code tests that an ``EmailField`` accepts
|
For example, the following code tests that an ``EmailField`` accepts
|
||||||
"a@a.com" as a valid email address, but rejects "aaa" with a reasonable
|
"a@a.com" as a valid email address, but rejects "aaa" with a reasonable
|
||||||
|
|
|
@ -1797,3 +1797,23 @@ class FormsTestCase(TestCase):
|
||||||
form = NameForm(data={'name' : ['fname', 'lname']})
|
form = NameForm(data={'name' : ['fname', 'lname']})
|
||||||
self.assertTrue(form.is_valid())
|
self.assertTrue(form.is_valid())
|
||||||
self.assertEqual(form.cleaned_data, {'name' : 'fname lname'})
|
self.assertEqual(form.cleaned_data, {'name' : 'fname lname'})
|
||||||
|
|
||||||
|
def test_custom_empty_values(self):
|
||||||
|
"""
|
||||||
|
Test that form fields can customize what is considered as an empty value
|
||||||
|
for themselves (#19997).
|
||||||
|
"""
|
||||||
|
class CustomJSONField(CharField):
|
||||||
|
empty_values = [None, '']
|
||||||
|
def to_python(self, value):
|
||||||
|
# Fake json.loads
|
||||||
|
if value == '{}':
|
||||||
|
return {}
|
||||||
|
return super(CustomJSONField, self).to_python(value)
|
||||||
|
|
||||||
|
class JSONForm(forms.Form):
|
||||||
|
json = CustomJSONField()
|
||||||
|
|
||||||
|
form = JSONForm(data={'json': '{}'});
|
||||||
|
form.full_clean()
|
||||||
|
self.assertEqual(form.cleaned_data, {'json' : {}})
|
||||||
|
|
Loading…
Reference in New Issue