Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

Refs #20199 #16986.

Thanks @akaariai, @bmispelon, @mjtamlyn, @timgraham for the reviews.
  • Loading branch information...
commit f563c339ca2eed81706ab17726c79a6f00d7c553 1 parent 7e2d61a
Loic Bistuer authored November 12, 2013
90  django/core/exceptions.py
@@ -77,64 +77,78 @@ class ValidationError(Exception):
77 77
     """An error while validating data."""
78 78
     def __init__(self, message, code=None, params=None):
79 79
         """
80  
-        ValidationError can be passed any object that can be printed (usually
81  
-        a string), a list of objects or a dictionary.
  80
+        The `message` argument can be a single error, a list of errors, or a
  81
+        dictionary that maps field names to lists of errors. What we define as
  82
+        an "error" can be either a simple string or an instance of
  83
+        ValidationError with its message attribute set, and what we define as
  84
+        list or dictionary can be an actual `list` or `dict` or an instance
  85
+        of ValidationError with its `error_list` or `error_dict` attribute set.
82 86
         """
  87
+        if isinstance(message, ValidationError):
  88
+            if hasattr(message, 'error_dict'):
  89
+                message = message.error_dict
  90
+            elif not hasattr(message, 'message'):
  91
+                message = message.error_list
  92
+            else:
  93
+                message, code, params = message.message, message.code, message.params
  94
+
83 95
         if isinstance(message, dict):
84  
-            self.error_dict = message
  96
+            self.error_dict = {}
  97
+            for field, messages in message.items():
  98
+                if not isinstance(messages, ValidationError):
  99
+                    messages = ValidationError(messages)
  100
+                self.error_dict[field] = messages.error_list
  101
+
85 102
         elif isinstance(message, list):
86  
-            self.error_list = message
  103
+            self.error_list = []
  104
+            for message in message:
  105
+                # Normalize plain strings to instances of ValidationError.
  106
+                if not isinstance(message, ValidationError):
  107
+                    message = ValidationError(message)
  108
+                self.error_list.extend(message.error_list)
  109
+
87 110
         else:
  111
+            self.message = message
88 112
             self.code = code
89 113
             self.params = params
90  
-            self.message = message
91 114
             self.error_list = [self]
92 115
 
93 116
     @property
94 117
     def message_dict(self):
95  
-        message_dict = {}
96  
-        for field, messages in self.error_dict.items():
97  
-            message_dict[field] = []
98  
-            for message in messages:
99  
-                if isinstance(message, ValidationError):
100  
-                    message_dict[field].extend(message.messages)
101  
-                else:
102  
-                    message_dict[field].append(force_text(message))
103  
-        return message_dict
  118
+        return dict(self)
104 119
 
105 120
     @property
106 121
     def messages(self):
107 122
         if hasattr(self, 'error_dict'):
108  
-            message_list = reduce(operator.add, self.error_dict.values())
109  
-        else:
110  
-            message_list = self.error_list
111  
-
112  
-        messages = []
113  
-        for message in message_list:
114  
-            if isinstance(message, ValidationError):
115  
-                params = message.params
116  
-                message = message.message
117  
-                if params:
118  
-                    message %= params
119  
-            message = force_text(message)
120  
-            messages.append(message)
121  
-        return messages
122  
-
123  
-    def __str__(self):
124  
-        if hasattr(self, 'error_dict'):
125  
-            return repr(self.message_dict)
126  
-        return repr(self.messages)
127  
-
128  
-    def __repr__(self):
129  
-        return 'ValidationError(%s)' % self
  123
+            return reduce(operator.add, dict(self).values())
  124
+        return list(self)
130 125
 
131 126
     def update_error_dict(self, error_dict):
132 127
         if hasattr(self, 'error_dict'):
133 128
             if error_dict:
134  
-                for k, v in self.error_dict.items():
135  
-                    error_dict.setdefault(k, []).extend(v)
  129
+                for field, errors in self.error_dict.items():
  130
+                    error_dict.setdefault(field, []).extend(errors)
136 131
             else:
137 132
                 error_dict = self.error_dict
138 133
         else:
139 134
             error_dict[NON_FIELD_ERRORS] = self.error_list
140 135
         return error_dict
  136
+
  137
