Refs #32338 -- Added Boundfield.legend_tag().

This commit is contained in:
David Smith 2021-11-06 06:50:29 +00:00 committed by Mariusz Felisiak
parent 81739a45b5
commit eba9a9b7f7
11 changed files with 188 additions and 24 deletions

View File

@ -145,7 +145,7 @@ class BoundField:
initial_value = self.initial initial_value = self.initial
return field.has_changed(initial_value, self.data) 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. 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 contents should be mark_safe'd to avoid HTML escaping. If contents
@ -181,9 +181,22 @@ class BoundField:
'label': contents, 'label': contents,
'attrs': attrs, 'attrs': attrs,
'use_tag': bool(id_), 'use_tag': bool(id_),
'tag': tag or 'label',
} }
return self.form.render(self.form.template_name_label, context) 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): def css_classes(self, extra_classes=None):
""" """
Return a string of space-separated CSS classes for this field. Return a string of space-separated CSS classes for this field.

View File

@ -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 %}

View File

@ -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 %}

View File

@ -542,9 +542,9 @@ default template, see also :ref:`overriding-built-in-form-templates`.
.. attribute:: Form.template_name_label .. attribute:: Form.template_name_label
The template used to render a field's ``<label>``, used when calling The template used to render a field's ``<label>``, used when calling
:meth:`BoundField.label_tag`. Can be changed per form by overriding this :meth:`BoundField.label_tag`/:meth:`~BoundField.legend_tag`. Can be changed per
attribute or more generally by overriding the default template, see also form by overriding this attribute or more generally by overriding the default
:ref:`overriding-built-in-form-templates`. template, see also :ref:`overriding-built-in-form-templates`.
``as_p()`` ``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> ... <tr><th><label for="id_cc_myself">Cc myself:<label> ...
>>> f['subject'].label_tag() >>> f['subject'].label_tag()
<label class="required" for="id_subject">Subject:</label> <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'}) >>> f['subject'].label_tag(attrs={'class': 'foo'})
<label for="id_subject" class="foo required">Subject:</label> <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: .. _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 This will take precedence over :attr:`Form.label_suffix
<django.forms.Form.label_suffix>`. The suffix can also be overridden at runtime <django.forms.Form.label_suffix>`. The suffix can also be overridden at runtime
using the ``label_suffix`` parameter to 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 .. 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 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 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 .. code-block:: html+django
@ -1142,7 +1148,7 @@ Attributes of ``BoundField``
.. attribute:: BoundField.label .. attribute:: BoundField.label
The :attr:`~django.forms.Field.label` of the field. This is used in 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 .. attribute:: BoundField.name
@ -1208,7 +1214,7 @@ Methods of ``BoundField``
>>> f['message'].css_classes('foo bar') >>> f['message'].css_classes('foo bar')
'foo bar required' '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 Renders a label tag for the form field using the template specified by
:attr:`.Form.template_name_label`. :attr:`.Form.template_name_label`.
@ -1225,7 +1231,8 @@ Methods of ``BoundField``
field's widget ``attrs`` or :attr:`BoundField.auto_id`. Additional field's widget ``attrs`` or :attr:`BoundField.auto_id`. Additional
attributes can be provided by the ``attrs`` argument. attributes can be provided by the ``attrs`` argument.
* ``use_tag``: A boolean which is ``True`` if the label has an ``id``. * ``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:: .. tip::
@ -1249,6 +1256,19 @@ Methods of ``BoundField``
The label is now rendered using the template engine. 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() .. method:: BoundField.value()
Use this method to render the raw value of this field as it would be rendered Use this method to render the raw value of this field as it would be rendered

View File

@ -177,7 +177,9 @@ File Uploads
Forms 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 Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -0,0 +1,7 @@
{% for field in form %}
{% if field.widget_type == 'radioselect' %}
{{ field.legend_tag }}
{% else %}
{{ field.label_tag }}
{% endif %}
{% endfor %}

View File

@ -2728,7 +2728,8 @@ Password: <input type="password" name="password" required>
def test_label_has_required_css_class(self): 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): class SomeForm(Form):
required_css_class = 'required' required_css_class = 'required'
@ -2737,11 +2738,23 @@ Password: <input type="password" name="password" required>
f = SomeForm({'field': 'test'}) f = SomeForm({'field': 'test'})
self.assertHTMLEqual(f['field'].label_tag(), '<label for="id_field" class="required">Field:</label>') 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( self.assertHTMLEqual(
f['field'].label_tag(attrs={'class': 'foo'}), f['field'].label_tag(attrs={'class': 'foo'}),
'<label for="id_field" class="foo required">Field:</label>' '<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'].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): def test_label_split_datetime_not_displayed(self):
class EventForm(Form): class EventForm(Form):
@ -2964,34 +2977,47 @@ Password: <input type="password" name="password" required>
testcases = [ # (args, kwargs, expected) testcases = [ # (args, kwargs, expected)
# without anything: just print the <label> # 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 # 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 # the overridden label is escaped
(('custom&',), {}, '<label for="id_field">custom&amp;:</label>'), (('custom&',), {}, '<%(tag)s for="id_field">custom&amp;:</%(tag)s>'),
((mark_safe('custom&'),), {}, '<label for="id_field">custom&:</label>'), ((mark_safe('custom&'),), {}, '<%(tag)s for="id_field">custom&:</%(tag)s>'),
# Passing attrs to add extra attributes on the <label> # 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: for args, kwargs, expected in testcases:
with self.subTest(args=args, kwargs=kwargs): 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): def test_boundfield_label_tag_no_id(self):
""" """
If a widget has no id, label_tag just returns the text with no If a widget has no id, label_tag() and legend_tag() return the text
surrounding <label>. with no surrounding <label>.
""" """
class SomeForm(Form): class SomeForm(Form):
field = CharField() field = CharField()
boundfield = SomeForm(auto_id='')['field'] boundfield = SomeForm(auto_id='')['field']
self.assertHTMLEqual(boundfield.label_tag(), 'Field:') self.assertHTMLEqual(boundfield.label_tag(), 'Field:')
self.assertHTMLEqual(boundfield.legend_tag(), 'Field:')
self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&amp;:') self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&amp;:')
self.assertHTMLEqual(boundfield.legend_tag('Custom&'), 'Custom&amp;:')
def test_boundfield_label_tag_custom_widget_id_for_label(self): def test_boundfield_label_tag_custom_widget_id_for_label(self):
class CustomIdForLabelTextInput(TextInput): class CustomIdForLabelTextInput(TextInput):
@ -3008,7 +3034,12 @@ Password: <input type="password" name="password" required>
form = SomeForm() form = SomeForm()
self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom:</label>') 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'].label_tag(), '<label>Empty:</label>')
self.assertHTMLEqual(form['empty'].legend_tag(), '<legend>Empty:</legend>')
def test_boundfield_empty_label(self): def test_boundfield_empty_label(self):
class SomeForm(Form): class SomeForm(Form):
@ -3016,6 +3047,10 @@ Password: <input type="password" name="password" required>
boundfield = SomeForm()['field'] boundfield = SomeForm()['field']
self.assertHTMLEqual(boundfield.label_tag(), '<label for="id_field"></label>') 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): def test_boundfield_id_for_label(self):
class SomeForm(Form): 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')
self.assertEqual(field.css_classes(extra_classes='test 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 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'] boundfield = SomeForm(label_suffix='!')['field']
self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>') 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): def test_error_dict(self):
class MyForm(Form): class MyForm(Form):
@ -3526,9 +3565,10 @@ Password: <input type="password" name="password" required>
def test_label_does_not_include_new_line(self): def test_label_does_not_include_new_line(self):
form = Person() form = Person()
field = form['first_name'] field = form['first_name']
self.assertEqual(field.label_tag(), '<label for="id_first_name">First name:</label>')
self.assertEqual( self.assertEqual(
field.label_tag(), field.legend_tag(),
'<label for="id_first_name">First name:</label>', '<legend for="id_first_name">First name:</legend>',
) )
@override_settings(USE_THOUSAND_SEPARATOR=True) @override_settings(USE_THOUSAND_SEPARATOR=True)
@ -3539,6 +3579,10 @@ Password: <input type="password" name="password" required>
field.label_tag(attrs={'number': 9999}), field.label_tag(attrs={'number': 9999}),
'<label number="9999" for="id_first_name">First name:</label>', '<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 @jinja2_tests
@ -3747,6 +3791,43 @@ class TemplateTests(SimpleTestCase):
'<input type="submit" required>' '<input type="submit" required>'
'</form>', '</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 # 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. # given field does not have help text, nothing will be output.
t = Template( t = Template(
@ -3965,3 +4046,15 @@ class OverrideTests(SimpleTestCase):
self.assertInHTML('<th>1</th>', f.render()) self.assertInHTML('<th>1</th>', f.render())
except RecursionError: except RecursionError:
self.fail('Cyclic reference in BoundField.render().') 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>',
)

View File

@ -44,7 +44,15 @@ class FormsI18nTests(SimpleTestCase):
f = SomeForm() f = SomeForm()
self.assertHTMLEqual(f['field_1'].label_tag(), '<label for="id_field_1">field_1:</label>') 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'].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): def test_non_ascii_choices(self):
class SomeForm(Form): class SomeForm(Form):

View File

@ -185,3 +185,4 @@ class CheckboxSelectMultipleTest(WidgetTest):
bound_field = TestForm()['f'] bound_field = TestForm()['f']
self.assertEqual(bound_field.field.widget.id_for_label('id'), '') self.assertEqual(bound_field.field.widget.id_for_label('id'), '')
self.assertEqual(bound_field.label_tag(), '<label>F:</label>') self.assertEqual(bound_field.label_tag(), '<label>F:</label>')
self.assertEqual(bound_field.legend_tag(), '<legend>F:</legend>')

View File

@ -830,6 +830,18 @@ class TestFieldOverridesByFormMeta(SimpleTestCase):
str(form['slug'].label_tag()), str(form['slug'].label_tag()),
'<label for="id_slug">Slug:</label>', '<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): def test_help_text_overrides(self):
form = FieldOverridesByFormMetaForm() form = FieldOverridesByFormMetaForm()

View File

@ -1801,6 +1801,10 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
}) })
form = BookFormSet.form() form = BookFormSet.form()
self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>') 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): def test_inlineformset_factory_labels_overrides(self):
BookFormSet = inlineformset_factory(Author, Book, fields="__all__", labels={ BookFormSet = inlineformset_factory(Author, Book, fields="__all__", labels={
@ -1808,6 +1812,10 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
}) })
form = BookFormSet.form() form = BookFormSet.form()
self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>') 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): def test_modelformset_factory_help_text_overrides(self):
BookFormSet = modelformset_factory(Book, fields="__all__", help_texts={ BookFormSet = modelformset_factory(Book, fields="__all__", help_texts={