Ver código fonte

Refs #32339 -- Added use_fieldset to Widget.

David 3 anos atrás
pai
commit
c8459708a7
28 arquivos alterados com 489 adições e 22 exclusões
  1. 7 0
      django/forms/boundfield.py
  2. 4 0
      django/forms/widgets.py
  3. 6 0
      docs/ref/forms/api.txt
  4. 13 0
      docs/ref/forms/widgets.txt
  5. 4 0
      docs/releases/4.1.txt
  6. 26 0
      docs/topics/forms/index.txt
  7. 18 0
      tests/forms_tests/templates/forms_tests/use_fieldset.html
  8. 14 1
      tests/forms_tests/widget_tests/test_checkboxinput.py
  9. 22 1
      tests/forms_tests/widget_tests/test_checkboxselectmultiple.py
  10. 26 1
      tests/forms_tests/widget_tests/test_clearablefileinput.py
  11. 14 1
      tests/forms_tests/widget_tests/test_dateinput.py
  12. 14 1
      tests/forms_tests/widget_tests/test_datetimeinput.py
  13. 14 1
      tests/forms_tests/widget_tests/test_fileinput.py
  14. 13 1
      tests/forms_tests/widget_tests/test_hiddeninput.py
  15. 18 1
      tests/forms_tests/widget_tests/test_multiplehiddeninput.py
  16. 23 1
      tests/forms_tests/widget_tests/test_multiwidget.py
  17. 17 1
      tests/forms_tests/widget_tests/test_nullbooleanselect.py
  18. 18 3
      tests/forms_tests/widget_tests/test_numberinput.py
  19. 14 1
      tests/forms_tests/widget_tests/test_passwordinput.py
  20. 24 1
      tests/forms_tests/widget_tests/test_radioselect.py
  21. 18 1
      tests/forms_tests/widget_tests/test_select.py
  22. 43 0
      tests/forms_tests/widget_tests/test_selectdatewidget.py
  23. 19 1
      tests/forms_tests/widget_tests/test_selectmultiple.py
  24. 24 1
      tests/forms_tests/widget_tests/test_splitdatetimewidget.py
  25. 33 1
      tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py
  26. 15 1
      tests/forms_tests/widget_tests/test_textarea.py
  27. 14 1
      tests/forms_tests/widget_tests/test_textinput.py
  28. 14 1
      tests/forms_tests/widget_tests/test_timeinput.py

+ 7 - 0
django/forms/boundfield.py

@@ -286,6 +286,13 @@ class BoundField:
             r"widget$|input$", "", self.field.widget.__class__.__name__.lower()
         )
 
+    @property
+    def use_fieldset(self):
+        """
+        Return the value of this BoundField widget's use_fieldset attribute.
+        """
+        return self.field.widget.use_fieldset
+
 
 @html_safe
 class BoundWidget:

+ 4 - 0
django/forms/widgets.py

@@ -234,6 +234,7 @@ class Widget(metaclass=MediaDefiningClass):
     is_localized = False
     is_required = False
     supports_microseconds = True
+    use_fieldset = False
 
     def __init__(self, attrs=None):
         self.attrs = {} if attrs is None else attrs.copy()
@@ -821,6 +822,7 @@ class RadioSelect(ChoiceWidget):
     input_type = "radio"
     template_name = "django/forms/widgets/radio.html"
     option_template_name = "django/forms/widgets/radio_option.html"
+    use_fieldset = True
 
     def id_for_label(self, id_, index=None):
         """
@@ -862,6 +864,7 @@ class MultiWidget(Widget):
     """
 
     template_name = "django/forms/widgets/multiwidget.html"
+    use_fieldset = True
 
     def __init__(self, widgets, attrs=None):
         if isinstance(widgets, dict):
@@ -1027,6 +1030,7 @@ class SelectDateWidget(Widget):
     input_type = "select"
     select_widget = Select
     date_re = _lazy_re_compile(r"(\d{4}|0)-(\d\d?)-(\d\d?)$")
+    use_fieldset = True
 
     def __init__(self, attrs=None, years=None, months=None, empty_label=None):
         self.attrs = attrs or {}

+ 6 - 0
docs/ref/forms/api.txt

@@ -1160,6 +1160,12 @@ Attributes of ``BoundField``
         >>> print(f['message'].name)
         message
 
+.. attribute:: BoundField.use_fieldset
+
+    .. versionadded:: 4.1
+
+    Returns the value of this BoundField widget's ``use_fieldset`` attribute.
+
 .. attribute:: BoundField.widget_type
 
     Returns the lowercased class name of the wrapped field's widget, with any

+ 13 - 0
docs/ref/forms/widgets.txt

@@ -315,6 +315,19 @@ foundation for custom widgets.
         ``<select multiple>`` don't appear in the data of an HTML form
         submission, so it's unknown whether or not the user submitted a value.
 
+    .. attribute:: Widget.use_fieldset
+
+        .. versionadded:: 4.1
+
+        An attribute to identify if the widget should be grouped in a
+        ``<fieldset>`` with a ``<legend>`` when rendered. Defaults to ``False``
+        but is ``True`` when the widget contains multiple ``<input>`` tags such as
+        :class:`~django.forms.CheckboxSelectMultiple`,
+        :class:`~django.forms.RadioSelect`,
+        :class:`~django.forms.MultiWidget`,
+        :class:`~django.forms.SplitDateTimeWidget`, and
+        :class:`~django.forms.SelectDateWidget`.
+
     .. method:: use_required_attribute(initial)
 
         Given a form field's ``initial`` value, returns whether or not the

+ 4 - 0
docs/releases/4.1.txt

@@ -217,6 +217,10 @@ Forms
   objects implement the ``__html__()`` method (typically when decorated with
   the :func:`~django.utils.html.html_safe` decorator).
 
+* The new :attr:`.BoundField.use_fieldset` and :attr:`.Widget.use_fieldset`
+  attributes help to identify widgets where its inputs should be grouped in a
+  ``<fieldset>`` with a ``<legend>``.
+
 Generic Views
 ~~~~~~~~~~~~~
 

+ 26 - 0
docs/topics/forms/index.txt

@@ -686,6 +686,32 @@ Useful attributes on ``{{ field }}`` include:
 
         <label for="id_email">Email address:</label>
 
+``{{ field.legend_tag }}``
+
+    .. versionadded:: 4.1
+
+    Similar to ``field.label_tag`` but uses a ``<legend>`` tag in place of
+    ``<label>``, for widgets with multiple inputs wrapped in a ``<fieldset>``.
+
+``{{ field.use_fieldset }}``
+
+    .. versionadded:: 4.1
+
+    This attribute is ``True`` if the form field's widget contains multiple
+    inputs that should be semantically grouped in a ``<fieldset>`` with a
+    ``<legend>`` to improve accessibility. An example use in a template:
+
+.. code-block:: html+django
+
+    {% if field.use_fieldset %}
+      <fieldset>
+      {% if field.label %}{{ field.legend_tag }}{% endif %}
+    {% else %}
+      {% if field.label %}{{ field.label_tag }}{% endif %}
+    {% endif %}
+    {{ field }}
+    {% if field.use_fieldset %}</fieldset>{% endif %}
+
 ``{{ field.value }}``
     The value of the field. e.g ``someone@example.com``.
 

+ 18 - 0
tests/forms_tests/templates/forms_tests/use_fieldset.html

@@ -0,0 +1,18 @@
+{% for field, errors in fields %}
+  <div>
+    {% if field.use_fieldset %}
+      <fieldset>
+      {% if field.label %}{{ field.legend_tag }}{% endif %}
+    {% else %}
+      {% if field.label %}{{ field.label_tag }}{% endif %}
+    {% endif %}
+    {{ field }}
+    {% if field.use_fieldset %}</fieldset>{% endif %}
+    {% if forloop.last %}
+      {% for field in hidden_fields %}{{ field }}{% endfor %}
+    {% endif %}
+  </div>
+{% endfor %}
+{% if not fields and not errors %}
+  {% for field in hidden_fields %}{{ field }}{% endfor %}
+{% endif %}

+ 14 - 1
tests/forms_tests/widget_tests/test_checkboxinput.py

@@ -1,4 +1,4 @@
-from django.forms import CheckboxInput
+from django.forms import BooleanField, CheckboxInput, Form
 
 from .base import WidgetTest
 
@@ -124,3 +124,16 @@ class CheckboxInputTest(WidgetTest):
         attrs = {"checked": False}
         self.widget.get_context("name", True, attrs)
         self.assertIs(attrs["checked"], False)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = BooleanField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            form.render(),
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" name="field" required type="checkbox"></div>',
+        )

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

@@ -1,7 +1,7 @@
 import datetime
 
 from django import forms
-from django.forms import CheckboxSelectMultiple
+from django.forms import CheckboxSelectMultiple, ChoiceField, Form
 from django.test import override_settings
 
 from .base import WidgetTest
@@ -254,3 +254,24 @@ class CheckboxSelectMultipleTest(WidgetTest):
         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>")
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = ChoiceField(widget=self.widget, choices=self.beatles)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            form.render(),
+            '<div><fieldset><legend>Field:</legend><div id="id_field">'
+            '<div><label for="id_field_0"><input type="checkbox" '
+            'name="field" value="J" id="id_field_0"> John</label></div>'
+            '<div><label for="id_field_1"><input type="checkbox" '
+            'name="field" value="P" id="id_field_1">Paul</label></div>'
+            '<div><label for="id_field_2"><input type="checkbox" '
+            'name="field" value="G" id="id_field_2"> George</label></div>'
+            '<div><label for="id_field_3"><input type="checkbox" '
+            'name="field" value="R" id="id_field_3">'
+            "Ringo</label></div></div></fieldset></div>",
+        )

+ 26 - 1
tests/forms_tests/widget_tests/test_clearablefileinput.py

@@ -1,5 +1,5 @@
 from django.core.files.uploadedfile import SimpleUploadedFile
-from django.forms import ClearableFileInput, MultiWidget
+from django.forms import ClearableFileInput, FileField, Form, MultiWidget
 
 from .base import WidgetTest
 
@@ -207,3 +207,28 @@ class ClearableFileInputTest(WidgetTest):
         self.assertIs(
             widget.value_omitted_from_data({"field-clear": "y"}, {}, "field"), False
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = FileField(widget=self.widget)
+            with_file = FileField(widget=self.widget, initial=FakeFieldFile())
+            clearable_file = FileField(
+                widget=self.widget, initial=FakeFieldFile(), required=False
+            )
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" name="field" type="file" required></div>'
+            '<div><label for="id_with_file">With file:</label>Currently: '
+            '<a href="something">something</a><br>Change:<input type="file" '
+            'name="with_file" id="id_with_file"></div>'
+            '<div><label for="id_clearable_file">Clearable file:</label>'
+            'Currently: <a href="something">something</a><input '
+            'type="checkbox" name="clearable_file-clear" id="clearable_file-clear_id">'
+            '<label for="clearable_file-clear_id">Clear</label><br>Change:'
+            '<input type="file" name="clearable_file" id="id_clearable_file"></div>',
+            form.render(),
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_dateinput.py

@@ -1,6 +1,6 @@
 from datetime import date
 
-from django.forms import DateInput
+from django.forms import CharField, DateInput, Form
 from django.utils import translation
 
 from .base import WidgetTest
@@ -60,3 +60,16 @@ class DateInputTest(WidgetTest):
             date(2007, 9, 17),
             html='<input type="text" name="date" value="17.09.2007">',
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            form.render(),
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" name="field" required type="text"></div>',
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_datetimeinput.py

@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from django.forms import DateTimeInput
+from django.forms import CharField, DateTimeInput, Form
 from django.test import ignore_warnings
 from django.utils import translation
 from django.utils.deprecation import RemovedInDjango50Warning
@@ -97,3 +97,16 @@ class DateTimeInputTest(WidgetTest):
                 d,
                 html='<input type="text" name="date" value="17/09/2007 12:51:34">',
             )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" name="field" required type="text"></div>',
+            form.render(),
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_fileinput.py

@@ -1,4 +1,4 @@
-from django.forms import FileInput
+from django.forms import FileField, FileInput, Form
 
 from .base import WidgetTest
 
@@ -35,3 +35,16 @@ class FileInputTest(WidgetTest):
         # user to keep the existing, initial value.
         self.assertIs(self.widget.use_required_attribute(None), True)
         self.assertIs(self.widget.use_required_attribute("resume.txt"), False)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = FileField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label><input id="id_field" '
+            'name="field" required type="file"></div>',
+            form.render(),
+        )

+ 13 - 1
tests/forms_tests/widget_tests/test_hiddeninput.py

@@ -1,4 +1,4 @@
-from django.forms import HiddenInput
+from django.forms import CharField, Form, HiddenInput
 
 from .base import WidgetTest
 
@@ -17,3 +17,15 @@ class HiddenInputTest(WidgetTest):
         self.assertIs(self.widget.use_required_attribute(None), False)
         self.assertIs(self.widget.use_required_attribute(""), False)
         self.assertIs(self.widget.use_required_attribute("foo"), False)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<input type="hidden" name="field" id="id_field">',
+            form.render(),
+        )

+ 18 - 1
tests/forms_tests/widget_tests/test_multiplehiddeninput.py

@@ -1,4 +1,5 @@
-from django.forms import MultipleHiddenInput
+from django.forms import Form, MultipleChoiceField, MultipleHiddenInput
+from django.utils.datastructures import MultiValueDict
 
 from .base import WidgetTest
 
@@ -104,3 +105,19 @@ class MultipleHiddenInputTest(WidgetTest):
                 '<input type="hidden" name="letters" value="c" id="hideme_2">'
             ),
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            composers = MultipleChoiceField(
+                choices=[("J", "John Lennon"), ("P", "Paul McCartney")],
+                widget=MultipleHiddenInput,
+            )
+
+        form = TestForm(MultiValueDict({"composers": ["J", "P"]}))
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<input type="hidden" name="composers" value="J" id="id_composers_0">'
+            '<input type="hidden" name="composers" value="P" id="id_composers_1">',
+            form.render(),
+        )

+ 23 - 1
tests/forms_tests/widget_tests/test_multiwidget.py

@@ -4,6 +4,7 @@ from datetime import datetime
 from django.forms import (
     CharField,
     FileInput,
+    Form,
     MultipleChoiceField,
     MultiValueField,
     MultiWidget,
@@ -51,7 +52,9 @@ class ComplexField(MultiValueField):
             MultipleChoiceField(choices=WidgetTest.beatles),
             SplitDateTimeField(),
         )
-        super().__init__(fields, required, widget, label, initial)
+        super().__init__(
+            fields, required=required, widget=widget, label=label, initial=initial
+        )
 
     def compress(self, data_list):
         if data_list:
@@ -296,3 +299,22 @@ class MultiWidgetTest(WidgetTest):
         # w2 ought to be independent of w1, since MultiWidget ought
         # to make a copy of its sub-widgets when it is copied.
         self.assertEqual(w1.choices, [1, 2, 3])
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = ComplexField(widget=ComplexMultiWidget)
+
+        form = TestForm()
+        self.assertIs(form["field"].field.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            "<div><fieldset><legend>Field:</legend>"
+            '<input type="text" name="field_0" required id="id_field_0">'
+            '<select name="field_1" required id="id_field_1" multiple>'
+            '<option value="J">John</option><option value="P">Paul</option>'
+            '<option value="G">George</option><option value="R">Ringo</option></select>'
+            '<input type="text" name="field_2_0" required id="id_field_2_0">'
+            '<input type="text" name="field_2_1" required id="id_field_2_1">'
+            "</fieldset></div>",
+            form.render(),
+        )

