diff options
| author | David Smith <smithdc@gmail.com> | 2021-11-06 06:50:29 +0000 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2021-12-09 07:16:33 +0100 |
| commit | eba9a9b7f72995206af867600d6685b5405f172a (patch) | |
| tree | b073ce1740aa1efd25c61fd2e3d528c590bdb307 | |
| parent | 81739a45b5ae8f534910aaabc7e9b457eaa34163 (diff) | |
Refs #32338 -- Added Boundfield.legend_tag().
| -rw-r--r-- | django/forms/boundfield.py | 15 | ||||
| -rw-r--r-- | django/forms/jinja2/django/forms/label.html | 2 | ||||
| -rw-r--r-- | django/forms/templates/django/forms/label.html | 2 | ||||
| -rw-r--r-- | docs/ref/forms/api.txt | 36 | ||||
| -rw-r--r-- | docs/releases/4.1.txt | 4 | ||||
| -rw-r--r-- | tests/forms_tests/templates/forms_tests/legend_test.html | 7 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_forms.py | 117 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_i18n.py | 8 | ||||
| -rw-r--r-- | tests/forms_tests/widget_tests/test_checkboxselectmultiple.py | 1 | ||||
| -rw-r--r-- | tests/model_forms/tests.py | 12 | ||||
| -rw-r--r-- | tests/model_formsets/tests.py | 8 |
11 files changed, 188 insertions, 24 deletions
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py index 5bbfcbe41c..62d1823d57 100644 --- a/django/forms/boundfield.py +++ b/django/forms/boundfield.py @@ -145,7 +145,7 @@ class BoundField: initial_value = self.initial return field.has_changed(initial_value, self.data) - def label_tag(self, contents=None, attrs=None, label_suffix=None): + def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None): """ Wrap the given contents in a <label>, if the field has an ID attribute. contents should be mark_safe'd to avoid HTML escaping. If contents @@ -181,9 +181,22 @@ class BoundField: 'label': contents, 'attrs': attrs, 'use_tag': bool(id_), + 'tag': tag or 'label', } return self.form.render(self.form.template_name_label, context) + def legend_tag(self, contents=None, attrs=None, label_suffix=None): + """ + Wrap the given contents in a <legend>, if the field has an ID + attribute. Contents should be mark_safe'd to avoid HTML escaping. If + contents aren't given, use the field's HTML-escaped label. + + If attrs are given, use them as HTML attributes on the <legend> tag. + + label_suffix overrides the form's label_suffix. + """ + return self.label_tag(contents, attrs, label_suffix, tag='legend') + def css_classes(self, extra_classes=None): """ Return a string of space-separated CSS classes for this field. diff --git a/django/forms/jinja2/django/forms/label.html b/django/forms/jinja2/django/forms/label.html index 7ad5257a71..6ce3885f79 100644 --- a/django/forms/jinja2/django/forms/label.html +++ b/django/forms/jinja2/django/forms/label.html @@ -1 +1 @@ -{% if use_tag %}<label{% if attrs %}{% include 'django/forms/attrs.html' %}{% endif %}>{{ label }}</label>{% else %}{{ label }}{% endif %} +{% if use_tag %}<{{ tag }}{% if attrs %}{% include 'django/forms/attrs.html' %}{% endif %}>{{ label }}</{{ tag }}>{% else %}{{ label }}{% endif %} diff --git a/django/forms/templates/django/forms/label.html b/django/forms/templates/django/forms/label.html index eb2a9f7973..0eba630e82 100644 --- a/django/forms/templates/django/forms/label.html +++ b/django/forms/templates/django/forms/label.html @@ -1 +1 @@ -{% if use_tag %}<label{% include 'django/forms/attrs.html' %}>{{ label }}</label>{% else %}{{ label }}{% endif %} +{% if use_tag %}<{{ tag }}{% include 'django/forms/attrs.html' %}>{{ label }}</{{ tag }}>{% else %}{{ label }}{% endif %} diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index d80d9ea224..e092a2f59e 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -542,9 +542,9 @@ default template, see also :ref:`overriding-built-in-form-templates`. .. attribute:: Form.template_name_label The template used to render a field's ``<label>``, used when calling -:meth:`BoundField.label_tag`. Can be changed per form by overriding this -attribute or more generally by overriding the default template, see also -:ref:`overriding-built-in-form-templates`. +:meth:`BoundField.label_tag`/:meth:`~BoundField.legend_tag`. Can be changed per +form by overriding this attribute or more generally by overriding the default +template, see also :ref:`overriding-built-in-form-templates`. ``as_p()`` ---------- @@ -672,8 +672,12 @@ classes, as needed. The HTML will look something like:: <tr><th><label for="id_cc_myself">Cc myself:<label> ... >>> f['subject'].label_tag() <label class="required" for="id_subject">Subject:</label> + >>> f['subject'].legend_tag() + <legend class="required" for="id_subject">Subject:</legend> >>> f['subject'].label_tag(attrs={'class': 'foo'}) <label for="id_subject" class="foo required">Subject:</label> + >>> f['subject'].legend_tag(attrs={'class': 'foo'}) + <legend for="id_subject" class="foo required">Subject:</legend> .. _ref-forms-api-configuring-label: @@ -797,7 +801,8 @@ Fields can also define their own :attr:`~django.forms.Field.label_suffix`. This will take precedence over :attr:`Form.label_suffix <django.forms.Form.label_suffix>`. The suffix can also be overridden at runtime using the ``label_suffix`` parameter to -:meth:`~django.forms.BoundField.label_tag`. +:meth:`~django.forms.BoundField.label_tag`/ +:meth:`~django.forms.BoundField.legend_tag`. .. attribute:: Form.use_required_attribute @@ -1092,7 +1097,8 @@ Attributes of ``BoundField`` Use this property to render the ID of this field. For example, if you are manually constructing a ``<label>`` in your template (despite the fact that - :meth:`~BoundField.label_tag` will do this for you): + :meth:`~BoundField.label_tag`/:meth:`~BoundField.legend_tag` will do this + for you): .. code-block:: html+django @@ -1142,7 +1148,7 @@ Attributes of ``BoundField`` .. attribute:: BoundField.label The :attr:`~django.forms.Field.label` of the field. This is used in - :meth:`~BoundField.label_tag`. + :meth:`~BoundField.label_tag`/:meth:`~BoundField.legend_tag`. .. attribute:: BoundField.name @@ -1208,7 +1214,7 @@ Methods of ``BoundField`` >>> f['message'].css_classes('foo bar') 'foo bar required' -.. method:: BoundField.label_tag(contents=None, attrs=None, label_suffix=None) +.. method:: BoundField.label_tag(contents=None, attrs=None, label_suffix=None, tag=None) Renders a label tag for the form field using the template specified by :attr:`.Form.template_name_label`. @@ -1225,7 +1231,8 @@ Methods of ``BoundField`` field's widget ``attrs`` or :attr:`BoundField.auto_id`. Additional attributes can be provided by the ``attrs`` argument. * ``use_tag``: A boolean which is ``True`` if the label has an ``id``. - If ``False`` the default template omits the ``<label>`` tag. + If ``False`` the default template omits the ``tag``. + * ``tag``: An optional string to customize the tag, defaults to ``label``. .. tip:: @@ -1249,6 +1256,19 @@ Methods of ``BoundField`` The label is now rendered using the template engine. + .. versionchanged:: 4.1 + + The ``tag`` argument was added. + +.. method:: BoundField.legend_tag(contents=None, attrs=None, label_suffix=None) + + .. versionadded:: 4.1 + + Calls :meth:`.label_tag` with ``tag='legend'`` to render the label with + ``<legend>`` tags. This is useful when rendering radio and multiple + checkbox widgets where ``<legend>`` may be more appropriate than a + ``<label>``. + .. method:: BoundField.value() Use this method to render the raw value of this field as it would be rendered diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index c55e638695..3fdd7f53c6 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -177,7 +177,9 @@ File Uploads Forms ~~~~~ -* ... +* The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field + labels in ``<legend>`` tags via the new ``tag`` argument of + :meth:`~django.forms.BoundField.label_tag`. Generic Views ~~~~~~~~~~~~~ diff --git a/tests/forms_tests/templates/forms_tests/legend_test.html b/tests/forms_tests/templates/forms_tests/legend_test.html new file mode 100644 index 0000000000..436485e794 --- /dev/null +++ b/tests/forms_tests/templates/forms_tests/legend_test.html @@ -0,0 +1,7 @@ +{% for field in form %} + {% if field.widget_type == 'radioselect' %} + {{ field.legend_tag }} + {% else %} + {{ field.label_tag }} + {% endif %} +{% endfor %} diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index c478a71699..7e0bdeeb77 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -2728,7 +2728,8 @@ Password: <input type="password" name="password" required> def test_label_has_required_css_class(self): """ - #17922 - required_css_class is added to the label_tag() of required fields. + required_css_class is added to label_tag() and legend_tag() of required + fields. """ class SomeForm(Form): required_css_class = 'required' @@ -2738,10 +2739,22 @@ Password: <input type="password" name="password" required> f = SomeForm({'field': 'test'}) self.assertHTMLEqual(f['field'].label_tag(), '<label for="id_field" class="required">Field:</label>') self.assertHTMLEqual( + f['field'].legend_tag(), + '<legend for="id_field" class="required">Field:</legend>', + ) + self.assertHTMLEqual( f['field'].label_tag(attrs={'class': 'foo'}), '<label for="id_field" class="foo required">Field:</label>' ) + self.assertHTMLEqual( + f['field'].legend_tag(attrs={'class': 'foo'}), + '<legend for="id_field" class="foo required">Field:</legend>' + ) self.assertHTMLEqual(f['field2'].label_tag(), '<label for="id_field2">Field2:</label>') + self.assertHTMLEqual( + f['field2'].legend_tag(), + '<legend for="id_field2">Field2:</legend>', + ) def test_label_split_datetime_not_displayed(self): class EventForm(Form): @@ -2964,34 +2977,47 @@ Password: <input type="password" name="password" required> testcases = [ # (args, kwargs, expected) # without anything: just print the <label> - ((), {}, '<label for="id_field">Field:</label>'), + ((), {}, '<%(tag)s for="id_field">Field:</%(tag)s>'), # passing just one argument: overrides the field's label - (('custom',), {}, '<label for="id_field">custom:</label>'), + (('custom',), {}, '<%(tag)s for="id_field">custom:</%(tag)s>'), # the overridden label is escaped - (('custom&',), {}, '<label for="id_field">custom&:</label>'), - ((mark_safe('custom&'),), {}, '<label for="id_field">custom&:</label>'), + (('custom&',), {}, '<%(tag)s for="id_field">custom&:</%(tag)s>'), + ((mark_safe('custom&'),), {}, '<%(tag)s for="id_field">custom&:</%(tag)s>'), # Passing attrs to add extra attributes on the <label> - ((), {'attrs': {'class': 'pretty'}}, '<label for="id_field" class="pretty">Field:</label>') + ( + (), + {'attrs': {'class': 'pretty'}}, + '<%(tag)s for="id_field" class="pretty">Field:</%(tag)s>', + ), ] for args, kwargs, expected in testcases: with self.subTest(args=args, kwargs=kwargs): - self.assertHTMLEqual(boundfield.label_tag(*args, **kwargs), expected) + self.assertHTMLEqual( + boundfield.label_tag(*args, **kwargs), + expected % {'tag': 'label'}, + ) + self.assertHTMLEqual( + boundfield.legend_tag(*args, **kwargs), + expected % {'tag': 'legend'}, + ) def test_boundfield_label_tag_no_id(self): """ - If a widget has no id, label_tag just returns the text with no - surrounding <label>. + If a widget has no id, label_tag() and legend_tag() return the text + with no surrounding <label>. """ class SomeForm(Form): field = CharField() boundfield = SomeForm(auto_id='')['field'] self.assertHTMLEqual(boundfield.label_tag(), 'Field:') + self.assertHTMLEqual(boundfield.legend_tag(), 'Field:') self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&:') + self.assertHTMLEqual(boundfield.legend_tag('Custom&'), 'Custom&:') def test_boundfield_label_tag_custom_widget_id_for_label(self): class CustomIdForLabelTextInput(TextInput): @@ -3008,7 +3034,12 @@ Password: <input type="password" name="password" required> form = SomeForm() self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom:</label>') + self.assertHTMLEqual( + form['custom'].legend_tag(), + '<legend for="custom_id_custom">Custom:</legend>', + ) self.assertHTMLEqual(form['empty'].label_tag(), '<label>Empty:</label>') + self.assertHTMLEqual(form['empty'].legend_tag(), '<legend>Empty:</legend>') def test_boundfield_empty_label(self): class SomeForm(Form): @@ -3016,6 +3047,10 @@ Password: <input type="password" name="password" required> boundfield = SomeForm()['field'] self.assertHTMLEqual(boundfield.label_tag(), '<label for="id_field"></label>') + self.assertHTMLEqual( + boundfield.legend_tag(), + '<legend for="id_field"></legend>', + ) def test_boundfield_id_for_label(self): class SomeForm(Form): @@ -3069,7 +3104,7 @@ Password: <input type="password" name="password" required> self.assertEqual(field.css_classes(extra_classes='test'), 'test') self.assertEqual(field.css_classes(extra_classes='test test'), 'test') - def test_label_tag_override(self): + def test_label_suffix_override(self): """ BoundField label_suffix (if provided) overrides Form label_suffix """ @@ -3078,6 +3113,10 @@ Password: <input type="password" name="password" required> boundfield = SomeForm(label_suffix='!')['field'] self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>') + self.assertHTMLEqual( + boundfield.legend_tag(label_suffix='$'), + '<legend for="id_field">Field$</legend>', + ) def test_error_dict(self): class MyForm(Form): @@ -3526,9 +3565,10 @@ Password: <input type="password" name="password" required> def test_label_does_not_include_new_line(self): form = Person() field = form['first_name'] + self.assertEqual(field.label_tag(), '<label for="id_first_name">First name:</label>') self.assertEqual( - field.label_tag(), - '<label for="id_first_name">First name:</label>', + field.legend_tag(), + '<legend for="id_first_name">First name:</legend>', ) @override_settings(USE_THOUSAND_SEPARATOR=True) @@ -3539,6 +3579,10 @@ Password: <input type="password" name="password" required> field.label_tag(attrs={'number': 9999}), '<label number="9999" for="id_first_name">First name:</label>', ) + self.assertHTMLEqual( + field.legend_tag(attrs={'number': 9999}), + '<legend number="9999" for="id_first_name">First name:</legend>', + ) @jinja2_tests @@ -3747,6 +3791,43 @@ class TemplateTests(SimpleTestCase): '<input type="submit" required>' '</form>', ) + # Use form.[field].legend_tag to output a field's label with a <legend> + # tag wrapped around it, but *only* if the given field has an "id" + # attribute. Recall from above that passing the "auto_id" argument to a + # Form gives each field an "id" attribute. + t = Template( + '<form>' + '<p>{{ form.username.legend_tag }} {{ form.username }}</p>' + '<p>{{ form.password1.legend_tag }} {{ form.password1 }}</p>' + '<p>{{ form.password2.legend_tag }} {{ form.password2 }}</p>' + '<input type="submit" required>' + '</form>' + ) + f = UserRegistration(auto_id=False) + self.assertHTMLEqual( + t.render(Context({'form': f})), + '<form>' + '<p>Username: ' + '<input type="text" name="username" maxlength="10" required></p>' + '<p>Password1: <input type="password" name="password1" required></p>' + '<p>Password2: <input type="password" name="password2" required></p>' + '<input type="submit" required>' + '</form>', + ) + f = UserRegistration(auto_id='id_%s') + self.assertHTMLEqual( + t.render(Context({'form': f})), + '<form>' + '<p><legend for="id_username">Username:</legend>' + '<input id="id_username" type="text" name="username" maxlength="10" ' + 'required></p>' + '<p><legend for="id_password1">Password1:</legend>' + '<input type="password" name="password1" id="id_password1" required></p>' + '<p><legend for="id_password2">Password2:</legend>' + '<input type="password" name="password2" id="id_password2" required></p>' + '<input type="submit" required>' + '</form>', + ) # Use form.[field].help_text to output a field's help text. If the # given field does not have help text, nothing will be output. t = Template( @@ -3965,3 +4046,15 @@ class OverrideTests(SimpleTestCase): self.assertInHTML('<th>1</th>', f.render()) except RecursionError: self.fail('Cyclic reference in BoundField.render().') + + def test_legend_tag(self): + class CustomFrameworkForm(FrameworkForm): + template_name = 'forms_tests/legend_test.html' + required_css_class = 'required' + + f = CustomFrameworkForm() + self.assertHTMLEqual( + str(f), + '<label for="id_name" class="required">Name:</label>' + '<legend class="required">Language:</legend>', + ) diff --git a/tests/forms_tests/tests/test_i18n.py b/tests/forms_tests/tests/test_i18n.py index 67aafa0527..29967b2cb7 100644 --- a/tests/forms_tests/tests/test_i18n.py +++ b/tests/forms_tests/tests/test_i18n.py @@ -44,7 +44,15 @@ class FormsI18nTests(SimpleTestCase): f = SomeForm() self.assertHTMLEqual(f['field_1'].label_tag(), '<label for="id_field_1">field_1:</label>') + self.assertHTMLEqual( + f['field_1'].legend_tag(), + '<legend for="id_field_1">field_1:</legend>', + ) self.assertHTMLEqual(f['field_2'].label_tag(), '<label for="field_2_id">field_2:</label>') + self.assertHTMLEqual( + f['field_2'].legend_tag(), + '<legend for="field_2_id">field_2:</legend>', + ) def test_non_ascii_choices(self): class SomeForm(Form): diff --git a/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py b/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py index e831038e02..65025ed748 100644 --- a/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py +++ b/tests/forms_tests/widget_tests/test_checkboxselectmultiple.py @@ -185,3 +185,4 @@ class CheckboxSelectMultipleTest(WidgetTest): bound_field = TestForm()['f'] self.assertEqual(bound_field.field.widget.id_for_label('id'), '') self.assertEqual(bound_field.label_tag(), '<label>F:</label>') + self.assertEqual(bound_field.legend_tag(), '<legend>F:</legend>') diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 7907aa1c3d..0307569c12 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -830,6 +830,18 @@ class TestFieldOverridesByFormMeta(SimpleTestCase): str(form['slug'].label_tag()), '<label for="id_slug">Slug:</label>', ) + self.assertHTMLEqual( + form['name'].legend_tag(), + '<legend for="id_name">Title:</legend>', + ) + self.assertHTMLEqual( + form['url'].legend_tag(), + '<legend for="id_url">The URL:</legend>', + ) + self.assertHTMLEqual( + form['slug'].legend_tag(), + '<legend for="id_slug">Slug:</legend>', + ) def test_help_text_overrides(self): form = FieldOverridesByFormMetaForm() diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 49e7705c2f..bb1a8a8a5e 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1801,6 +1801,10 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): }) form = BookFormSet.form() self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>') + self.assertHTMLEqual( + form['title'].legend_tag(), + '<legend for="id_title">Name:</legend>', + ) def test_inlineformset_factory_labels_overrides(self): BookFormSet = inlineformset_factory(Author, Book, fields="__all__", labels={ @@ -1808,6 +1812,10 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): }) form = BookFormSet.form() self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>') + self.assertHTMLEqual( + form['title'].legend_tag(), + '<legend for="id_title">Name:</legend>', + ) def test_modelformset_factory_help_text_overrides(self): BookFormSet = modelformset_factory(Book, fields="__all__", help_texts={ |
