Skip to content

Instantly share code, notes, and snippets.

@nathan-osman
Last active December 21, 2015 18:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathan-osman/6347178 to your computer and use it in GitHub Desktop.
Save nathan-osman/6347178 to your computer and use it in GitHub Desktop.
Bug #15511 and #17051 Patch for Django 1.5.2
=== modified file 'django/contrib/auth/tests/forms.py'
--- django/contrib/auth/tests/forms.py 2013-08-26 21:11:16 +0000
+++ django/contrib/auth/tests/forms.py 2013-08-27 00:25:07 +0000
@@ -7,7 +7,7 @@
ReadOnlyPasswordHashWidget)
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.core import mail
-from django.forms.fields import Field, EmailField, CharField
+from django.forms.fields import Field, CharField
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.encoding import force_text
@@ -322,8 +322,7 @@
data = {'email': 'not valid'}
form = PasswordResetForm(data)
self.assertFalse(form.is_valid())
- self.assertEqual(form['email'].errors,
- [force_text(EmailField.default_error_messages['invalid'])])
+ self.assertEqual(form['email'].errors, [_('Enter a valid email address.')])
def test_nonexistant_email(self):
# Test nonexistant email address
=== modified file 'django/forms/fields.py'
--- django/forms/fields.py 2013-08-26 21:11:16 +0000
+++ django/forms/fields.py 2013-08-27 00:25:07 +0000
@@ -47,9 +47,10 @@
widget = TextInput # Default widget to use when rendering this type of Field.
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
default_validators = [] # Default set of validators
+ # Add an 'invalid' entry to default_error_message if you want a specific
+ # field error message not raised by the field validators.
default_error_messages = {
'required': _('This field is required.'),
- 'invalid': _('Enter a valid value.'),
}
# Tracks each time a Field instance is created. Used to retain order.
@@ -207,8 +208,6 @@
class IntegerField(Field):
default_error_messages = {
'invalid': _('Enter a whole number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
}
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
@@ -460,9 +459,6 @@
regex = property(_get_regex, _set_regex)
class EmailField(CharField):
- default_error_messages = {
- 'invalid': _('Enter a valid email address.'),
- }
default_validators = [validators.validate_email]
def clean(self, value):
@@ -833,15 +829,20 @@
"""
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):
@@ -871,15 +872,24 @@
field_value = value[i]
except IndexError:
field_value = None
- if self.required and field_value in validators.EMPTY_VALUES:
- raise ValidationError(self.error_messages['required'])
+ if field_value in validators.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'])
+ 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.messages)
+ # exception for the first error we encounter. Skip duplicates.
+ errors.extend(m for m in e.messages if m not in errors)
if errors:
raise ValidationError(errors)
@@ -983,34 +993,22 @@
class IPAddressField(CharField):
- default_error_messages = {
- 'invalid': _('Enter a valid IPv4 address.'),
- }
default_validators = [validators.validate_ipv4_address]
class GenericIPAddressField(CharField):
- default_error_messages = {}
-
def __init__(self, protocol='both', unpack_ipv4=False, *args, **kwargs):
self.unpack_ipv4 = unpack_ipv4
- self.default_validators, invalid_error_message = \
- validators.ip_address_validators(protocol, unpack_ipv4)
- self.default_error_messages['invalid'] = invalid_error_message
+ self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0]
super(GenericIPAddressField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return ''
if value and ':' in value:
- return clean_ipv6_address(value,
- self.unpack_ipv4, self.error_messages['invalid'])
+ return clean_ipv6_address(value, self.unpack_ipv4)
return value
class SlugField(CharField):
- default_error_messages = {
- 'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
- " underscores or hyphens."),
- }
default_validators = [validators.validate_slug]
=== modified file 'django/utils/ipv6.py'
--- django/utils/ipv6.py 2013-08-26 21:11:16 +0000
+++ django/utils/ipv6.py 2013-08-27 00:25:07 +0000
@@ -5,7 +5,7 @@
from django.utils.six.moves import xrange
def clean_ipv6_address(ip_str, unpack_ipv4=False,
- error_message="This is not a valid IPv6 address"):
+ error_message="This is not a valid IPv6 address."):
"""
Cleans a IPv6 address string.
=== modified file 'docs/ref/forms/fields.txt'
--- docs/ref/forms/fields.txt 2013-08-26 21:11:16 +0000
+++ docs/ref/forms/fields.txt 2013-08-27 00:25:07 +0000
@@ -861,7 +861,7 @@
* 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.
@@ -882,6 +882,45 @@
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`.
=== modified file 'docs/ref/forms/validation.txt'
--- docs/ref/forms/validation.txt 2013-08-26 21:11:16 +0000
+++ docs/ref/forms/validation.txt 2013-08-27 00:25:07 +0000
@@ -181,24 +181,20 @@
the ``default_validators`` attribute.
Simple validators can be used to validate values inside the field, let's have
-a look at Django's ``EmailField``::
-
- class EmailField(CharField):
- default_error_messages = {
- 'invalid': _('Enter a valid email address.'),
- }
- default_validators = [validators.validate_email]
-
-As you can see, ``EmailField`` is just a ``CharField`` with customized error
-message and a validator that validates email addresses. This can also be done
-on field definition so::
-
- email = forms.EmailField()
+a look at Django's ``SlugField``::
+
+ class SlugField(CharField):
+ default_validators = [validators.validate_slug]
+
+As you can see, ``SlugField`` is just a ``CharField`` with a customized
+validator that validates that submitted text obeys to some character rules.
+This can also be done on field definition so::
+
+ slug = forms.SlugField()
is equivalent to::
- email = forms.CharField(validators=[validators.validate_email],
- error_messages={'invalid': _('Enter a valid email address.')})
+ slug = forms.CharField(validators=[validators.validate_slug])
Form field default cleaning
=== modified file 'tests/regressiontests/forms/tests/extra.py'
--- tests/regressiontests/forms/tests/extra.py 2013-08-26 21:11:16 +0000
+++ tests/regressiontests/forms/tests/extra.py 2013-08-27 00:25:07 +0000
@@ -489,11 +489,11 @@
self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '256.125.1.5')
self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), 'fe80::223:6cff:fe8a:2e8a')
self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), '2a02::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '12345:2:3:4')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3::4')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1:2')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1:2')
def test_generic_ipaddress_as_ipv4_only(self):
f = GenericIPAddressField(protocol="IPv4")
@@ -518,11 +518,11 @@
self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, '256.125.1.5')
self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), 'fe80::223:6cff:fe8a:2e8a')
self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), '2a02::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, '12345:2:3:4')
- self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, '1::2:3::4')
- self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
- self.assertFormErrors(['Enter a valid IPv6 address.'], f.clean, '1:2')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1:2')
def test_generic_ipaddress_as_generic_not_required(self):
f = GenericIPAddressField(required=False)
@@ -535,11 +535,11 @@
self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '256.125.1.5')
self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), 'fe80::223:6cff:fe8a:2e8a')
self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), '2a02::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '12345:2:3:4')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3::4')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
- self.assertFormErrors(['Enter a valid IPv4 or IPv6 address.'], f.clean, '1:2')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors(['This is not a valid IPv6 address.'], f.clean, '1:2')
def test_generic_ipaddress_normalization(self):
# Test the normalising code
=== modified file 'tests/regressiontests/forms/tests/forms.py'
--- tests/regressiontests/forms/tests/forms.py 2013-08-26 21:11:16 +0000
+++ tests/regressiontests/forms/tests/forms.py 2013-08-27 00:25:07 +0000
@@ -4,6 +4,7 @@
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
@@ -1781,6 +1782,75 @@
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_boundfield_label_tag(self):
class SomeForm(Form):
field = CharField()
=== modified file 'tests/regressiontests/forms/tests/validators.py'
--- tests/regressiontests/forms/tests/validators.py 2013-08-26 21:11:16 +0000
+++ tests/regressiontests/forms/tests/validators.py 2013-08-27 00:25:07 +0000
@@ -1,16 +1,39 @@
+from __future__ import unicode_literals
+
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.unittest import TestCase
+class UserForm(forms.Form):
+ full_name = forms.CharField(
+ max_length = 50,
+ validators = [
+ validators.validate_integer,
+ validators.validate_email,
+ ]
+ )
+ string = forms.CharField(
+ max_length = 50,
+ validators = [
+ validators.RegexValidator(
+ regex='^[a-zA-Z]*$',
+ message="Letters only.",
+ )
+ ]
+ )
+
+
class TestFieldWithValidators(TestCase):
def test_all_errors_get_reported(self):
- field = forms.CharField(
- validators=[validators.validate_integer, validators.validate_email]
- )
- self.assertRaises(ValidationError, field.clean, 'not int nor mail')
+ form = UserForm({'full_name': 'not int nor mail', 'string': '2 is not correct'})
+ self.assertRaises(ValidationError, form.fields['full_name'].clean, 'not int nor mail')
+
try:
- field.clean('not int nor mail')
+ form.fields['full_name'].clean('not int nor mail')
except ValidationError as e:
self.assertEqual(2, len(e.messages))
+
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['string'], ["Letters only."])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment