Browse Source

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

David Smith 3 years ago
parent
commit
eba9a9b7f7

+ 14 - 1
django/forms/boundfield.py

@@ -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.

+ 1 - 1
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 %}

+ 1 - 1
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 %}

+ 28 - 8
docs/ref/forms/api.txt

@@ -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
-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()``
 ``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

+ 3 - 1
docs/releases/4.1.txt

@@ -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
 ~~~~~~~~~~~~~
 ~~~~~~~~~~~~~

+ 7 - 0
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 %}

+ 105 - 12
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):
     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>'),
-            ((mark_safe('custom&'),), {}, '<label for="id_field">custom&:</label>'),
+            (('custom&',), {}, '<%(tag)s for="id_field">custom&amp;:</%(tag)s>'),
+            ((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
-        surrounding <label>.
+        If a widget has no id, label_tag() and legend_tag() return the text
+        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(),
-            '<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)
     @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>',
+        )

+ 8 - 0
tests/forms_tests/tests/test_i18n.py

@@ -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):

+ 1 - 0
tests/forms_tests/widget_tests/test_checkboxselectmultiple.py

@@ -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>')

+ 12 - 0
tests/model_forms/tests.py

@@ -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()

+ 8 - 0
tests/model_formsets/tests.py

@@ -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={