Fixed #15511 -- Allow optional fields on ``MultiValueField` subclasses.
The `MultiValueField` class gets a new ``require_all_fields`` argument that defaults to ``True``. If set to ``False``, individual fields can be made optional, and a new ``incomplete`` validation error will be raised if any required fields have empty values. The ``incomplete`` error message can be defined on a `MultiValueField` subclass or on each individual field. Skip duplicate errors.
This commit is contained in:
parent
c33d1ca1d9
commit
1280675834
|
@ -955,15 +955,20 @@ class MultiValueField(Field):
|
||||||
"""
|
"""
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _('Enter a list of values.'),
|
'invalid': _('Enter a list of values.'),
|
||||||
|
'incomplete': _('Enter a complete value.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, fields=(), *args, **kwargs):
|
def __init__(self, fields=(), *args, **kwargs):
|
||||||
|
self.require_all_fields = kwargs.pop('require_all_fields', True)
|
||||||
super(MultiValueField, self).__init__(*args, **kwargs)
|
super(MultiValueField, self).__init__(*args, **kwargs)
|
||||||
# Set 'required' to False on the individual fields, because the
|
|
||||||
# required validation will be handled by MultiValueField, not by those
|
|
||||||
# individual fields.
|
|
||||||
for f in fields:
|
for f in fields:
|
||||||
f.required = False
|
f.error_messages.setdefault('incomplete',
|
||||||
|
self.error_messages['incomplete'])
|
||||||
|
if self.require_all_fields:
|
||||||
|
# Set 'required' to False on the individual fields, because the
|
||||||
|
# required validation will be handled by MultiValueField, not
|
||||||
|
# by those individual fields.
|
||||||
|
f.required = False
|
||||||
self.fields = fields
|
self.fields = fields
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
@ -993,15 +998,26 @@ 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 self.empty_values:
|
if field_value in self.empty_values:
|
||||||
raise ValidationError(self.error_messages['required'], code='required')
|
if self.require_all_fields:
|
||||||
|
# Raise a 'required' error if the MultiValueField is
|
||||||
|
# required and any field is empty.
|
||||||
|
if self.required:
|
||||||
|
raise ValidationError(self.error_messages['required'], code='required')
|
||||||
|
elif field.required:
|
||||||
|
# Otherwise, add an 'incomplete' error to the list of
|
||||||
|
# collected errors and skip field cleaning, if a required
|
||||||
|
# field is empty.
|
||||||
|
if field.error_messages['incomplete'] not in errors:
|
||||||
|
errors.append(field.error_messages['incomplete'])
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
clean_data.append(field.clean(field_value))
|
clean_data.append(field.clean(field_value))
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
# Collect all validation errors in a single list, which we'll
|
# Collect all validation errors in a single list, which we'll
|
||||||
# raise at the end of clean(), rather than raising a single
|
# raise at the end of clean(), rather than raising a single
|
||||||
# exception for the first error we encounter.
|
# exception for the first error we encounter. Skip duplicates.
|
||||||
errors.extend(e.error_list)
|
errors.extend(m for m in e.error_list if m not in errors)
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
|
|
|
@ -877,7 +877,7 @@ Slightly complex built-in ``Field`` classes
|
||||||
* Normalizes to: the type returned by the ``compress`` method of the subclass.
|
* Normalizes to: the type returned by the ``compress`` method of the subclass.
|
||||||
* Validates that the given value against each of the fields specified
|
* Validates that the given value against each of the fields specified
|
||||||
as an argument to the ``MultiValueField``.
|
as an argument to the ``MultiValueField``.
|
||||||
* Error message keys: ``required``, ``invalid``
|
* Error message keys: ``required``, ``invalid``, ``incomplete``
|
||||||
|
|
||||||
Aggregates the logic of multiple fields that together produce a single
|
Aggregates the logic of multiple fields that together produce a single
|
||||||
value.
|
value.
|
||||||
|
@ -898,6 +898,45 @@ Slightly complex built-in ``Field`` classes
|
||||||
Once all fields are cleaned, the list of clean values is combined into
|
Once all fields are cleaned, the list of clean values is combined into
|
||||||
a single value by :meth:`~MultiValueField.compress`.
|
a single value by :meth:`~MultiValueField.compress`.
|
||||||
|
|
||||||
|
Also takes one extra optional argument:
|
||||||
|
|
||||||
|
.. attribute:: require_all_fields
|
||||||
|
|
||||||
|
.. versionadded:: 1.7
|
||||||
|
|
||||||
|
Defaults to ``True``, in which case a ``required`` validation error
|
||||||
|
will be raised if no value is supplied for any field.
|
||||||
|
|
||||||
|
When set to ``False``, the :attr:`Field.required` attribute can be set
|
||||||
|
to ``False`` for individual fields to make them optional. If no value
|
||||||
|
is supplied for a required field, an ``incomplete`` validation error
|
||||||
|
will be raised.
|
||||||
|
|
||||||
|
A default ``incomplete`` error message can be defined on the
|
||||||
|
:class:`MultiValueField` subclass, or different messages can be defined
|
||||||
|
on each individual field. For example::
|
||||||
|
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
|
||||||
|
class PhoneField(MultiValueField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Define one message for all fields.
|
||||||
|
error_messages = {
|
||||||
|
'incomplete': 'Enter a country code and phone number.',
|
||||||
|
}
|
||||||
|
# Or define a different message for each field.
|
||||||
|
fields = (
|
||||||
|
CharField(error_messages={'incomplete': 'Enter a country code.'},
|
||||||
|
validators=[RegexValidator(r'^\d+$', 'Enter a valid country code.')]),
|
||||||
|
CharField(error_messages={'incomplete': 'Enter a phone number.'},
|
||||||
|
validators=[RegexValidator(r'^\d+$', 'Enter a valid phone number.')]),
|
||||||
|
CharField(validators=[RegexValidator(r'^\d+$', 'Enter a valid extension.')],
|
||||||
|
required=False),
|
||||||
|
)
|
||||||
|
super(PhoneField, self).__init__(
|
||||||
|
self, error_messages=error_messages, fields=fields,
|
||||||
|
require_all_fields=False, *args, **kwargs)
|
||||||
|
|
||||||
.. attribute:: MultiValueField.widget
|
.. attribute:: MultiValueField.widget
|
||||||
|
|
||||||
Must be a subclass of :class:`django.forms.MultiWidget`.
|
Must be a subclass of :class:`django.forms.MultiWidget`.
|
||||||
|
|
|
@ -122,6 +122,11 @@ Minor features
|
||||||
``html_email_template_name`` parameter used to send a multipart HTML email
|
``html_email_template_name`` parameter used to send a multipart HTML email
|
||||||
for password resets.
|
for password resets.
|
||||||
|
|
||||||
|
* :class:`~django.forms.MultiValueField` allows optional subfields by setting
|
||||||
|
the ``require_all_fields`` argument to ``False``. The ``required`` attribute
|
||||||
|
for each individual field will be respected, and a new ``incomplete``
|
||||||
|
validation error will be raised when any required fields are empty.
|
||||||
|
|
||||||
Backwards incompatible changes in 1.7
|
Backwards incompatible changes in 1.7
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
from django.forms import *
|
from django.forms import *
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
@ -1792,6 +1793,75 @@ class FormsTestCase(TestCase):
|
||||||
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_multivalue_optional_subfields(self):
|
||||||
|
class PhoneField(MultiValueField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
fields = (
|
||||||
|
CharField(label='Country Code', validators=[
|
||||||
|
RegexValidator(r'^\+\d{1,2}$', message='Enter a valid country code.')]),
|
||||||
|
CharField(label='Phone Number'),
|
||||||
|
CharField(label='Extension', error_messages={'incomplete': 'Enter an extension.'}),
|
||||||
|
CharField(label='Label', required=False, help_text='E.g. home, work.'),
|
||||||
|
)
|
||||||
|
super(PhoneField, self).__init__(fields, *args, **kwargs)
|
||||||
|
|
||||||
|
def compress(self, data_list):
|
||||||
|
if data_list:
|
||||||
|
return '%s.%s ext. %s (label: %s)' % tuple(data_list)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# An empty value for any field will raise a `required` error on a
|
||||||
|
# required `MultiValueField`.
|
||||||
|
f = PhoneField()
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, [])
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61'])
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61', '287654321', '123'])
|
||||||
|
self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home']))
|
||||||
|
self.assertRaisesMessage(ValidationError,
|
||||||
|
"'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home'])
|
||||||
|
|
||||||
|
# Empty values for fields will NOT raise a `required` error on an
|
||||||
|
# optional `MultiValueField`
|
||||||
|
f = PhoneField(required=False)
|
||||||
|
self.assertEqual(None, f.clean(''))
|
||||||
|
self.assertEqual(None, f.clean(None))
|
||||||
|
self.assertEqual(None, f.clean([]))
|
||||||
|
self.assertEqual('+61. ext. (label: )', f.clean(['+61']))
|
||||||
|
self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123']))
|
||||||
|
self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home']))
|
||||||
|
self.assertRaisesMessage(ValidationError,
|
||||||
|
"'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home'])
|
||||||
|
|
||||||
|
# For a required `MultiValueField` with `require_all_fields=False`, a
|
||||||
|
# `required` error will only be raised if all fields are empty. Fields
|
||||||
|
# can individually be required or optional. An empty value for any
|
||||||
|
# required field will raise an `incomplete` error.
|
||||||
|
f = PhoneField(require_all_fields=False)
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
|
||||||
|
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, [])
|
||||||
|
self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61'])
|
||||||
|
self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123']))
|
||||||
|
six.assertRaisesRegex(self, ValidationError,
|
||||||
|
"'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home'])
|
||||||
|
self.assertRaisesMessage(ValidationError,
|
||||||
|
"'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home'])
|
||||||
|
|
||||||
|
# For an optional `MultiValueField` with `require_all_fields=False`, we
|
||||||
|
# don't get any `required` error but we still get `incomplete` errors.
|
||||||
|
f = PhoneField(required=False, require_all_fields=False)
|
||||||
|
self.assertEqual(None, f.clean(''))
|
||||||
|
self.assertEqual(None, f.clean(None))
|
||||||
|
self.assertEqual(None, f.clean([]))
|
||||||
|
self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61'])
|
||||||
|
self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123']))
|
||||||
|
six.assertRaisesRegex(self, ValidationError,
|
||||||
|
"'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home'])
|
||||||
|
self.assertRaisesMessage(ValidationError,
|
||||||
|
"'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home'])
|
||||||
|
|
||||||
def test_custom_empty_values(self):
|
def test_custom_empty_values(self):
|
||||||
"""
|
"""
|
||||||
Test that form fields can customize what is considered as an empty value
|
Test that form fields can customize what is considered as an empty value
|
||||||
|
|
Loading…
Reference in New Issue