Fixed #25841 -- Handled base array fields validation errors with params.

Thanks to Trac alias benzid-wael for the report.
This commit is contained in:
Simon Charette 2015-12-01 17:59:58 -05:00
parent 86eccdc8b6
commit 3738e4ac46
4 changed files with 129 additions and 45 deletions

View File

@ -7,8 +7,9 @@ from django.core import checks, exceptions
from django.db.models import Field, IntegerField, Transform from django.db.models import Field, IntegerField, Transform
from django.db.models.lookups import Exact, In from django.db.models.lookups import Exact, In
from django.utils import six from django.utils import six
from django.utils.translation import string_concat, ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from ..utils import prefix_validation_error
from .utils import AttributeSetter from .utils import AttributeSetter
__all__ = ['ArrayField'] __all__ = ['ArrayField']
@ -133,14 +134,15 @@ class ArrayField(Field):
def validate(self, value, model_instance): def validate(self, value, model_instance):
super(ArrayField, self).validate(value, model_instance) super(ArrayField, self).validate(value, model_instance)
for i, part in enumerate(value): for index, part in enumerate(value):
try: try:
self.base_field.validate(part, model_instance) self.base_field.validate(part, model_instance)
except exceptions.ValidationError as e: except exceptions.ValidationError as error:
raise exceptions.ValidationError( raise prefix_validation_error(
string_concat(self.error_messages['item_invalid'], e.message), error,
prefix=self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
) )
if isinstance(self.base_field, ArrayField): if isinstance(self.base_field, ArrayField):
if len({len(i) for i in value}) > 1: if len({len(i) for i in value}) > 1:
@ -151,14 +153,15 @@ class ArrayField(Field):
def run_validators(self, value): def run_validators(self, value):
super(ArrayField, self).run_validators(value) super(ArrayField, self).run_validators(value)
for i, part in enumerate(value): for index, part in enumerate(value):
try: try:
self.base_field.run_validators(part) self.base_field.run_validators(part)
except exceptions.ValidationError as e: except exceptions.ValidationError as error:
raise exceptions.ValidationError( raise prefix_validation_error(
string_concat(self.error_messages['item_invalid'], ' '.join(e.messages)), error,
prefix=self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
) )
def formfield(self, **kwargs): def formfield(self, **kwargs):

View File

