diff options
| author | David Smith <smithdc@gmail.com> | 2023-11-18 20:36:45 +0000 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2024-12-05 10:24:39 +0100 |
| commit | edd74c3417fa3a0b29295012ff31dbe44843303c (patch) | |
| tree | f2992dd39bea49bcd891b63e9ecd691b25617dd7 | |
| parent | 28f81a10190de9aa00925156c0005f6c787afeb3 (diff) | |
Refs #32819 -- Added id to ErrorList class and template.
| -rw-r--r-- | django/forms/forms.py | 5 | ||||
| -rw-r--r-- | django/forms/jinja2/django/forms/errors/list/ul.html | 2 | ||||
| -rw-r--r-- | django/forms/templates/django/forms/errors/list/ul.html | 2 | ||||
| -rw-r--r-- | django/forms/utils.py | 3 | ||||
| -rw-r--r-- | docs/ref/forms/api.txt | 16 | ||||
| -rw-r--r-- | docs/releases/5.2.txt | 4 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_error_messages.py | 9 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_forms.py | 96 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_i18n.py | 2 | ||||
| -rw-r--r-- | tests/model_forms/tests.py | 11 |
10 files changed, 91 insertions, 59 deletions
diff --git a/django/forms/forms.py b/django/forms/forms.py index 452f554e1e..549a3adf6f 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -298,7 +298,10 @@ class BaseForm(RenderableFormMixin): error_class="nonfield", renderer=self.renderer ) else: - self._errors[field] = self.error_class(renderer=self.renderer) + self._errors[field] = self.error_class( + renderer=self.renderer, + field_id=self[field].auto_id, + ) self._errors[field].extend(error_list) if field in self.cleaned_data: del self.cleaned_data[field] diff --git a/django/forms/jinja2/django/forms/errors/list/ul.html b/django/forms/jinja2/django/forms/errors/list/ul.html index 752f7c2c8b..59528efccc 100644 --- a/django/forms/jinja2/django/forms/errors/list/ul.html +++ b/django/forms/jinja2/django/forms/errors/list/ul.html @@ -1 +1 @@ -{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %} +{% if errors %}<ul class="{{ error_class }}"{% if errors.field_id %} id="{{ errors.field_id }}_error"{% endif %}>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %} diff --git a/django/forms/templates/django/forms/errors/list/ul.html b/django/forms/templates/django/forms/errors/list/ul.html index 57b34ccb88..c28ce8af67 100644 --- a/django/forms/templates/django/forms/errors/list/ul.html +++ b/django/forms/templates/django/forms/errors/list/ul.html @@ -1 +1 @@ -{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
\ No newline at end of file +{% if errors %}<ul class="{{ error_class }}"{% if errors.field_id %} id="{{ errors.field_id }}_error"{% endif %}>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
\ No newline at end of file diff --git a/django/forms/utils.py b/django/forms/utils.py index f4fbf3e241..d24711d1a0 100644 --- a/django/forms/utils.py +++ b/django/forms/utils.py @@ -147,7 +147,7 @@ class ErrorList(UserList, list, RenderableErrorMixin): template_name_text = "django/forms/errors/list/text.txt" template_name_ul = "django/forms/errors/list/ul.html" - def __init__(self, initlist=None, error_class=None, renderer=None): + def __init__(self, initlist=None, error_class=None, renderer=None, field_id=None): super().__init__(initlist) if error_class is None: @@ -155,6 +155,7 @@ class ErrorList(UserList, list, RenderableErrorMixin): else: self.error_class = "errorlist {}".format(error_class) self.renderer = renderer or get_default_renderer() + self.field_id = field_id def as_data(self): return ValidationError(self.data).error_list diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index c6c83dcdfb..4875a1ab72 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -1025,13 +1025,17 @@ method you're using: Customizing the error list format --------------------------------- -.. class:: ErrorList(initlist=None, error_class=None, renderer=None) +.. class:: ErrorList(initlist=None, error_class=None, renderer=None, field_id=None) By default, forms use ``django.forms.utils.ErrorList`` to format validation errors. ``ErrorList`` is a list like object where ``initlist`` is the list of errors. In addition this class has the following attributes and methods. + .. versionchanged:: 5.2 + + The ``field_id`` argument was added. + .. attribute:: error_class The CSS classes to be used when rendering the error list. Any provided @@ -1043,6 +1047,16 @@ Customizing the error list format Defaults to ``None`` which means to use the default renderer specified by the :setting:`FORM_RENDERER` setting. + .. attribute:: field_id + + .. versionadded:: 5.2 + + An ``id`` for the field for which the errors relate. This allows an + HTML ``id`` attribute to be added in the error template and is useful + to associate the errors with the field. The default template uses the + format ``id="{{ field_id }}_error"`` and a value is provided by + :meth:`.Form.add_error` using the field's :attr:`~.BoundField.auto_id`. + .. attribute:: template_name The name of the template used when calling ``__str__`` or diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 4b05fd3279..b6c44c43f8 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -249,6 +249,10 @@ Forms * The new :class:`~django.forms.TelInput` form widget is for entering telephone numbers and renders as ``<input type="tel" ...>``. +* The new ``field_id`` argument for :class:`~django.forms.ErrorList` allows an + HTML ``id`` attribute to be added in the error template. See + :attr:`.ErrorList.field_id` for details. + Generic Views ~~~~~~~~~~~~~ diff --git a/tests/forms_tests/tests/test_error_messages.py b/tests/forms_tests/tests/test_error_messages.py index e44c6d6668..f4f5700107 100644 --- a/tests/forms_tests/tests/test_error_messages.py +++ b/tests/forms_tests/tests/test_error_messages.py @@ -249,7 +249,8 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin): form1 = TestForm({"first_name": "John"}) self.assertHTMLEqual( str(form1["last_name"].errors), - '<ul class="errorlist"><li>This field is required.</li></ul>', + '<ul class="errorlist" id="id_last_name_error"><li>' + "This field is required.</li></ul>", ) self.assertHTMLEqual( str(form1.errors["__all__"]), @@ -280,7 +281,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin): f = SomeForm({"field": "<script>"}) self.assertHTMLEqual( t.render(Context({"form": f})), - '<ul class="errorlist"><li>field<ul class="errorlist">' + '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">' "<li>Select a valid choice. <script> is not one of the " "available choices.</li></ul></li></ul>", ) @@ -291,7 +292,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin): f = SomeForm({"field": ["<script>"]}) self.assertHTMLEqual( t.render(Context({"form": f})), - '<ul class="errorlist"><li>field<ul class="errorlist">' + '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">' "<li>Select a valid choice. <script> is not one of the " "available choices.</li></ul></li></ul>", ) @@ -302,7 +303,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin): f = SomeForm({"field": ["<script>"]}) self.assertHTMLEqual( t.render(Context({"form": f})), - '<ul class="errorlist"><li>field<ul class="errorlist">' + '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">' "<li>“<script>” is not a valid value.</li>" "</ul></li></ul>", ) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 3982cc93fe..cd909628cb 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -181,53 +181,55 @@ class FormsTestCase(SimpleTestCase): self.assertHTMLEqual( str(p), '<div><label for="id_first_name">First name:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="first_name" aria-invalid="true" required ' - 'id="id_first_name"></div>' + '<ul class="errorlist" id="id_first_name_error"><li>This field is required.' + '</li></ul><input type="text" name="first_name" aria-invalid="true" ' + 'required id="id_first_name"></div>' '<div><label for="id_last_name">Last name:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="last_name" aria-invalid="true" required ' - 'id="id_last_name"></div><div>' + '<ul class="errorlist" id="id_last_name_error"><li>This field is required.' + '</li></ul><input type="text" name="last_name" aria-invalid="true" ' + 'required id="id_last_name"></div><div>' '<label for="id_birthday">Birthday:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="birthday" aria-invalid="true" required ' + '<ul class="errorlist" id="id_birthday_error"><li>This field is required.' + '</li></ul><input type="text" name="birthday" aria-invalid="true" required ' 'id="id_birthday"></div>', ) self.assertHTMLEqual( p.as_table(), """<tr><th><label for="id_first_name">First name:</label></th><td> -<ul class="errorlist"><li>This field is required.</li></ul> +<ul class="errorlist" id="id_first_name_error"><li>This field is required.</li></ul> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> </td></tr><tr><th><label for="id_last_name">Last name:</label></th> -<td><ul class="errorlist"><li>This field is required.</li></ul> +<td><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> </td></tr><tr><th><label for="id_birthday">Birthday:</label></th> -<td><ul class="errorlist"><li>This field is required.</li></ul> +<td><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> </td></tr>""", ) self.assertHTMLEqual( p.as_ul(), - """<li><ul class="errorlist"><li>This field is required.</li></ul> + """<li><ul class="errorlist" id="id_first_name_error"> +<li>This field is required.</li></ul> <label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> -</li><li><ul class="errorlist"><li>This field is required.</li></ul> -<label for="id_last_name">Last name:</label> +</li><li><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li> +</ul><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> -</li><li><ul class="errorlist"><li>This field is required.</li></ul> -<label for="id_birthday">Birthday:</label> +</li><li><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li> +</ul><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> </li>""", ) self.assertHTMLEqual( p.as_p(), - """<ul class="errorlist"><li>This field is required.</li></ul> + """<ul class="errorlist" id="id_first_name_error"><li> +This field is required.</li></ul> <p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> -</p><ul class="errorlist"><li>This field is required.</li></ul> +</p><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul> <p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> -</p><ul class="errorlist"><li>This field is required.</li></ul> +</p><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul> <p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> </p>""", @@ -235,16 +237,16 @@ class FormsTestCase(SimpleTestCase): self.assertHTMLEqual( p.as_div(), '<div><label for="id_first_name">First name:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="first_name" aria-invalid="true" required ' - 'id="id_first_name"></div>' + '<ul class="errorlist" id="id_first_name_error"><li>This field is required.' + '</li></ul><input type="text" name="first_name" aria-invalid="true" ' + 'required id="id_first_name"></div>' '<div><label for="id_last_name">Last name:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="last_name" aria-invalid="true" required ' - 'id="id_last_name"></div><div>' + '<ul class="errorlist" id="id_last_name_error"><li>This field is required.' + '</li></ul><input type="text" name="last_name" aria-invalid="true" ' + 'required id="id_last_name"></div><div>' '<label for="id_birthday">Birthday:</label>' - '<ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="birthday" aria-invalid="true" required ' + '<ul class="errorlist" id="id_birthday_error"><li>This field is required.' + '</li></ul><input type="text" name="birthday" aria-invalid="true" required ' 'id="id_birthday"></div>', ) @@ -387,7 +389,8 @@ class FormsTestCase(SimpleTestCase): self.assertEqual(p["first_name"].errors, ["This field is required."]) self.assertHTMLEqual( p["first_name"].errors.as_ul(), - '<ul class="errorlist"><li>This field is required.</li></ul>', + '<ul class="errorlist" id="id_first_name_error">' + "<li>This field is required.</li></ul>", ) self.assertEqual(p["first_name"].errors.as_text(), "* This field is required.") @@ -3706,7 +3709,7 @@ Options: <select multiple name="options" aria-invalid="true" required> self.assertHTMLEqual( p.as_ul(), """ - <li class="required error"><ul class="errorlist"> + <li class="required error"><ul class="errorlist" id="id_name_error"> <li>This field is required.</li></ul> <label class="required" for="id_name">Name:</label> <input type="text" name="name" id="id_name" aria-invalid="true" required> @@ -3719,7 +3722,7 @@ Options: <select multiple name="options" aria-invalid="true" required> </select></li> <li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></li> - <li class="required error"><ul class="errorlist"> + <li class="required error"><ul class="errorlist" id="id_age_error"> <li>This field is required.</li></ul> <label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" aria-invalid="true" required> @@ -3729,8 +3732,8 @@ Options: <select multiple name="options" aria-invalid="true" required> self.assertHTMLEqual( p.as_p(), """ - <ul class="errorlist"><li>This field is required.</li></ul> - <p class="required error"> + <ul class="errorlist" id="id_name_error"><li>This field is required.</li> + </ul><p class="required error"> <label class="required" for="id_name">Name:</label> <input type="text" name="name" id="id_name" aria-invalid="true" required> </p><p class="required"> @@ -3742,17 +3745,17 @@ Options: <select multiple name="options" aria-invalid="true" required> </select></p> <p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></p> - <ul class="errorlist"><li>This field is required.</li></ul> - <p class="required error"><label class="required" for="id_age">Age:</label> - <input type="number" name="age" id="id_age" aria-invalid="true" required> - </p>""", + <ul class="errorlist" id="id_age_error"><li>This field is required.</li> + </ul><p class="required error"><label class="required" for="id_age"> + Age:</label><input type="number" name="age" id="id_age" aria-invalid="true" + required></p>""", ) self.assertHTMLEqual( p.as_table(), """<tr class="required error"> <th><label class="required" for="id_name">Name:</label></th> -<td><ul class="errorlist"><li>This field is required.</li></ul> +<td><ul class="errorlist" id="id_name_error"><li>This field is required.</li></ul> <input type="text" name="name" id="id_name" aria-invalid="true" required></td></tr> <tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th> <td><select name="is_cool" id="id_is_cool"> @@ -3763,14 +3766,14 @@ Options: <select multiple name="options" aria-invalid="true" required> <tr><th><label for="id_email">Email:</label></th><td> <input type="email" name="email" id="id_email" maxlength="320"></td></tr> <tr class="required error"><th><label class="required" for="id_age">Age:</label></th> -<td><ul class="errorlist"><li>This field is required.</li></ul> +<td><ul class="errorlist" id="id_age_error"><li>This field is required.</li></ul> <input type="number" name="age" id="id_age" aria-invalid="true" required></td></tr>""", ) self.assertHTMLEqual( p.as_div(), '<div class="required error"><label for="id_name" class="required">Name:' - '</label><ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="text" name="name" required id="id_name" ' + '</label><ul class="errorlist" id="id_name_error"><li>This field is ' + 'required.</li></ul><input type="text" name="name" required id="id_name" ' 'aria-invalid="true" /></div>' '<div class="required"><label for="id_is_cool" class="required">Is cool:' '</label><select name="is_cool" id="id_is_cool">' @@ -3779,8 +3782,8 @@ Options: <select multiple name="options" aria-invalid="true" required> '</select></div><div><label for="id_email">Email:</label>' '<input type="email" name="email" id="id_email" maxlength="320"/></div>' '<div class="required error"><label for="id_age" class="required">Age:' - '</label><ul class="errorlist"><li>This field is required.</li></ul>' - '<input type="number" name="age" required id="id_age" ' + '</label><ul class="errorlist" id="id_age_error"><li>This field is ' + 'required.</li></ul><input type="number" name="age" required id="id_age" ' 'aria-invalid="true" /></div>', ) @@ -4255,8 +4258,10 @@ Options: <select multiple name="options" aria-invalid="true" required> errors = form.errors.as_ul() 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>foo<ul class="errorlist" id="id_foo_error"><li>This field is required.' + "</li></ul></li>", + '<li>bar<ul class="errorlist" id="id_bar_error"><li>This field is required.' + "</li></ul></li>", '<li>__all__<ul class="errorlist nonfield"><li>Non-field error.</li></ul>' "</li>", ] @@ -4461,7 +4466,8 @@ Options: <select multiple name="options" aria-invalid="true" required> form.as_ul(), '<li><ul class="errorlist nonfield">' "<li>(Hidden field hidden) Foo & "bar"!</li></ul></li>" - '<li><ul class="errorlist"><li>Foo & "bar"!</li></ul>' + '<li><ul class="errorlist" id="id_visible_error"><li>Foo & ' + ""bar"!</li></ul>" '<label for="id_visible">Visible:</label> ' '<input type="text" name="visible" aria-invalid="true" value="b" ' 'id="id_visible" required>' diff --git a/tests/forms_tests/tests/test_i18n.py b/tests/forms_tests/tests/test_i18n.py index 15c456c0bd..5da0c0e3d0 100644 --- a/tests/forms_tests/tests/test_i18n.py +++ b/tests/forms_tests/tests/test_i18n.py @@ -97,7 +97,7 @@ class FormsI18nTests(SimpleTestCase): f = SomeForm({}) self.assertHTMLEqual( f.as_p(), - '<ul class="errorlist"><li>' + '<ul class="errorlist" id="id_somechoice_error"><li>' "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c" "\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul>\n" "<p><label>\xc5\xf8\xdf:</label>" diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 3c4c510440..4f7fc2c1c0 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -3207,11 +3207,13 @@ class ModelFormCustomErrorTests(SimpleTestCase): errors = CustomErrorMessageForm(data).errors self.assertHTMLEqual( str(errors["name1"]), - '<ul class="errorlist"><li>Form custom error message.</li></ul>', + '<ul class="errorlist" id="id_name1_error">' + "<li>Form custom error message.</li></ul>", ) self.assertHTMLEqual( str(errors["name2"]), - '<ul class="errorlist"><li>Model custom error message.</li></ul>', + '<ul class="errorlist" id="id_name2_error">' + "<li>Model custom error message.</li></ul>", ) def test_model_clean_error_messages(self): @@ -3220,14 +3222,15 @@ class ModelFormCustomErrorTests(SimpleTestCase): self.assertFalse(form.is_valid()) self.assertHTMLEqual( str(form.errors["name1"]), - '<ul class="errorlist"><li>Model.clean() error messages.</li></ul>', + '<ul class="errorlist" id="id_name1_error">' + "<li>Model.clean() error messages.</li></ul>", ) data = {"name1": "FORBIDDEN_VALUE2", "name2": "ABC"} form = CustomErrorMessageForm(data) self.assertFalse(form.is_valid()) self.assertHTMLEqual( str(form.errors["name1"]), - '<ul class="errorlist">' + '<ul class="errorlist" id="id_name1_error">' "<li>Model.clean() error messages (simpler syntax).</li></ul>", ) data = {"name1": "GLOBAL_ERROR", "name2": "ABC"} |
