Browse Source

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

Jon Dufresne 4 years ago
parent
commit
83fbaa9231
3 changed files with 85 additions and 19 deletions
  1. 19 18
      django/core/validators.py
  2. 3 1
      docs/releases/3.2.txt
  3. 63 0
      tests/forms_tests/tests/test_validators.py

+ 19 - 18
django/core/validators.py

@@ -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 (

+ 3 - 1
docs/releases/3.2.txt

@@ -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:
 

+ 63 - 0
tests/forms_tests/tests/test_validators.py

@@ -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']})