Commit 3738e4ac authored by Simon Charette's avatar Simon Charette
Browse files

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

Thanks to Trac alias benzid-wael for the report.
parent 86eccdc8
Loading
Loading
Loading
Loading
+14 −11
Original line number Diff line number Diff line
@@ -7,8 +7,9 @@ from django.core import checks, exceptions
from django.db.models import Field, IntegerField, Transform
from django.db.models.lookups import Exact, In
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

__all__ = ['ArrayField']
@@ -133,14 +134,15 @@ class ArrayField(Field):

    def validate(self, value, model_instance):
        super(ArrayField, self).validate(value, model_instance)
        for i, part in enumerate(value):
        for index, part in enumerate(value):
            try:
                self.base_field.validate(part, model_instance)
            except exceptions.ValidationError as e:
                raise exceptions.ValidationError(
                    string_concat(self.error_messages['item_invalid'], e.message),
            except exceptions.ValidationError as error:
                raise prefix_validation_error(
                    error,
                    prefix=self.error_messages['item_invalid'],
                    code='item_invalid',
                    params={'nth': i},
                    params={'nth': index},
                )
        if isinstance(self.base_field, ArrayField):
            if len({len(i) for i in value}) > 1:
@@ -151,14 +153,15 @@ class ArrayField(Field):

    def run_validators(self, value):
        super(ArrayField, self).run_validators(value)
        for i, part in enumerate(value):
        for index, part in enumerate(value):
            try:
                self.base_field.run_validators(part)
            except exceptions.ValidationError as e:
                raise exceptions.ValidationError(
                    string_concat(self.error_messages['item_invalid'], ' '.join(e.messages)),
            except exceptions.ValidationError as error:
                raise prefix_validation_error(
                    error,
                    prefix=self.error_messages['item_invalid'],
                    code='item_invalid',
                    params={'nth': i},
                    params={'nth': index},
                )

    def formfield(self, **kwargs):
+37 −32
Original line number Diff line number Diff line
import copy
from itertools import chain

from django import forms
from django.contrib.postgres.validators import (
@@ -7,7 +8,9 @@ from django.contrib.postgres.validators import (
from django.core.exceptions import ValidationError
from django.utils import six
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):
@@ -38,15 +41,15 @@ class SimpleArrayField(forms.CharField):
            items = []
        errors = []
        values = []
        for i, item in enumerate(items):
        for index, item in enumerate(items):
            try:
                values.append(self.base_field.to_python(item))
            except ValidationError as e:
                for error in e.error_list:
                    errors.append(ValidationError(
                        string_concat(self.error_messages['item_invalid'], error.message),
            except ValidationError as error:
                errors.append(prefix_validation_error(
                    error,
                    prefix=self.error_messages['item_invalid'],
                    code='item_invalid',
                        params={'nth': i},
                    params={'nth': index},
                ))
        if errors:
            raise ValidationError(errors)
@@ -55,15 +58,15 @@ class SimpleArrayField(forms.CharField):
    def validate(self, value):
        super(SimpleArrayField, self).validate(value)
        errors = []
        for i, item in enumerate(value):
        for index, item in enumerate(value):
            try:
                self.base_field.validate(item)
            except ValidationError as e:
                for error in e.error_list:
                    errors.append(ValidationError(
                        string_concat(self.error_messages['item_invalid'], error.message),
            except ValidationError as error:
                errors.append(prefix_validation_error(
                    error,
                    prefix=self.error_messages['item_invalid'],
                    code='item_invalid',
                        params={'nth': i},
                    params={'nth': index},
                ))
        if errors:
            raise ValidationError(errors)
@@ -71,15 +74,15 @@ class SimpleArrayField(forms.CharField):
    def run_validators(self, value):
        super(SimpleArrayField, self).run_validators(value)
        errors = []
        for i, item in enumerate(value):
        for index, item in enumerate(value):
            try:
                self.base_field.run_validators(item)
            except ValidationError as e:
                for error in e.error_list:
                    errors.append(ValidationError(
                        string_concat(self.error_messages['item_invalid'], error.message),
            except ValidationError as error:
                errors.append(prefix_validation_error(
                    error,
                    prefix=self.error_messages['item_invalid'],
                    code='item_invalid',
                        params={'nth': i},
                    params={'nth': index},
                ))
        if errors:
            raise ValidationError(errors)
@@ -159,18 +162,20 @@ class SplitArrayField(forms.Field):
        if not any(value) and self.required:
            raise ValidationError(self.error_messages['required'])
        max_size = max(self.size, len(value))
        for i in range(max_size):
            item = value[i]
        for index in range(max_size):
            item = value[index]
            try:
                cleaned_data.append(self.base_field.clean(item))
                errors.append(None)
            except ValidationError as error:
                errors.append(ValidationError(
                    string_concat(self.error_messages['item_invalid'], ' '.join(error.messages)),
                errors.append(prefix_validation_error(
                    error,
                    self.error_messages['item_invalid'],
                    code='item_invalid',
                    params={'nth': i},
                    params={'nth': index},
                ))
                cleaned_data.append(None)
            else:
                errors.append(None)
        if self.remove_trailing_nulls:
            null_index = None
            for i, value in reversed(list(enumerate(cleaned_data))):
@@ -183,5 +188,5 @@ class SplitArrayField(forms.Field):
                errors = errors[:null_index]
        errors = list(filter(None, errors))
        if errors:
            raise ValidationError(errors)
            raise ValidationError(list(chain.from_iterable(errors)))
        return cleaned_data
+30 −0
Original line number Diff line number Diff line
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
    ])
+48 −2
Original line number Diff line number Diff line
@@ -507,16 +507,32 @@ class TestValidation(PostgreSQLTestCase):
        self.assertEqual(cm.exception.code, 'nested_array_mismatch')
        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):
        field = ArrayField(models.IntegerField(validators=[validators.MinValueValidator(1)]))
        field.clean([1, 2], None)
        with self.assertRaises(exceptions.ValidationError) as cm:
            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(
            cm.exception.messages[0],
            exception.message,
            '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):
@@ -538,6 +554,27 @@ class TestSimpleFormField(PostgreSQLTestCase):
            field.clean('a,b,')
        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):
        field = SimpleArrayField(forms.RegexField('[a-e]{2}'))
        with self.assertRaises(exceptions.ValidationError) as cm:
@@ -648,3 +685,12 @@ class TestSplitFormField(PostgreSQLTestCase):
                </td>
            </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).',
        ])