+    def __iter__(self):
  138
+        if hasattr(self, 'error_dict'):
  139
+            for field, errors in self.error_dict.items():
  140
+                yield field, list(ValidationError(errors))
  141
+        else:
  142
+            for error in self.error_list:
  143
+                message = error.message
  144
+                if error.params:
  145
+                    message %= error.params
  146
+                yield force_text(message)
  147
+
  148
+    def __str__(self):
  149
+        if hasattr(self, 'error_dict'):
  150
+            return repr(dict(self))
  151
+        return repr(list(self))
  152
+
  153
+    def __repr__(self):
  154
+        return 'ValidationError(%s)' % self
2  django/db/models/base.py
@@ -987,7 +987,7 @@ def full_clean(self, exclude=None, validate_unique=True):
987 987
 
988 988
     def clean_fields(self, exclude=None):
989 989
         """
990  
-        Cleans all fields and raises a ValidationError containing message_dict
  990
+        Cleans all fields and raises a ValidationError containing a dict
991 991
         of all validation errors if any occur.
992 992
         """
993 993
         if exclude is None:
52  django/forms/forms.py
@@ -290,6 +290,51 @@ def _raw_value(self, fieldname):
290 290
         prefix = self.add_prefix(fieldname)
291 291
         return field.widget.value_from_datadict(self.data, self.files, prefix)
292 292
 
  293
+    def add_error(self, field, error):
  294
+        """
  295
+        Update the content of `self._errors`.
  296
+
  297
+        The `field` argument is the name of the field to which the errors
  298
+        should be added. If its value is None the errors will be treated as
  299
+        NON_FIELD_ERRORS.
  300
+
  301
+        The `error` argument can be a single error, a list of errors, or a
  302
+        dictionary that maps field names to lists of errors. What we define as
  303
+        an "error" can be either a simple string or an instance of
  304
+        ValidationError with its message attribute set and what we define as
  305
+        list or dictionary can be an actual `list` or `dict` or an instance
  306
+        of ValidationError with its `error_list` or `error_dict` attribute set.
  307
+
  308
+        If `error` is a dictionary, the `field` argument *must* be None and
  309
+        errors will be added to the fields that correspond to the keys of the
  310
+        dictionary.
  311
+        """
  312
+        if not isinstance(error, ValidationError):
  313
+            # Normalize to ValidationError and let its constructor
  314
+            # do the hard work of making sense of the input.
  315
+            error = ValidationError(error)
  316
+
  317
+        if hasattr(error, 'error_dict'):
  318
+            if field is not None:
  319
+                raise TypeError(
  320
+                    "The argument `field` must be `None` when the `error` "
  321
+                    "argument contains errors for multiple fields."
  322
+                )
  323
+            else:
  324
+                error = dict(error)
  325
+        else:
  326
+            error = {field or NON_FIELD_ERRORS: list(error)}
  327
+
  328
+        for field, error_list in error.items():
  329
+            if field not in self.errors:
  330
+                if field != NON_FIELD_ERRORS and field not in self.fields:
  331
+                    raise ValueError(
  332
+                        "'%s' has no field named '%s'." % (self.__class__.__name__, field))
  333
+                self._errors[field] = self.error_class()
  334
+            self._errors[field].extend(error_list)
  335
+            if field in self.cleaned_data:
  336
+                del self.cleaned_data[field]
  337
+
293 338
     def full_clean(self):
