diff options
| author | Nick Presta <nick@nickpresta.ca> | 2014-04-14 23:58:51 -0400 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2014-06-02 15:22:15 -0400 |
| commit | 11f0899bbe7c04dc6109f57c3b36ed8621b8f08e (patch) | |
| tree | 92bbc93a67159d5c418ae128598de4b6db326b52 | |
| parent | a00efa30d6e5ca220b914415904fff5fa0a6f2c5 (diff) | |
Fixed #11776 -- Added CSS class for non-field/top of form errors.
Thanks Daniel Pope for the suggestion.
| -rw-r--r-- | AUTHORS | 1 | ||||
| -rw-r--r-- | django/forms/forms.py | 7 | ||||
| -rw-r--r-- | django/forms/utils.py | 12 | ||||
| -rw-r--r-- | docs/releases/1.8.txt | 4 | ||||
| -rw-r--r-- | docs/topics/forms/index.txt | 11 | ||||
| -rw-r--r-- | tests/admin_inlines/tests.py | 2 | ||||
| -rw-r--r-- | tests/admin_views/tests.py | 4 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_error_messages.py | 2 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_forms.py | 90 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_regressions.py | 4 |
10 files changed, 120 insertions, 17 deletions
@@ -510,6 +510,7 @@ answer newbie questions, and generally made Django that much better: polpak@yahoo.com Ross Poulton <ross@rossp.org> Mihai Preda <mihai_preda@yahoo.com> + Nick Presta <nick@nickpresta.ca> Matthias Pronk <django@masida.nl> Jyrki Pulliainen <jyrki.pulliainen@gmail.com> Thejaswi Puthraya <thejaswi.puthraya@gmail.com> diff --git a/django/forms/forms.py b/django/forms/forms.py index b5cf17f4eb..347eccab4b 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -280,7 +280,7 @@ class BaseForm(object): field -- i.e., from Form.clean(). Returns an empty ErrorList if there are none. """ - return self.errors.get(NON_FIELD_ERRORS, self.error_class()) + return self.errors.get(NON_FIELD_ERRORS, self.error_class(error_class='nonfield')) def _raw_value(self, fieldname): """ @@ -331,7 +331,10 @@ class BaseForm(object): 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() + if field == NON_FIELD_ERRORS: + self._errors[field] = self.error_class(error_class='nonfield') + else: + self._errors[field] = self.error_class() self._errors[field].extend(error_list) if field in self.cleaned_data: del self.cleaned_data[field] diff --git a/django/forms/utils.py b/django/forms/utils.py index f7f53e9a0a..14253f36fe 100644 --- a/django/forms/utils.py +++ b/django/forms/utils.py @@ -80,6 +80,14 @@ class ErrorList(UserList, list): """ A collection of errors that knows how to display itself in various formats. """ + def __init__(self, initlist=None, error_class=None): + super(ErrorList, self).__init__(initlist) + + if error_class is None: + self.error_class = 'errorlist' + else: + self.error_class = 'errorlist {}'.format(error_class) + def as_data(self): return ValidationError(self.data).error_list @@ -99,8 +107,10 @@ class ErrorList(UserList, list): def as_ul(self): if not self.data: return '' + return format_html( - '<ul class="errorlist">{0}</ul>', + '<ul class="{0}">{1}</ul>', + self.error_class, format_html_join('', '<li>{0}</li>', ((force_text(e),) for e in self)) ) diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 056ea86baf..f716c995ca 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -129,6 +129,10 @@ Forms the ``<label>`` tags for required fields will have this class present in its attributes. +* The rendering of non-field errors in unordered lists (``<ul>``) now includes + ``nonfield`` in its list of classes to distinguish them from field-specific + errors. + * :class:`~django.forms.Field` now accepts a :attr:`~django.forms.Field.label_suffix` argument, which will override the form's :attr:`~django.forms.Form.label_suffix`. This enables customizing the diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index f3afac5ab9..8db24f68d0 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -292,6 +292,17 @@ over them:: </ol> {% endif %} +.. versionchanged:: 1.8 + +Non-field errors (and/or hidden field errors that are rendered at the top of +the form when using helpers like ``form.as_p()``) will be rendered with an +additional class of ``nonfield`` to help distinguish them from field-specific +errors. For example, ``{{ form.non_field_errors }}`` would look like:: + + <ul class="errorlist nonfield"> + <li>Generic validation error</li> + </ul> + Looping over the form's fields ------------------------------ diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 5d97381f18..56f006c441 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -94,7 +94,7 @@ class TestInline(TestCase): } response = self.client.post('/admin/admin_inlines/titlecollection/add/', data) # Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbox. - self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>The two titles must be the same</li></ul></td></tr>') + self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist nonfield"><li>The two titles must be the same</li></ul></td></tr>') def test_no_parent_callable_lookup(self): """Admin inline `readonly_field` shouldn't invoke parent ModelAdmin callable""" diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index ae6c66dc37..4b08956936 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2035,7 +2035,7 @@ class AdminViewListEditable(TestCase): "_save": "Save", } response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data) - self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 1, html=True) + self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 1, html=True) data = { "form-TOTAL_FORMS": "3", @@ -2062,7 +2062,7 @@ class AdminViewListEditable(TestCase): "_save": "Save", } response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data) - self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 2, html=True) + self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 2, html=True) def test_non_form_errors(self): # test if non-form errors are handled; ticket #12716 diff --git a/tests/forms_tests/tests/test_error_messages.py b/tests/forms_tests/tests/test_error_messages.py index 0a2bc0a8bf..94f5424c4e 100644 --- a/tests/forms_tests/tests/test_error_messages.py +++ b/tests/forms_tests/tests/test_error_messages.py @@ -238,7 +238,7 @@ class FormsErrorMessagesTestCase(TestCase, AssertFormErrorsMixin): # This form should print errors the default way. form1 = TestForm({'first_name': 'John'}) self.assertHTMLEqual(str(form1['last_name'].errors), '<ul class="errorlist"><li>This field is required.</li></ul>') - self.assertHTMLEqual(str(form1.errors['__all__']), '<ul class="errorlist"><li>I like to be awkward.</li></ul>') + self.assertHTMLEqual(str(form1.errors['__all__']), '<ul class="errorlist nonfield"><li>I like to be awkward.</li></ul>') # This one should wrap error groups in the customized way. form2 = TestForm({'first_name': 'John'}, error_class=CustomErrorList) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index d6a0367975..985ff8e2e0 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -713,11 +713,11 @@ class FormsTestCase(TestCase): f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False) self.assertEqual(f.errors['__all__'], ['Please make sure your passwords match.']) - self.assertHTMLEqual(f.as_table(), """<tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr> + self.assertHTMLEqual(f.as_table(), """<tr><td colspan="2"><ul class="errorlist nonfield"><li>Please make sure your passwords match.</li></ul></td></tr> <tr><th>Username:</th><td><input type="text" name="username" value="adrian" maxlength="10" /></td></tr> <tr><th>Password1:</th><td><input type="password" name="password1" /></td></tr> <tr><th>Password2:</th><td><input type="password" name="password2" /></td></tr>""") - self.assertHTMLEqual(f.as_ul(), """<li><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></li> + self.assertHTMLEqual(f.as_ul(), """<li><ul class="errorlist nonfield"><li>Please make sure your passwords match.</li></ul></li> <li>Username: <input type="text" name="username" value="adrian" maxlength="10" /></li> <li>Password1: <input type="password" name="password1" /></li> <li>Password2: <input type="password" name="password2" /></li>""") @@ -947,15 +947,15 @@ class FormsTestCase(TestCase): # prepended. This message is displayed at the top of the output, regardless of # its field's order in the form. p = Person({'first_name': 'John', 'last_name': 'Lennon', 'birthday': '1940-10-9'}, auto_id=False) - self.assertHTMLEqual(p.as_table(), """<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr> + self.assertHTMLEqual(p.as_table(), """<tr><td colspan="2"><ul class="errorlist nonfield"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr> <tr><th>First name:</th><td><input type="text" name="first_name" value="John" /></td></tr> <tr><th>Last name:</th><td><input type="text" name="last_name" value="Lennon" /></td></tr> <tr><th>Birthday:</th><td><input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></td></tr>""") - self.assertHTMLEqual(p.as_ul(), """<li><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></li> + self.assertHTMLEqual(p.as_ul(), """<li><ul class="errorlist nonfield"><li>(Hidden field hidden_text) This field is required.</li></ul></li> <li>First name: <input type="text" name="first_name" value="John" /></li> <li>Last name: <input type="text" name="last_name" value="Lennon" /></li> <li>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></li>""") - self.assertHTMLEqual(p.as_p(), """<ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul> + self.assertHTMLEqual(p.as_p(), """<ul class="errorlist nonfield"><li>(Hidden field hidden_text) This field is required.</li></ul> <p>First name: <input type="text" name="first_name" value="John" /></p> <p>Last name: <input type="text" name="last_name" value="Lennon" /></p> <p>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></p>""") @@ -1637,7 +1637,7 @@ class FormsTestCase(TestCase): # Case 2: POST with erroneous data (a redisplayed form, with errors).) self.assertHTMLEqual(my_function('POST', {'username': 'this-is-a-long-username', 'password1': 'foo', 'password2': 'bar'}), """<form action="" method="post"> <table> -<tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr> +<tr><td colspan="2"><ul class="errorlist nonfield"><li>Please make sure your passwords match.</li></ul></td></tr> <tr><th>Username:</th><td><ul class="errorlist"><li>Ensure this value has at most 10 characters (it has 23).</li></ul><input type="text" name="username" value="this-is-a-long-username" maxlength="10" /></td></tr> <tr><th>Password1:</th><td><input type="password" name="password1" /></td></tr> <tr><th>Password2:</th><td><input type="password" name="password2" /></td></tr> @@ -1764,7 +1764,7 @@ class FormsTestCase(TestCase): <input type="submit" /> </form>''') self.assertHTMLEqual(t.render(Context({'form': UserRegistration({'username': 'django', 'password1': 'foo', 'password2': 'bar'}, auto_id=False)})), """<form action=""> -<ul class="errorlist"><li>Please make sure your passwords match.</li></ul> +<ul class="errorlist nonfield"><li>Please make sure your passwords match.</li></ul> <p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p> <p><label>Password: <input type="password" name="password1" /></label></p> <p><label>Password (again): <input type="password" name="password2" /></label></p> @@ -2137,7 +2137,7 @@ class FormsTestCase(TestCase): control = [ '<li>foo<ul class="errorlist"><li>This field is required.</li></ul></li>', '<li>bar<ul class="errorlist"><li>This field is required.</li></ul></li>', - '<li>__all__<ul class="errorlist"><li>Non-field error.</li></ul></li>', + '<li>__all__<ul class="errorlist nonfield"><li>Non-field error.</li></ul></li>', ] for error in control: self.assertInHTML(error, errors) @@ -2200,3 +2200,77 @@ class FormsTestCase(TestCase): json.loads(e.as_json()), [{"message": "Foo", "code": ""}, {"message": "Foobar", "code": "foobar"}] ) + + def test_error_list_class_not_specified(self): + e = ErrorList() + e.append('Foo') + e.append(ValidationError('Foo%(bar)s', code='foobar', params={'bar': 'bar'})) + self.assertEqual( + e.as_ul(), + '<ul class="errorlist"><li>Foo</li><li>Foobar</li></ul>' + ) + + def test_error_list_class_has_one_class_specified(self): + e = ErrorList(error_class='foobar-error-class') + e.append('Foo') + e.append(ValidationError('Foo%(bar)s', code='foobar', params={'bar': 'bar'})) + self.assertEqual( + e.as_ul(), + '<ul class="errorlist foobar-error-class"><li>Foo</li><li>Foobar</li></ul>' + ) + + def test_error_list_with_hidden_field_errors_has_correct_class(self): + class Person(Form): + first_name = CharField() + last_name = CharField(widget=HiddenInput) + + p = Person({'first_name': 'John'}) + self.assertHTMLEqual( + p.as_ul(), + """<li><ul class="errorlist nonfield"><li>(Hidden field last_name) This field is required.</li></ul></li><li><label for="id_first_name">First name:</label> <input id="id_first_name" name="first_name" type="text" value="John" /><input id="id_last_name" name="last_name" type="hidden" /></li>""" + ) + self.assertHTMLEqual( + p.as_p(), + """<ul class="errorlist nonfield"><li>(Hidden field last_name) This field is required.</li></ul> +<p><label for="id_first_name">First name:</label> <input id="id_first_name" name="first_name" type="text" value="John" /><input id="id_last_name" name="last_name" type="hidden" /></p>""" + ) + self.assertHTMLEqual( + p.as_table(), + """<tr><td colspan="2"><ul class="errorlist nonfield"><li>(Hidden field last_name) This field is required.</li></ul></td></tr> +<tr><th><label for="id_first_name">First name:</label></th><td><input id="id_first_name" name="first_name" type="text" value="John" /><input id="id_last_name" name="last_name" type="hidden" /></td></tr>""" + ) + + def test_error_list_with_non_field_errors_has_correct_class(self): + class Person(Form): + first_name = CharField() + last_name = CharField() + + def clean(self): + raise ValidationError('Generic validation error') + + p = Person({'first_name': 'John', 'last_name': 'Lennon'}) + self.assertHTMLEqual( + str(p.non_field_errors()), + '<ul class="errorlist nonfield"><li>Generic validation error</li></ul>' + ) + self.assertHTMLEqual( + p.as_ul(), + """<li><ul class="errorlist nonfield"><li>Generic validation error</li></ul></li><li><label for="id_first_name">First name:</label> <input id="id_first_name" name="first_name" type="text" value="John" /></li> +<li><label for="id_last_name">Last name:</label> <input id="id_last_name" name="last_name" type="text" value="Lennon" /></li>""" + ) + self.assertHTMLEqual( + p.non_field_errors().as_text(), + '* Generic validation error' + ) + self.assertHTMLEqual( + p.as_p(), + """<ul class="errorlist nonfield"><li>Generic validation error</li></ul> +<p><label for="id_first_name">First name:</label> <input id="id_first_name" name="first_name" type="text" value="John" /></p> +<p><label for="id_last_name">Last name:</label> <input id="id_last_name" name="last_name" type="text" value="Lennon" /></p>""" + ) + self.assertHTMLEqual( + p.as_table(), + """<tr><td colspan="2"><ul class="errorlist nonfield"><li>Generic validation error</li></ul></td></tr> +<tr><th><label for="id_first_name">First name:</label></th><td><input id="id_first_name" name="first_name" type="text" value="John" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><input id="id_last_name" name="last_name" type="text" value="Lennon" /></td></tr>""" + ) diff --git a/tests/forms_tests/tests/test_regressions.py b/tests/forms_tests/tests/test_regressions.py index b009e800e1..4eea444808 100644 --- a/tests/forms_tests/tests/test_regressions.py +++ b/tests/forms_tests/tests/test_regressions.py @@ -98,8 +98,8 @@ class FormsRegressionsTestCase(TestCase): data = IntegerField(widget=HiddenInput) f = HiddenForm({}) - self.assertHTMLEqual(f.as_p(), '<ul class="errorlist"><li>(Hidden field data) This field is required.</li></ul>\n<p> <input type="hidden" name="data" id="id_data" /></p>') - self.assertHTMLEqual(f.as_table(), '<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field data) This field is required.</li></ul><input type="hidden" name="data" id="id_data" /></td></tr>') + self.assertHTMLEqual(f.as_p(), '<ul class="errorlist nonfield"><li>(Hidden field data) This field is required.</li></ul>\n<p> <input type="hidden" name="data" id="id_data" /></p>') + self.assertHTMLEqual(f.as_table(), '<tr><td colspan="2"><ul class="errorlist nonfield"><li>(Hidden field data) This field is required.</li></ul><input type="hidden" name="data" id="id_data" /></td></tr>') def test_xss_error_messages(self): ################################################### |