@ -1,4 +1,5 @@
import copy import copy
from itertools import chain
from django import forms from django import forms
from django.contrib.postgres.validators import ( from django.contrib.postgres.validators import (
@ -7,7 +8,9 @@ from django.contrib.postgres.validators import (
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import six from django.utils import six
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import string_concat, ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from ..utils import prefix_validation_error
class SimpleArrayField(forms.CharField): class SimpleArrayField(forms.CharField):
@ -38,16 +41,16 @@ class SimpleArrayField(forms.CharField):
items = [] items = []
errors = [] errors = []
values = [] values = []
for i, item in enumerate(items): for index, item in enumerate(items):
try: try:
values.append(self.base_field.to_python(item)) values.append(self.base_field.to_python(item))
except ValidationError as e: except ValidationError as error:
for error in e.error_list: errors.append(prefix_validation_error(
errors.append(ValidationError( error,
string_concat(self.error_messages['item_invalid'], error.message), prefix=self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
)) ))
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
return values return values
@ -55,32 +58,32 @@ class SimpleArrayField(forms.CharField):
def validate(self, value): def validate(self, value):
super(SimpleArrayField, self).validate(value) super(SimpleArrayField, self).validate(value)
errors = [] errors = []
for i, item in enumerate(value): for index, item in enumerate(value):
try: try:
self.base_field.validate(item) self.base_field.validate(item)
except ValidationError as e: except ValidationError as error:
for error in e.error_list: errors.append(prefix_validation_error(
errors.append(ValidationError( error,
string_concat(self.error_messages['item_invalid'], error.message), prefix=self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
)) ))
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
def run_validators(self, value): def run_validators(self, value):
super(SimpleArrayField, self).run_validators(value) super(SimpleArrayField, self).run_validators(value)
errors = [] errors = []
for i, item in enumerate(value): for index, item in enumerate(value):
try: try:
self.base_field.run_validators(item) self.base_field.run_validators(item)
except ValidationError as e: except ValidationError as error:
for error in e.error_list: errors.append(prefix_validation_error(
errors.append(ValidationError( error,
string_concat(self.error_messages['item_invalid'], error.message), prefix=self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
)) ))
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
@ -159,18 +162,20 @@ class SplitArrayField(forms.Field):
if not any(value) and self.required: if not any(value) and self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
max_size = max(self.size, len(value)) max_size = max(self.size, len(value))
for i in range(max_size): for index in range(max_size):
item = value[i] item = value[index]
try: try:
cleaned_data.append(self.base_field.clean(item)) cleaned_data.append(self.base_field.clean(item))
errors.append(None)
except ValidationError as error: except ValidationError as error:
errors.append(ValidationError( errors.append(prefix_validation_error(
string_concat(self.error_messages['item_invalid'], ' '.join(error.messages)), error,
self.error_messages['item_invalid'],
code='item_invalid', code='item_invalid',
params={'nth': i}, params={'nth': index},
)) ))
cleaned_data.append(None) cleaned_data.append(None)
else:
errors.append(None)
if self.remove_trailing_nulls: if self.remove_trailing_nulls:
null_index = None null_index = None
for i, value in reversed(list(enumerate(cleaned_data))): for i, value in reversed(list(enumerate(cleaned_data))):
@ -183,5 +188,5 @@ class SplitArrayField(forms.Field):
errors = errors[:null_index] errors = errors[:null_index]
errors = list(filter(None, errors)) errors = list(filter(None, errors))
if errors: if errors:
raise ValidationError(errors) raise ValidationError(list(chain.from_iterable(errors)))
return cleaned_data return cleaned_data

View File

@ -0,0 +1,30 @@
from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.utils.functional import SimpleLazyObject
from django.utils.translation import string_concat
def prefix_validation_error(error, prefix, code, params):
"""
Prefix a validation error message while maintaining the existing
validation data structure.
"""
if error.error_list == [error]:
error_params = error.params or {}
return ValidationError(
# We can't simply concatenate messages since they might require
# their associated parameters to be expressed correctly which
# is not something `string_concat` does. For example, proxied
# ungettext calls require a count parameter and are converted
# to an empty string if they are missing it.
message=string_concat(
SimpleLazyObject(lambda: prefix % params),
SimpleLazyObject(lambda: error.message % error_params),
),
code=code,
params=dict(error_params, **params),
)
return ValidationError([
prefix_validation_error(e, prefix, code, params) for e in error.error_list
])

View File

@ -507,16 +507,32 @@ class TestValidation(PostgreSQLTestCase):
self.assertEqual(cm.exception.code, 'nested_array_mismatch') self.assertEqual(cm.exception.code, 'nested_array_mismatch')
self.assertEqual(cm.exception.messages[0], 'Nested arrays must have the same length.') self.assertEqual(cm.exception.messages[0], 'Nested arrays must have the same length.')
def test_with_base_field_error_params(self):
field = ArrayField(models.CharField(max_length=2))
with self.assertRaises(exceptions.ValidationError) as cm:
field.clean(['abc'], None)
self.assertEqual(len(cm.exception.error_list), 1)
exception = cm.exception.error_list[0]
self.assertEqual(
exception.message,
'Item 0 in the array did not validate: Ensure this value has at most 2 characters (it has 3).'
)
self.assertEqual(exception.code, 'item_invalid')
self.assertEqual(exception.params, {'nth': 0, 'value': 'abc', 'limit_value': 2, 'show_value': 3})
def test_with_validators(self): def test_with_validators(self):
field = ArrayField(models.IntegerField(validators=[validators.MinValueValidator(1)])) field = ArrayField(models.IntegerField(validators=[validators.MinValueValidator(1)]))
field.clean([1, 2], None) field.clean([1, 2], None)
with self.assertRaises(exceptions.ValidationError) as cm: with self.assertRaises(exceptions.ValidationError) as cm:
field.clean([0], None) field.clean([0], None)
self.assertEqual(cm.exception.code, 'item_invalid') self.assertEqual(len(cm.exception.error_list), 1)
exception = cm.exception.error_list[0]
self.assertEqual( self.assertEqual(
cm.exception.messages[0], exception.message,
'Item 0 in the array did not validate: Ensure this value is greater than or equal to 1.' 'Item 0 in the array did not validate: Ensure this value is greater than or equal to 1.'
) )
self.assertEqual(exception.code, 'item_invalid')
self.assertEqual(exception.params, {'nth': 0, 'value': 0, 'limit_value': 1, 'show_value': 0})
class TestSimpleFormField(PostgreSQLTestCase): class TestSimpleFormField(PostgreSQLTestCase):
@ -538,6 +554,27 @@ class TestSimpleFormField(PostgreSQLTestCase):
field.clean('a,b,') field.clean('a,b,')
self.assertEqual(cm.exception.messages[0], 'Item 2 in the array did not validate: This field is required.') self.assertEqual(cm.exception.messages[0], 'Item 2 in the array did not validate: This field is required.')
def test_validate_fail_base_field_error_params(self):
field = SimpleArrayField(forms.CharField(max_length=2))
with self.assertRaises(exceptions.ValidationError) as cm:
field.clean('abc,c,defg')
errors = cm.exception.error_list
self.assertEqual(len(errors), 2)
first_error = errors[0]
self.assertEqual(
first_error.message,
'Item 0 in the array did not validate: Ensure this value has at most 2 characters (it has 3).'
)
self.assertEqual(first_error.code, 'item_invalid')
self.assertEqual(first_error.params, {'nth': 0, 'value': 'abc', 'limit_value': 2, 'show_value': 3})
second_error = errors[1]
self.assertEqual(
second_error.message,
'Item 2 in the array did not validate: Ensure this value has at most 2 characters (it has 4).'
)
self.assertEqual(second_error.code, 'item_invalid')
self.assertEqual(second_error.params, {'nth': 2, 'value': 'defg', 'limit_value': 2, 'show_value': 4})
def test_validators_fail(self): def test_validators_fail(self):
field = SimpleArrayField(forms.RegexField('[a-e]{2}')) field = SimpleArrayField(forms.RegexField('[a-e]{2}'))
with self.assertRaises(exceptions.ValidationError) as cm: with self.assertRaises(exceptions.ValidationError) as cm:
@ -648,3 +685,12 @@ class TestSplitFormField(PostgreSQLTestCase):
</td> </td>
</tr> </tr>
''') ''')
def test_invalid_char_length(self):
field = SplitArrayField(forms.CharField(max_length=2), size=3)
with self.assertRaises(exceptions.ValidationError) as cm:
field.clean(['abc', 'c', 'defg'])
self.assertEqual(cm.exception.messages, [
'Item 0 in the array did not validate: Ensure this value has at most 2 characters (it has 3).',
'Item 2 in the array did not validate: Ensure this value has at most 2 characters (it has 4).',
])