294 339
         """
295 340
         Cleans all of self.data and populates self._errors and
@@ -303,6 +348,7 @@ def full_clean(self):
303 348
         # changed from the initial data, short circuit any validation.
304 349
         if self.empty_permitted and not self.has_changed():
305 350
             return
  351
+
306 352
         self._clean_fields()
307 353
         self._clean_form()
308 354
         self._post_clean()
@@ -324,15 +370,13 @@ def _clean_fields(self):
324 370
                     value = getattr(self, 'clean_%s' % name)()
325 371
                     self.cleaned_data[name] = value
326 372
             except ValidationError as e:
327  
-                self._errors[name] = self.error_class(e.messages)
328  
-                if name in self.cleaned_data:
329  
-                    del self.cleaned_data[name]
  373
+                self.add_error(name, e)
330 374
 
331 375
     def _clean_form(self):
332 376
         try:
333 377
             cleaned_data = self.clean()
334 378
         except ValidationError as e:
335  
-            self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
  379
+            self.add_error(None, e)
336 380
         else:
337 381
             if cleaned_data is not None:
338 382
                 self.cleaned_data = cleaned_data
43  django/forms/models.py
@@ -326,27 +326,6 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
326 326
         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
327 327
                                             error_class, label_suffix, empty_permitted)
328 328
 
329  
-    def _update_errors(self, errors):
330  
-        for field, messages in errors.error_dict.items():
331  
-            if field not in self.fields:
332  
-                continue
333  
-            field = self.fields[field]
334  
-            for message in messages:
335  
-                if isinstance(message, ValidationError):
336  
-                    if message.code in field.error_messages:
337  
-                        message.message = field.error_messages[message.code]
338  
-
339  
-        message_dict = errors.message_dict
340  
-        for k, v in message_dict.items():
341  
-            if k != NON_FIELD_ERRORS:
342  
-                self._errors.setdefault(k, self.error_class()).extend(v)
343  
-                # Remove the data from the cleaned_data dict since it was invalid
344  
-                if k in self.cleaned_data:
345  
-                    del self.cleaned_data[k]
346  
-        if NON_FIELD_ERRORS in message_dict:
347  
-            messages = message_dict[NON_FIELD_ERRORS]
348  
-            self._errors.setdefault(NON_FIELD_ERRORS, self.error_class()).extend(messages)
349  
-
350 329
     def _get_validation_exclusions(self):
351 330
         """
352 331
         For backwards-compatibility, several types of fields need to be
@@ -393,6 +372,20 @@ def clean(self):
393 372
         self._validate_unique = True
394 373
         return self.cleaned_data
395 374
 
  375
+    def _update_errors(self, errors):
  376
+        # Override any validation error messages defined at the model level
  377
+        # with those defined on the form fields.
  378
+        for field, messages in errors.error_dict.items():
  379
+            if field not in self.fields:
  380
+                continue
  381
+            field = self.fields[field]
  382
+            for message in messages:
  383
+                if (isinstance(message, ValidationError) and
  384
+                        message.code in field.error_messages):
  385
+                    message.message = field.error_messages[message.code]
  386
+
  387
+        self.add_error(None, errors)
  388
+
396 389
     def _post_clean(self):
397 390
         opts = self._meta
398 391
         # Update the model instance with self.cleaned_data.
@@ -407,13 +400,12 @@ def _post_clean(self):
407 400
         # object being referred to may not yet fully exist (#12749).
408 401
         # However, these fields *must* be included in uniqueness checks,
409 402
         # so this can't be part of _get_validation_exclusions().
410  
-        for f_name, field in self.fields.items():
  403
+        for name, field in self.fields.items():
411 404
             if isinstance(field, InlineForeignKeyField):
412  
-                exclude.append(f_name)
  405
+                exclude.append(name)
413 406
 
414 407
         try:
415  
-            self.instance.full_clean(exclude=exclude,
416  
-                validate_unique=False)
  408
+            self.instance.full_clean(exclude=exclude, validate_unique=False)
417 409
         except ValidationError as e:
418 410
             self._update_errors(e)
419 411
 
@@ -695,6 +687,7 @@ def validate_unique(self):
695 687
                         del form.cleaned_data[field]
696 688
                     # mark the data as seen
697 689
                     seen_data.add(data)
  690
+
698 691
         if errors:
699 692
             raise ValidationError(errors)
700 693
 
20  docs/ref/forms/api.txt
@@ -117,6 +117,26 @@ The validation routines will only get called once, regardless of how many times
117 117
 you access :attr:`~Form.errors` or call :meth:`~Form.is_valid`. This means that
118 118
 if validation has side effects, those side effects will only be triggered once.
119 119
 
  120
+.. method:: Form.add_error(field, error)
  121
+
  122
+.. versionadded:: 1.7
  123
+
  124
+This method allows adding errors to specific fields from within the
  125
+``Form.clean()`` method, or from outside the form altogether; for instance
  126
+from a view. This is a better alternative to fiddling directly with
  127
+``Form._errors`` as described in :ref:`modifying-field-errors`.
  128
