Commit f563c339 authored by Loic Bistuer's avatar Loic Bistuer
Browse files

Fixed #20867 -- Added the Form.add_error() method.

Refs #20199 #16986.

Thanks @akaariai, @bmispelon, @mjtamlyn, @timgraham for the reviews.
parent 7e2d61a9
Loading
Loading
Loading
Loading
+52 −38
Original line number Diff line number Diff line
@@ -77,64 +77,78 @@ class ValidationError(Exception):
    """An error while validating data."""
    def __init__(self, message, code=None, params=None):
        """
        ValidationError can be passed any object that can be printed (usually
        a string), a list of objects or a dictionary.
        The `message` argument can be a single error, a list of errors, or a
        dictionary that maps field names to lists of errors. What we define as
        an "error" can be either a simple string or an instance of
        ValidationError with its message attribute set, and what we define as
        list or dictionary can be an actual `list` or `dict` or an instance
        of ValidationError with its `error_list` or `error_dict` attribute set.
        """
        if isinstance(message, ValidationError):
            if hasattr(message, 'error_dict'):
                message = message.error_dict
            elif not hasattr(message, 'message'):
                message = message.error_list
            else:
                message, code, params = message.message, message.code, message.params

        if isinstance(message, dict):
            self.error_dict = message
            self.error_dict = {}
            for field, messages in message.items():
                if not isinstance(messages, ValidationError):
                    messages = ValidationError(messages)
                self.error_dict[field] = messages.error_list

        elif isinstance(message, list):
            self.error_list = message
            self.error_list = []
            for message in message:
                # Normalize plain strings to instances of ValidationError.
                if not isinstance(message, ValidationError):
                    message = ValidationError(message)
                self.error_list.extend(message.error_list)

        else:
            self.message = message
            self.code = code
            self.params = params
            self.message = message
            self.error_list = [self]

    @property
    def message_dict(self):
        message_dict = {}
        for field, messages in self.error_dict.items():
            message_dict[field] = []
            for message in messages:
                if isinstance(message, ValidationError):
                    message_dict[field].extend(message.messages)
                else:
                    message_dict[field].append(force_text(message))
        return message_dict
        return dict(self)

    @property
    def messages(self):
        if hasattr(self, 'error_dict'):
            message_list = reduce(operator.add, self.error_dict.values())
        else:
            message_list = self.error_list

        messages = []
        for message in message_list:
            if isinstance(message, ValidationError):
                params = message.params
                message = message.message
                if params:
                    message %= params
            message = force_text(message)
            messages.append(message)
        return messages

    def __str__(self):
        if hasattr(self, 'error_dict'):
            return repr(self.message_dict)
        return repr(self.messages)

    def __repr__(self):
        return 'ValidationError(%s)' % self
            return reduce(operator.add, dict(self).values())
        return list(self)

    def update_error_dict(self, error_dict):
        if hasattr(self, 'error_dict'):
            if error_dict:
                for k, v in self.error_dict.items():
                    error_dict.setdefault(k, []).extend(v)
                for field, errors in self.error_dict.items():
                    error_dict.setdefault(field, []).extend(errors)
            else:
                error_dict = self.error_dict
        else:
            error_dict[NON_FIELD_ERRORS] = self.error_list
        return error_dict

    def __iter__(self):
        if hasattr(self, 'error_dict'):
            for field, errors in self.error_dict.items():
                yield field, list(ValidationError(errors))
        else:
            for error in self.error_list:
                message = error.message
                if error.params:
                    message %= error.params
                yield force_text(message)

    def __str__(self):
        if hasattr(self, 'error_dict'):
            return repr(dict(self))
        return repr(list(self))

    def __repr__(self):
        return 'ValidationError(%s)' % self
+1 −1
Original line number Diff line number Diff line
@@ -987,7 +987,7 @@ class Model(six.with_metaclass(ModelBase)):

    def clean_fields(self, exclude=None):
        """
        Cleans all fields and raises a ValidationError containing message_dict
        Cleans all fields and raises a ValidationError containing a dict
        of all validation errors if any occur.
        """
        if exclude is None:
