Fixed #31806 -- Made validators include the value in ValidationErrors.
This commit is contained in:
parent
013e06bb37
commit
83fbaa9231
|
@ -48,7 +48,7 @@ class RegexValidator:
|
|||
regex_matches = self.regex.search(str(value))
|
||||
invalid_input = regex_matches if self.inverse_match else not regex_matches
|
||||
if invalid_input:
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
|
@ -100,11 +100,11 @@ class URLValidator(RegexValidator):
|
|||
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, str):
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
# Check if the scheme is valid.
|
||||
scheme = value.split('://')[0].lower()
|
||||
if scheme not in self.schemes:
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
# Then check full URL
|
||||
try:
|
||||
|
@ -115,7 +115,7 @@ class URLValidator(RegexValidator):
|
|||
try:
|
||||
scheme, netloc, path, query, fragment = urlsplit(value)
|
||||
except ValueError: # for example, "Invalid IPv6 URL"
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
try:
|
||||
netloc = punycode(netloc) # IDN -> ACE
|
||||
except UnicodeError: # invalid domain part
|
||||
|
@ -132,14 +132,14 @@ class URLValidator(RegexValidator):
|
|||
try:
|
||||
validate_ipv6_address(potential_ip)
|
||||
except ValidationError:
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
# The maximum length of a full host name is 253 characters per RFC 1034
|
||||
# section 3.1. It's defined to be 255 bytes or less, but this includes
|
||||
# one byte for the length of the name and one byte for the trailing dot
|
||||
# that's used to indicate absolute names in DNS.
|
||||
if len(urlsplit(value).netloc) > 253:
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
|
||||
integer_validator = RegexValidator(
|
||||
|
@ -208,12 +208,12 @@ class EmailValidator:
|
|||
|
||||
def __call__(self, value):
|
||||
if not value or '@' not in value:
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
user_part, domain_part = value.rsplit('@', 1)
|
||||
|
||||
if not self.user_regex.match(user_part):
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
if (domain_part not in self.domain_allowlist and
|
||||
not self.validate_domain_part(domain_part)):
|
||||
|
@ -225,7 +225,7 @@ class EmailValidator:
|
|||
else:
|
||||
if self.validate_domain_part(domain_part):
|
||||
return
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
def validate_domain_part(self, domain_part):
|
||||
if self.domain_regex.match(domain_part):
|
||||
|
@ -272,12 +272,12 @@ def validate_ipv4_address(value):
|
|||
try:
|
||||
ipaddress.IPv4Address(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_('Enter a valid IPv4 address.'), code='invalid')
|
||||
raise ValidationError(_('Enter a valid IPv4 address.'), code='invalid', params={'value': value})
|
||||
|
||||
|
||||
def validate_ipv6_address(value):
|
||||
if not is_valid_ipv6_address(value):
|
||||
raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid')
|
||||
raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid', params={'value': value})
|
||||
|
||||
|
||||
def validate_ipv46_address(value):
|
||||
|
@ -287,7 +287,7 @@ def validate_ipv46_address(value):
|
|||
try:
|
||||
validate_ipv6_address(value)
|
||||
except ValidationError:
|
||||
raise ValidationError(_('Enter a valid IPv4 or IPv6 address.'), code='invalid')
|
||||
raise ValidationError(_('Enter a valid IPv4 or IPv6 address.'), code='invalid', params={'value': value})
|
||||
|
||||
|
||||
ip_address_validator_map = {
|
||||
|
@ -438,7 +438,7 @@ class DecimalValidator:
|
|||
def __call__(self, value):
|
||||
digit_tuple, exponent = value.as_tuple()[1:]
|
||||
if exponent in {'F', 'n', 'N'}:
|
||||
raise ValidationError(self.messages['invalid'], code='invalid')
|
||||
raise ValidationError(self.messages['invalid'], code='invalid', params={'value': value})
|
||||
if exponent >= 0:
|
||||
# A positive exponent adds that many trailing zeros.
|
||||
digits = len(digit_tuple) + exponent
|
||||
|
@ -460,20 +460,20 @@ class DecimalValidator:
|
|||
raise ValidationError(
|
||||
self.messages['max_digits'],
|
||||
code='max_digits',
|
||||
params={'max': self.max_digits},
|
||||
params={'max': self.max_digits, 'value': value},
|
||||
)
|
||||
if self.decimal_places is not None and decimals > self.decimal_places:
|
||||
raise ValidationError(
|
||||
self.messages['max_decimal_places'],
|
||||
code='max_decimal_places',
|
||||
params={'max': self.decimal_places},
|
||||
params={'max': self.decimal_places, 'value': value},
|
||||
)
|
||||
if (self.max_digits is not None and self.decimal_places is not None and
|
||||
whole_digits > (self.max_digits - self.decimal_places)):
|
||||
raise ValidationError(
|
||||
self.messages['max_whole_digits'],
|
||||
code='max_whole_digits',
|
||||
params={'max': (self.max_digits - self.decimal_places)},
|
||||
params={'max': (self.max_digits - self.decimal_places), 'value': value},
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
|
@ -509,7 +509,8 @@ class FileExtensionValidator:
|
|||
code=self.code,
|
||||
params={
|
||||
'extension': extension,
|
||||
'allowed_extensions': ', '.join(self.allowed_extensions)
|
||||
'allowed_extensions': ', '.join(self.allowed_extensions),
|
||||
'value': value,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -550,7 +551,7 @@ class ProhibitNullCharactersValidator:
|
|||
|
||||
def __call__(self, value):
|
||||
if '\x00' in str(value):
|
||||
raise ValidationError(self.message, code=self.code)
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
|
|
|
@ -327,7 +327,9 @@ Utilities
|
|||
Validators
|
||||
~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* Built-in validators now include the provided value in the ``params`` argument
|
||||
of a raised :exc:`~django.core.exceptions.ValidationError`. This allows
|
||||
custom error messages to use the ``%(value)s`` placeholder.
|
||||
|
||||
.. _backwards-incompatible-3.2:
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from unittest import TestCase
|
|||
from django import forms
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
|
||||
class TestFieldWithValidators(TestCase):
|
||||
|
@ -68,8 +69,28 @@ class TestFieldWithValidators(TestCase):
|
|||
class ValidatorCustomMessageTests(TestCase):
|
||||
def test_value_placeholder_with_char_field(self):
|
||||
cases = [
|
||||
(validators.validate_integer, '-42.5', 'invalid'),
|
||||
(validators.validate_email, 'a', 'invalid'),
|
||||
(validators.validate_email, 'a@b\n.com', 'invalid'),
|
||||
(validators.validate_email, 'a\n@b.com', 'invalid'),
|
||||
(validators.validate_slug, '你 好', 'invalid'),
|
||||
(validators.validate_unicode_slug, '你 好', 'invalid'),
|
||||
(validators.validate_ipv4_address, '256.1.1.1', 'invalid'),
|
||||
(validators.validate_ipv6_address, '1:2', 'invalid'),
|
||||
(validators.validate_ipv46_address, '256.1.1.1', 'invalid'),
|
||||
(validators.validate_comma_separated_integer_list, 'a,b,c', 'invalid'),
|
||||
(validators.int_list_validator(), '-1,2,3', 'invalid'),
|
||||
(validators.MaxLengthValidator(10), 11 * 'x', 'max_length'),
|
||||
(validators.MinLengthValidator(10), 9 * 'x', 'min_length'),
|
||||
(validators.URLValidator(), 'no_scheme', 'invalid'),
|
||||
(validators.URLValidator(), 'http://test[.com', 'invalid'),
|
||||
(validators.URLValidator(), 'http://[::1:2::3]/', 'invalid'),
|
||||
(
|
||||
validators.URLValidator(),
|
||||
'http://' + '.'.join(['a' * 35 for _ in range(9)]),
|
||||
'invalid',
|
||||
),
|
||||
(validators.RegexValidator('[0-9]+'), 'xxxxxx', 'invalid'),
|
||||
]
|
||||
for validator, value, code in cases:
|
||||
if isinstance(validator, types.FunctionType):
|
||||
|
@ -87,10 +108,21 @@ class ValidatorCustomMessageTests(TestCase):
|
|||
self.assertIs(form.is_valid(), False)
|
||||
self.assertEqual(form.errors, {'field': [value]})
|
||||
|
||||
def test_value_placeholder_with_null_character(self):
|
||||
class MyForm(forms.Form):
|
||||
field = forms.CharField(
|
||||
error_messages={'null_characters_not_allowed': '%(value)s'},
|
||||
)
|
||||
|
||||
form = MyForm({'field': 'a\0b'})
|
||||
self.assertIs(form.is_valid(), False)
|
||||
self.assertEqual(form.errors, {'field': ['a\x00b']})
|
||||
|
||||
def test_value_placeholder_with_integer_field(self):
|
||||
cases = [
|
||||
(validators.MaxValueValidator(0), 1, 'max_value'),
|
||||
(validators.MinValueValidator(0), -1, 'min_value'),
|
||||
(validators.URLValidator(), '1', 'invalid'),
|
||||
]
|
||||
for validator, value, code in cases:
|
||||
with self.subTest(type(validator).__name__, value=value):
|
||||
|
@ -103,3 +135,34 @@ class ValidatorCustomMessageTests(TestCase):
|
|||
form = MyForm({'field': value})
|
||||
self.assertIs(form.is_valid(), False)
|
||||
self.assertEqual(form.errors, {'field': [str(value)]})
|
||||
|
||||
def test_value_placeholder_with_decimal_field(self):
|
||||
cases = [
|
||||
('NaN', 'invalid'),
|
||||
('123', 'max_digits'),
|
||||
('0.12', 'max_decimal_places'),
|
||||
('12', 'max_whole_digits'),
|
||||
]
|
||||
for value, code in cases:
|
||||
with self.subTest(value=value):
|
||||
class MyForm(forms.Form):
|
||||
field = forms.DecimalField(
|
||||
max_digits=2,
|
||||
decimal_places=1,
|
||||
error_messages={code: '%(value)s'},
|
||||
)
|
||||
|
||||
form = MyForm({'field': value})
|
||||
self.assertIs(form.is_valid(), False)
|
||||
self.assertEqual(form.errors, {'field': [value]})
|
||||
|
||||
def test_value_placeholder_with_file_field(self):
|
||||
class MyForm(forms.Form):
|
||||
field = forms.FileField(
|
||||
validators=[validators.validate_image_file_extension],
|
||||
error_messages={'invalid_extension': '%(value)s'},
|
||||
)
|
||||
|
||||
form = MyForm(files={'field': SimpleUploadedFile('myfile.txt', b'abc')})
|
||||
self.assertIs(form.is_valid(), False)
|
||||
self.assertEqual(form.errors, {'field': ['myfile.txt']})
|
||||
|
|
Loading…
Reference in New Issue