Fixed #31806 -- Made validators include the value in ValidationErrors.

This commit is contained in:
Jon Dufresne 2020-07-20 16:11:23 -07:00 committed by Mariusz Felisiak
parent 013e06bb37
commit 83fbaa9231
3 changed files with 85 additions and 19 deletions

View File

@ -48,7 +48,7 @@ class RegexValidator:
regex_matches = self.regex.search(str(value)) regex_matches = self.regex.search(str(value))
invalid_input = regex_matches if self.inverse_match else not regex_matches invalid_input = regex_matches if self.inverse_match else not regex_matches
if invalid_input: if invalid_input:
raise ValidationError(self.message, code=self.code) raise ValidationError(self.message, code=self.code, params={'value': value})
def __eq__(self, other): def __eq__(self, other):
return ( return (
@ -100,11 +100,11 @@ class URLValidator(RegexValidator):
def __call__(self, value): def __call__(self, value):
if not isinstance(value, str): 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. # Check if the scheme is valid.
scheme = value.split('://')[0].lower() scheme = value.split('://')[0].lower()
if scheme not in self.schemes: 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 # Then check full URL
try: try:
@ -115,7 +115,7 @@ class URLValidator(RegexValidator):
try: try:
scheme, netloc, path, query, fragment = urlsplit(value) scheme, netloc, path, query, fragment = urlsplit(value)
except ValueError: # for example, "Invalid IPv6 URL" 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: try:
netloc = punycode(netloc) # IDN -> ACE netloc = punycode(netloc) # IDN -> ACE
except UnicodeError: # invalid domain part except UnicodeError: # invalid domain part
@ -132,14 +132,14 @@ class URLValidator(RegexValidator):
try: try:
validate_ipv6_address(potential_ip) validate_ipv6_address(potential_ip)
except ValidationError: 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 # 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 # 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 # one byte for the length of the name and one byte for the trailing dot
# that's used to indicate absolute names in DNS. # that's used to indicate absolute names in DNS.
if len(urlsplit(value).netloc) > 253: 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( integer_validator = RegexValidator(
@ -208,12 +208,12 @@ class EmailValidator:
def __call__(self, value): def __call__(self, value):
if not value or '@' not in 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) user_part, domain_part = value.rsplit('@', 1)
if not self.user_regex.match(user_part): 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 if (domain_part not in self.domain_allowlist and
not self.validate_domain_part(domain_part)): not self.validate_domain_part(domain_part)):
@ -225,7 +225,7 @@ class EmailValidator:
else: else:
if self.validate_domain_part(domain_part): if self.validate_domain_part(domain_part):
return 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): def validate_domain_part(self, domain_part):
if self.domain_regex.match(domain_part): if self.domain_regex.match(domain_part):
@ -272,12 +272,12 @@ def validate_ipv4_address(value):
try: try:
ipaddress.IPv4Address(value) ipaddress.IPv4Address(value)
except ValueError: 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): def validate_ipv6_address(value):
if not is_valid_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): def validate_ipv46_address(value):
@ -287,7 +287,7 @@ def validate_ipv46_address(value):
try: try:
validate_ipv6_address(value) validate_ipv6_address(value)
except ValidationError: 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 = { ip_address_validator_map = {
@ -438,7 +438,7 @@ class DecimalValidator:
def __call__(self, value): def __call__(self, value):
digit_tuple, exponent = value.as_tuple()[1:] digit_tuple, exponent = value.as_tuple()[1:]
if exponent in {'F', 'n', 'N'}: 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: if exponent >= 0:
# A positive exponent adds that many trailing zeros. # A positive exponent adds that many trailing zeros.
digits = len(digit_tuple) + exponent digits = len(digit_tuple) + exponent
@ -460,20 +460,20 @@ class DecimalValidator:
raise ValidationError( raise ValidationError(
self.messages['max_digits'], self.messages['max_digits'],
code='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: if self.decimal_places is not None and decimals > self.decimal_places:
raise ValidationError( raise ValidationError(
self.messages['max_decimal_places'], self.messages['max_decimal_places'],
code='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 if (self.max_digits is not None and self.decimal_places is not None and
whole_digits > (self.max_digits - self.decimal_places)): whole_digits > (self.max_digits - self.decimal_places)):
raise ValidationError( raise ValidationError(
self.messages['max_whole_digits'], self.messages['max_whole_digits'],
code='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): def __eq__(self, other):
@ -509,7 +509,8 @@ class FileExtensionValidator:
code=self.code, code=self.code,
params={ params={
'extension': extension, '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): def __call__(self, value):
if '\x00' in str(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): def __eq__(self, other):
return ( return (

View File

@ -327,7 +327,9 @@ Utilities
Validators 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: .. _backwards-incompatible-3.2:

View File

@ -5,6 +5,7 @@ from unittest import TestCase
from django import forms from django import forms
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
class TestFieldWithValidators(TestCase): class TestFieldWithValidators(TestCase):
@ -68,8 +69,28 @@ class TestFieldWithValidators(TestCase):
class ValidatorCustomMessageTests(TestCase): class ValidatorCustomMessageTests(TestCase):
def test_value_placeholder_with_char_field(self): def test_value_placeholder_with_char_field(self):
cases = [ 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.MaxLengthValidator(10), 11 * 'x', 'max_length'),
(validators.MinLengthValidator(10), 9 * 'x', 'min_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: for validator, value, code in cases:
if isinstance(validator, types.FunctionType): if isinstance(validator, types.FunctionType):
@ -87,10 +108,21 @@ class ValidatorCustomMessageTests(TestCase):
self.assertIs(form.is_valid(), False) self.assertIs(form.is_valid(), False)
self.assertEqual(form.errors, {'field': [value]}) 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): def test_value_placeholder_with_integer_field(self):
cases = [ cases = [
(validators.MaxValueValidator(0), 1, 'max_value'), (validators.MaxValueValidator(0), 1, 'max_value'),
(validators.MinValueValidator(0), -1, 'min_value'), (validators.MinValueValidator(0), -1, 'min_value'),
(validators.URLValidator(), '1', 'invalid'),
] ]
for validator, value, code in cases: for validator, value, code in cases:
with self.subTest(type(validator).__name__, value=value): with self.subTest(type(validator).__name__, value=value):
@ -103,3 +135,34 @@ class ValidatorCustomMessageTests(TestCase):
form = MyForm({'field': value}) form = MyForm({'field': value})
self.assertIs(form.is_valid(), False) self.assertIs(form.is_valid(), False)
self.assertEqual(form.errors, {'field': [str(value)]}) 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']})