+
  129
+The ``field`` argument is the name of the field to which the errors
  130
+should be added. If its value is ``None`` the error will be treated as
  131
+a non-field error as returned by ``Form.non_field_errors()``.
  132
+
  133
+The ``error`` argument can be a simple string, or preferably an instance of
  134
+``ValidationError``. See :ref:`raising-validation-error` for best practices
  135
+when defining form errors.
  136
+
  137
+Note that ``Form.add_error()`` automatically removes the relevant field from
  138
+``cleaned_data``.
  139
+
120 140
 Behavior of unbound forms
121 141
 ~~~~~~~~~~~~~~~~~~~~~~~~~
122 142
 
28  docs/ref/forms/validation.txt
@@ -464,3 +464,31 @@ Secondly, once we have decided that the combined data in the two fields we are
464 464
 considering aren't valid, we must remember to remove them from the
465 465
 ``cleaned_data``. `cleaned_data`` is present even if the form doesn't
466 466
 validate, but it contains only field values that did validate.
  467
+
  468
+.. versionchanged:: 1.7
  469
+
  470
+In lieu of manipulating ``_errors`` directly, it's now possible to add errors
  471
+to specific fields with :meth:`django.forms.Form.add_error()`::
  472
+
  473
+    from django import forms
  474
+
  475
+    class ContactForm(forms.Form):
  476
+        # Everything as before.
  477
+        ...
  478
+
  479
+        def clean(self):
  480
+            cleaned_data = super(ContactForm, self).clean()
  481
+            cc_myself = cleaned_data.get("cc_myself")
  482
+            subject = cleaned_data.get("subject")
  483
+
  484
+            if cc_myself and subject and "help" not in subject:
  485
+                msg = u"Must put 'help' in subject when cc'ing yourself."
  486
+                self.add_error('cc_myself', msg)
  487
+                self.add_error('subject', msg)
  488
+
  489
+The second argument of ``add_error()`` can be a simple string, or preferably
  490
+an instance of ``ValidationError``. See :ref:`raising-validation-error` for
  491
+more details.
  492
+
  493
+Unlike the ``_errors`` approach, ``add_error()` automatically removes the field
  494
+from ``cleaned_data``.
3  docs/releases/1.7.txt
@@ -350,6 +350,9 @@ Forms
350 350
 * It's now possible to opt-out from a ``Form`` field declared in a parent class
351 351
   by shadowing it with a non-``Field`` value.
352 352
 
  353
+* The new :meth:`~django.forms.Form.add_error()` method allows adding errors
  354
+  to specific form fields.
  355
+
353 356
 Internationalization
354 357
 ^^^^^^^^^^^^^^^^^^^^
355 358
 
50  tests/forms_tests/tests/test_forms.py
@@ -657,25 +657,49 @@ def clean_password2(self):
657 657
         self.assertEqual(f.cleaned_data['password2'], 'foo')
658 658
 
659 659
         # Another way of doing multiple-field validation is by implementing the
660  
-        # Form's clean() method. If you do this, any ValidationError raised by that
661  
-        # method will not be associated with a particular field; it will have a
662  
-        # special-case association with the field named '__all__'.
663  
-        # Note that in Form.clean(), you have access to self.cleaned_data, a dictionary of
664  
-        # all the fields/values that have *not* raised a ValidationError. Also note
665  
-        # Form.clean() is required to return a dictionary of all clean data.
  660
+        # Form's clean() method. Usually ValidationError raised by that method
  661
+        # will not be associated with a particular field and will have a
  662
+        # special-case association with the field named '__all__'. It's
  663
+        # possible to associate the errors to particular field with the
  664
+        # Form.add_error() method or by passing a dictionary that maps each
  665
+        # field to one or more errors.
  666
+        #
  667
+        # Note that in Form.clean(), you have access to self.cleaned_data, a
  668
+        # dictionary of all the fields/values that have *not* raised a
  669
+        # ValidationError. Also note Form.clean() is required to return a
  670
+        # dictionary of all clean data.
666 671
         class UserRegistration(Form):
667 672
             username = CharField(max_length=10)
668 673
             password1 = CharField(widget=PasswordInput)
669 674
             password2 = CharField(widget=PasswordInput)