+ 17 - 1
tests/forms_tests/widget_tests/test_nullbooleanselect.py

@@ -1,4 +1,4 @@
-from django.forms import NullBooleanSelect
+from django.forms import Form, NullBooleanField, NullBooleanSelect
 from django.utils import translation
 
 from .base import WidgetTest
@@ -154,3 +154,19 @@ class NullBooleanSelectTest(WidgetTest):
                 """
                 ),
             )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = NullBooleanField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<select name="field" id="id_field">'
+            '<option value="unknown" selected>Unknown</option>'
+            '<option value="true">Yes</option>'
+            '<option value="false">No</option></select></div>',
+            form.render(),
+        )

+ 18 - 3
tests/forms_tests/widget_tests/test_numberinput.py

@@ -1,17 +1,32 @@
-from django.forms.widgets import NumberInput
+from django.forms import CharField, Form, NumberInput
 from django.test import override_settings
 
 from .base import WidgetTest
 
 
 class NumberInputTests(WidgetTest):
+    widget = NumberInput(attrs={"max": 12345, "min": 1234, "step": 9999})
+
     @override_settings(USE_THOUSAND_SEPARATOR=True)
     def test_attrs_not_localized(self):
-        widget = NumberInput(attrs={"max": 12345, "min": 1234, "step": 9999})
         self.check_html(
-            widget,
+            self.widget,
             "name",
             "value",
             '<input type="number" name="name" value="value" max="12345" min="1234" '
             'step="9999">',
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" max="12345" min="1234" '
+            'name="field" required step="9999" type="number"></div>',
+            form.render(),
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_passwordinput.py

@@ -1,4 +1,4 @@
-from django.forms import PasswordInput
+from django.forms import CharField, Form, PasswordInput
 
 from .base import WidgetTest
 
@@ -37,3 +37,16 @@ class PasswordInputTest(WidgetTest):
             "test@example.com",
             html='<input type="password" name="password" value="test@example.com">',
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input type="password" name="field" required id="id_field"></div>',
+            form.render(),
+        )

+ 24 - 1
tests/forms_tests/widget_tests/test_radioselect.py

@@ -1,6 +1,6 @@
 import datetime
 
-from django.forms import MultiWidget, RadioSelect
+from django.forms import ChoiceField, Form, MultiWidget, RadioSelect
 from django.test import override_settings
 
 from .base import WidgetTest
@@ -199,3 +199,26 @@ class RadioSelectTest(WidgetTest):
             </div>
         """,
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = ChoiceField(
+                widget=self.widget, choices=self.beatles, required=False
+            )
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            '<div><fieldset><legend>Field:</legend><div id="id_field">'
+            '<div><label for="id_field_0">'
+            '<input type="radio" name="field" value="J" id="id_field_0"> John'
+            '</label></div><div><label for="id_field_1">'
+            '<input type="radio" name="field" value="P" id="id_field_1">Paul'
+            '</label></div><div><label for="id_field_2"><input type="radio" '
+            'name="field" value="G" id="id_field_2"> George</label></div>'
+            '<div><label for="id_field_3"><input type="radio" name="field" '
+            'value="R" id="id_field_3">Ringo</label></div></div></fieldset>'
+            "</div>",
+            form.render(),
+        )

+ 18 - 1
tests/forms_tests/widget_tests/test_select.py

@@ -1,7 +1,7 @@
 import copy
 import datetime
 
-from django.forms import Select
+from django.forms import ChoiceField, Form, Select
 from django.test import override_settings
 from django.utils.safestring import mark_safe
 
@@ -485,3 +485,20 @@ class SelectTest(WidgetTest):
     def test_doesnt_render_required_when_no_choices_are_available(self):
         widget = self.widget(choices=[])
         self.assertIs(widget.use_required_attribute(initial=None), False)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = ChoiceField(widget=self.widget, choices=self.beatles)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<select name="field" id="id_field">'