+48 −4
Original line number Diff line number Diff line
@@ -290,6 +290,51 @@ class BaseForm(object):
        prefix = self.add_prefix(fieldname)
        return field.widget.value_from_datadict(self.data, self.files, prefix)

    def add_error(self, field, error):
        """
        Update the content of `self._errors`.

        The `field` argument is the name of the field to which the errors
        should be added. If its value is None the errors will be treated as
        NON_FIELD_ERRORS.

        The `error` argument can be a single error, a list of errors, or a
        dictionary that maps field names to lists of errors. What we define as
        an "error" can be either a simple string or an instance of
        ValidationError with its message attribute set and what we define as
        list or dictionary can be an actual `list` or `dict` or an instance
        of ValidationError with its `error_list` or `error_dict` attribute set.

        If `error` is a dictionary, the `field` argument *must* be None and
        errors will be added to the fields that correspond to the keys of the
        dictionary.
        """
        if not isinstance(error, ValidationError):
            # Normalize to ValidationError and let its constructor
            # do the hard work of making sense of the input.
            error = ValidationError(error)

        if hasattr(error, 'error_dict'):
            if field is not None:
                raise TypeError(
                    "The argument `field` must be `None` when the `error` "
                    "argument contains errors for multiple fields."
                )
            else:
                error = dict(error)
        else:
            error = {field or NON_FIELD_ERRORS: list(error)}

        for field, error_list in error.items():
            if field not in self.errors:
                if field != NON_FIELD_ERRORS and field not in self.fields:
                    raise ValueError(
                        "'%s' has no field named '%s'." % (self.__class__.__name__, field))
                self._errors[field] = self.error_class()
            self._errors[field].extend(error_list)
            if field in self.cleaned_data:
                del self.cleaned_data[field]

    def full_clean(self):
        """
        Cleans all of self.data and populates self._errors and
@@ -303,6 +348,7 @@ class BaseForm(object):
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        self._post_clean()
@@ -324,15 +370,13 @@ class BaseForm(object):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self._errors[name] = self.error_class(e.messages)
                if name in self.cleaned_data:
                    del self.cleaned_data[name]
                self.add_error(name, e)

    def _clean_form(self):
        try:
            cleaned_data = self.clean()
        except ValidationError as e:
            self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
            self.add_error(None, e)
        else:
            if cleaned_data is not None:
                self.cleaned_data = cleaned_data
+18 −25
Original line number Diff line number Diff line
@@ -326,27 +326,6 @@ class BaseModelForm(BaseForm):
        super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
                                            error_class, label_suffix, empty_permitted)

    def _update_errors(self, errors):
        for field, messages in errors.error_dict.items():
            if field not in self.fields:
                continue
            field = self.fields[field]
            for message in messages:
                if isinstance(message, ValidationError):
                    if message.code in field.error_messages:
                        message.message = field.error_messages[message.code]

        message_dict = errors.message_dict
        for k, v in message_dict.items():
            if k != NON_FIELD_ERRORS:
                self._errors.setdefault(k, self.error_class()).extend(v)
                # Remove the data from the cleaned_data dict since it was invalid
                if k in self.cleaned_data:
                    del self.cleaned_data[k]
        if NON_FIELD_ERRORS in message_dict:
            messages = message_dict[NON_FIELD_ERRORS]
            self._errors.setdefault(NON_FIELD_ERRORS, self.error_class()).extend(messages)

    def _get_validation_exclusions(self):
        """
        For backwards-compatibility, several types of fields need to be
@@ -393,6 +372,20 @@ class BaseModelForm(BaseForm):
        self._validate_unique = True
        return self.cleaned_data

    def _update_errors(self, errors):
        # Override any validation error messages defined at the model level
        # with those defined on the form fields.
        for field, messages in errors.error_dict.items():
            if field not in self.fields:
                continue
            field = self.fields[field]
            for message in messages:
                if (isinstance(message, ValidationError) and
                        message.code in field.error_messages):
                    message.message = field.error_messages[message.code]

        self.add_error(None, errors)

    def _post_clean(self):
        opts = self._meta
        # Update the model instance with self.cleaned_data.
@@ -407,13 +400,12 @@ class BaseModelForm(BaseForm):
        # object being referred to may not yet fully exist (#12749).
        # However, these fields *must* be included in uniqueness checks,
        # so this can't be part of _get_validation_exclusions().
        for f_name, field in self.fields.items():
        for name, field in self.fields.items():
            if isinstance(field, InlineForeignKeyField):
                exclude.append(f_name)
                exclude.append(name)

        try:
            self.instance.full_clean(exclude=exclude,
                validate_unique=False)
            self.instance.full_clean(exclude=exclude, validate_unique=False)
        except ValidationError as e:
            self._update_errors(e)

@@ -695,6 +687,7 @@ class BaseModelFormSet(BaseFormSet):
                        del form.cleaned_data[field]
                    # mark the data as seen
                    seen_data.add(data)

        if errors:
            raise ValidationError(errors)

+20 −0
Original line number Diff line number Diff line
@@ -117,6 +117,26 @@ The validation routines will only get called once, regardless of how many times
you access :attr:`~Form.errors` or call :meth:`~Form.is_valid`. This means that
if validation has side effects, those side effects will only be triggered once.

.. method:: Form.add_error(field, error)

.. versionadded:: 1.7

This method allows adding errors to specific fields from within the
``Form.clean()`` method, or from outside the form altogether; for instance
from a view. This is a better alternative to fiddling directly with
``Form._errors`` as described in :ref:`modifying-field-errors`.

The ``field`` argument is the name of the field to which the errors
should be added. If its value is ``None`` the error will be treated as
a non-field error as returned by ``Form.non_field_errors()``.

The ``error`` argument can be a simple string, or preferably an instance of
``ValidationError``. See :ref:`raising-validation-error` for best practices
when defining form errors.

Note that ``Form.add_error()`` automatically removes the relevant field from
``cleaned_data``.

Behavior of unbound forms
~~~~~~~~~~~~~~~~~~~~~~~~~

Loading