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 = {
|
||||
'invalid': _('Enter a list of values.'),
|
||||
'incomplete': _('Enter a complete value.'),
|
||||
}
|
||||
|
||||
def __init__(self, fields=(), *args, **kwargs):
|
||||
self.require_all_fields = kwargs.pop('require_all_fields', True)
|
||||
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:
|
||||
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
|
||||
|
||||
def validate(self, value):
|
||||
|
@ -993,15 +998,26 @@ class MultiValueField(Field):
|
|||
field_value = value[i]
|
||||
except IndexError:
|
||||
field_value = None
|
||||
if self.required and field_value in self.empty_values:
|
||||
raise ValidationError(self.error_messages['required'], code='required')
|
||||
if field_value in self.empty_values:
|
||||
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:
|
||||
clean_data.append(field.clean(field_value))
|
||||
except ValidationError as e:
|
||||
# Collect all validation errors in a single list, which we'll
|
||||
# raise at the end of clean(), rather than raising a single
|
||||
# exception for the first error we encounter.
|
||||
errors.extend(e.error_list)
|
||||
# exception for the first error we encounter. Skip duplicates.
|
||||
errors.extend(m for m in e.error_list if m not in errors)
|
||||
if 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.
|
||||
* Validates that the given value against each of the fields specified
|
||||
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
|
||||
value.
|
||||
|
@ -898,6 +898,45 @@ Slightly complex built-in ``Field`` classes
|
|||
Once all fields are cleaned, the list of clean values is combined into
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
=====================================
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||
import datetime
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import RegexValidator
|
||||
from django.forms import *
|
||||
from django.http import QueryDict
|
||||
from django.template import Template, Context
|
||||
|
@ -1792,6 +1793,75 @@ class FormsTestCase(TestCase):
|
|||
self.assertTrue(form.is_valid())
|
||||
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):
|
||||
"""
|
||||
Test that form fields can customize what is considered as an empty value
|
||||
|
|
Loading…
Reference in New Issue