+            '<option value="J">John</option>  '
+            '<option value="P">Paul</option>'
+            '<option value="G">George</option>'
+            '<option value="R">Ringo</option></select></div>',
+            form.render(),
+        )

+ 43 - 0
tests/forms_tests/widget_tests/test_selectdatewidget.py

@@ -707,3 +707,46 @@ class SelectDateWidgetTest(WidgetTest):
             """
             ),
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = DateField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            '<div><fieldset><legend for="id_field_month">Field:</legend>'
+            '<select name="field_month" required id="id_field_month">'
+            '<option value="1">January</option><option value="2">February</option>'
+            '<option value="3">March</option><option value="4">April</option>'
+            '<option value="5">May</option><option value="6">June</option>'
+            '<option value="7">July</option><option value="8">August</option>'
+            '<option value="9">September</option><option value="10">October</option>'
+            '<option value="11">November</option><option value="12">December</option>'
+            '</select><select name="field_day" required id="id_field_day">'
+            '<option value="1">1</option><option value="2">2</option>'
+            '<option value="3">3</option><option value="4">4</option>'
+            '<option value="5">5</option><option value="6">6</option>'
+            '<option value="7">7</option><option value="8">8</option>'
+            '<option value="9">9</option><option value="10">10</option>'
+            '<option value="11">11</option><option value="12">12</option>'
+            '<option value="13">13</option><option value="14">14</option>'
+            '<option value="15">15</option><option value="16">16</option>'
+            '<option value="17">17</option><option value="18">18</option>'
+            '<option value="19">19</option><option value="20">20</option>'
+            '<option value="21">21</option><option value="22">22</option>'
+            '<option value="23">23</option><option value="24">24</option>'
+            '<option value="25">25</option><option value="26">26</option>'
+            '<option value="27">27</option><option value="28">28</option>'
+            '<option value="29">29</option><option value="30">30</option>'
+            '<option value="31">31</option></select>'
+            '<select name="field_year" required id="id_field_year">'
+            '<option value="2007">2007</option><option value="2008">2008</option>'
+            '<option value="2009">2009</option><option value="2010">2010</option>'
+            '<option value="2011">2011</option><option value="2012">2012</option>'
+            '<option value="2013">2013</option><option value="2014">2014</option>'
+            '<option value="2015">2015</option><option value="2016">2016</option>'
+            "</select></fieldset></div>",
+            form.render(),
+        )

+ 19 - 1
tests/forms_tests/widget_tests/test_selectmultiple.py

@@ -1,4 +1,4 @@
-from django.forms import SelectMultiple
+from django.forms import ChoiceField, Form, SelectMultiple
 
 from .base import WidgetTest
 
@@ -190,3 +190,21 @@ class SelectMultipleTest(WidgetTest):
         self.assertIs(
             widget.value_omitted_from_data({"field": "value"}, {}, "field"), False
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = ChoiceField(
+                widget=self.widget, choices=self.beatles, required=False
+            )
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<select multiple name="field" id="id_field">'
+            '<option value="J">John</option>  <option value="P">Paul</option>'
+            '<option value="G">George</option><option value="R">Ringo'
+            "</option></select></div>",
+            form.render(),
+        )

+ 24 - 1
tests/forms_tests/widget_tests/test_splitdatetimewidget.py

@@ -1,6 +1,6 @@
 from datetime import date, datetime, time
 
-from django.forms import SplitDateTimeWidget
+from django.forms import Form, SplitDateTimeField, SplitDateTimeWidget
 
 from .base import WidgetTest
 
@@ -94,3 +94,26 @@ class SplitDateTimeWidgetTest(WidgetTest):
                 '<input type="text" name="date_1" value="07:30">'
             ),
         )
+        self.check_html(
+            widget,
+            "date",
+            datetime(2006, 1, 10, 7, 30),
+            html=(
+                '<input type="text" name="date_0" value="10/01/2006">'
+                '<input type="text" name="date_1" value="07:30">'
+            ),
+        )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = SplitDateTimeField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            '<div><fieldset><legend>Field:</legend><input type="text" '
+            'name="field_0" required id="id_field_0"><input type="text" '
+            'name="field_1" required id="id_field_1"></fieldset></div>',
+            form.render(),
+        )

+ 33 - 1
tests/forms_tests/widget_tests/test_splithiddendatetimewidget.py

@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from django.forms import SplitHiddenDateTimeWidget
+from django.forms import Form, SplitDateTimeField, SplitHiddenDateTimeWidget
 from django.utils import translation
 
 from .base import WidgetTest
@@ -81,3 +81,35 @@ class SplitHiddenDateTimeWidgetTest(WidgetTest):
             time_attrs={"class": "bar"}, attrs={"class": "foo"}
         )
         self.check_html(widget, "date", datetime(2006, 1, 10, 7, 30), html=html)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = SplitDateTimeField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            '<input type="hidden" name="field_0" id="id_field_0">'
+            '<input type="hidden" name="field_1" id="id_field_1">',
+            form.render(),
+        )
+
+    def test_fieldset_with_unhidden_field(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            hidden_field = SplitDateTimeField(widget=self.widget)
+            unhidden_field = SplitDateTimeField()
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, True)
+        self.assertHTMLEqual(
+            "<div><fieldset><legend>Unhidden field:</legend>"
+            '<input type="text" name="unhidden_field_0" required '
+            'id="id_unhidden_field_0"><input type="text" '
+            'name="unhidden_field_1" required id="id_unhidden_field_1">'
+            '</fieldset><input type="hidden" name="hidden_field_0" '
+            'id="id_hidden_field_0"><input type="hidden" '
+            'name="hidden_field_1" id="id_hidden_field_1"></div>',
+            form.render(),
+        )

+ 15 - 1
tests/forms_tests/widget_tests/test_textarea.py

@@ -1,4 +1,4 @@
-from django.forms import Textarea
+from django.forms import CharField, Form, Textarea
 from django.utils.safestring import mark_safe
 
 from .base import WidgetTest
@@ -62,3 +62,17 @@ class TextareaTest(WidgetTest):
                 "</textarea>"
             ),
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<textarea cols="40" id="id_field" name="field" '
+            'required rows="10"></textarea></div>',
+            form.render(),
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_textinput.py

@@ -1,4 +1,4 @@
-from django.forms import TextInput
+from django.forms import CharField, Form, TextInput
 from django.utils.safestring import mark_safe
 
 from .base import WidgetTest
@@ -119,3 +119,16 @@ class TextInputTest(WidgetTest):
         self.assertIs(self.widget.use_required_attribute(None), True)
         self.assertIs(self.widget.use_required_attribute(""), True)
         self.assertIs(self.widget.use_required_attribute("resume.txt"), True)
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input type="text" name="field" required id="id_field"></div>',
+            form.render(),
+        )

+ 14 - 1
tests/forms_tests/widget_tests/test_timeinput.py

@@ -1,6 +1,6 @@
 from datetime import time
 
-from django.forms import TimeInput
+from django.forms import CharField, Form, TimeInput
 from django.utils import translation
 
 from .base import WidgetTest
@@ -67,3 +67,16 @@ class TimeInputTest(WidgetTest):
             t,
             html='<input type="text" name="time" value="12:51:34">',
         )
+
+    def test_fieldset(self):
+        class TestForm(Form):
+            template_name = "forms_tests/use_fieldset.html"
+            field = CharField(widget=self.widget)
+
+        form = TestForm()
+        self.assertIs(self.widget.use_fieldset, False)
+        self.assertHTMLEqual(
+            '<div><label for="id_field">Field:</label>'
+            '<input id="id_field" name="field" required type="text"></div>',
+            form.render(),
+        )