Last active
December 21, 2015 18:28
-
-
Save nathan-osman/6347178 to your computer and use it in GitHub Desktop.
Bug #15511 and #17051 Patch for Django 1.5.2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
=== 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