670 675
 
671 676
             def clean(self):
  677
+                # Test raising a ValidationError as NON_FIELD_ERRORS.
672 678
                 if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']:
673 679
                     raise ValidationError('Please make sure your passwords match.')
674 680
 
  681
+                # Test raising ValidationError that targets multiple fields.
  682
+                errors = {}
  683
+                if self.cleaned_data.get('password1') == 'FORBIDDEN_VALUE':
  684
+                    errors['password1'] = 'Forbidden value.'
  685
+                if self.cleaned_data.get('password2') == 'FORBIDDEN_VALUE':
  686
+                    errors['password2'] = ['Forbidden value.']
  687
+                if errors:
  688
+                    raise ValidationError(errors)
  689
+
  690
+                # Test Form.add_error()
  691
+                if self.cleaned_data.get('password1') == 'FORBIDDEN_VALUE2':
  692
+                    self.add_error(None, 'Non-field error 1.')
  693
+                    self.add_error('password1', 'Forbidden value 2.')
  694
+                if self.cleaned_data.get('password2') == 'FORBIDDEN_VALUE2':
  695
+                    self.add_error('password2', 'Forbidden value 2.')
  696
+                    raise ValidationError('Non-field error 2.')
  697
+
675 698
                 return self.cleaned_data
676 699
 
677 700
         f = UserRegistration(auto_id=False)
678 701
         self.assertEqual(f.errors, {})
  702
+
679 703
         f = UserRegistration({}, auto_id=False)
680 704
         self.assertHTMLEqual(f.as_table(), """<tr><th>Username:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="username" maxlength="10" /></td></tr>
681 705
 <tr><th>Password1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="password" name="password1" /></td></tr>
@@ -683,6 +707,7 @@ def clean(self):
683 707
         self.assertEqual(f.errors['username'], ['This field is required.'])
684 708
         self.assertEqual(f.errors['password1'], ['This field is required.'])
685 709
         self.assertEqual(f.errors['password2'], ['This field is required.'])
  710
+
686 711
         f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False)
687 712
         self.assertEqual(f.errors['__all__'], ['Please make sure your passwords match.'])
688 713
         self.assertHTMLEqual(f.as_table(), """<tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr>
@@ -693,12 +718,25 @@ def clean(self):
693 718
 <li>Username: <input type="text" name="username" value="adrian" maxlength="10" /></li>
694 719
 <li>Password1: <input type="password" name="password1" /></li>
695 720
 <li>Password2: <input type="password" name="password2" /></li>""")
  721
+
696 722
         f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False)
697 723
         self.assertEqual(f.errors, {})
698 724
         self.assertEqual(f.cleaned_data['username'], 'adrian')
699 725
         self.assertEqual(f.cleaned_data['password1'], 'foo')
700 726
         self.assertEqual(f.cleaned_data['password2'], 'foo')
701 727
 
  728
+        f = UserRegistration({'username': 'adrian', 'password1': 'FORBIDDEN_VALUE', 'password2': 'FORBIDDEN_VALUE'}, auto_id=False)
  729
+        self.assertEqual(f.errors['password1'], ['Forbidden value.'])
  730
+        self.assertEqual(f.errors['password2'], ['Forbidden value.'])
  731
+
  732
+        f = UserRegistration({'username': 'adrian', 'password1': 'FORBIDDEN_VALUE2', 'password2': 'FORBIDDEN_VALUE2'}, auto_id=False)
  733
+        self.assertEqual(f.errors['__all__'], ['Non-field error 1.', 'Non-field error 2.'])
  734
+        self.assertEqual(f.errors['password1'], ['Forbidden value 2.'])
  735
+        self.assertEqual(f.errors['password2'], ['Forbidden value 2.'])
  736
+
  737
+        with six.assertRaisesRegex(self, ValueError, "has no field named"):
  738
+            f.add_error('missing_field', 'Some error.')
  739
+
702 740
     def test_dynamic_construction(self):
703 741
         # It's possible to construct a Form dynamically by adding to the self.fields
704 742
         # dictionary in __init__(). Don't forget to call Form.__init__() within the

0 notes on commit f563c33

Please sign in to comment.
Something went wrong with that request. Please try again.