diff --git a/django/forms/forms.py b/django/forms/forms.py index da30a6220a..97a344e9fc 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -334,6 +334,15 @@ class BaseForm(object): if field in self.cleaned_data: del self.cleaned_data[field] + def has_error(self, field, code=None): + if code is None: + return field in self.errors + if field in self.errors: + for error in self.errors.as_data()[field]: + if error.code == code: + return True + return False + def full_clean(self): """ Cleans all of self.data and populates self._errors and diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index c4d0b18b0f..cb6cf7a51b 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -182,6 +182,17 @@ when defining form errors. Note that ``Form.add_error()`` automatically removes the relevant field from ``cleaned_data``. +.. method:: Form.has_error(field, code=None) + +.. versionadded:: 1.8 + +This method returns a boolean designating whether a field has an error with +a specific error ``code``. If ``code`` is ``None``, it will return ``True`` +if the field contains any errors at all. + +To check for non-field errors use +:data:`~django.core.exceptions.NON_FIELD_ERRORS` as the ``field`` parameter. + Behavior of unbound forms ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 2214b77839..10acdb6df4 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -109,6 +109,9 @@ Forms * Form widgets now render attributes with a value of ``True`` or ``False`` as HTML5 boolean attributes. +* The new :meth:`~django.forms.Form.has_error()` method allows checking + if a specific error has happened. + Internationalization ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index c05c79a68e..6b90390159 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -6,6 +6,7 @@ import datetime import json import warnings +from django.core.exceptions import NON_FIELD_ERRORS from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import RegexValidator from django.forms import ( @@ -739,6 +740,39 @@ class FormsTestCase(TestCase): with six.assertRaisesRegex(self, ValueError, "has no field named"): f.add_error('missing_field', 'Some error.') + def test_has_error(self): + class UserRegistration(Form): + username = CharField(max_length=10) + password1 = CharField(widget=PasswordInput, min_length=5) + password2 = CharField(widget=PasswordInput) + + def clean(self): + if (self.cleaned_data.get('password1') and self.cleaned_data.get('password2') + and self.cleaned_data['password1'] != self.cleaned_data['password2']): + raise ValidationError( + 'Please make sure your passwords match.', + code='password_mismatch', + ) + + f = UserRegistration(data={}) + self.assertTrue(f.has_error('password1')) + self.assertTrue(f.has_error('password1', 'required')) + self.assertFalse(f.has_error('password1', 'anything')) + + f = UserRegistration(data={'password1': 'Hi', 'password2': 'Hi'}) + self.assertTrue(f.has_error('password1')) + self.assertTrue(f.has_error('password1', 'min_length')) + self.assertFalse(f.has_error('password1', 'anything')) + self.assertFalse(f.has_error('password2')) + self.assertFalse(f.has_error('password2', 'anything')) + + f = UserRegistration(data={'password1': 'Bonjour', 'password2': 'Hello'}) + self.assertFalse(f.has_error('password1')) + self.assertFalse(f.has_error('password1', 'required')) + self.assertTrue(f.has_error(NON_FIELD_ERRORS)) + self.assertTrue(f.has_error(NON_FIELD_ERRORS, 'password_mismatch')) + self.assertFalse(f.has_error(NON_FIELD_ERRORS, 'anything')) + def test_dynamic_construction(self): # It's possible to construct a Form dynamically by adding to the self.fields # dictionary in __init__(). Don't forget to call Form.__init__() within the