瀏覽代碼

Fixed #32339 -- Added div.html form template.

David Smith 2 年之前
父節點
當前提交
ec5659382a

+ 1 - 0
django/forms/forms.py

@@ -66,6 +66,7 @@ class BaseForm(RenderableFormMixin):
     prefix = None
     use_required_attribute = True
 
+    template_name_div = "django/forms/div.html"
     template_name_p = "django/forms/p.html"
     template_name_table = "django/forms/table.html"
     template_name_ul = "django/forms/ul.html"

+ 1 - 0
django/forms/formsets.py

@@ -63,6 +63,7 @@ class BaseFormSet(RenderableFormMixin):
         ),
     }
 
+    template_name_div = "django/forms/formsets/div.html"
     template_name_p = "django/forms/formsets/p.html"
     template_name_table = "django/forms/formsets/table.html"
     template_name_ul = "django/forms/formsets/ul.html"

+ 24 - 0
django/forms/jinja2/django/forms/div.html

@@ -0,0 +1,24 @@
+{{ errors }}
+{% if errors and not fields %}
+  <div>{% for field in hidden_fields %}{{ field }}{% endfor %}</div>
+{% endif %}
+{% for field, errors in fields %}
+  <div{% set classes = field.css_classes() %}{% if classes %} class="{{ classes }}"{% endif %}>
+    {% if field.use_fieldset %}
+      <fieldset>
+      {% if field.label %}{{ field.legend_tag() }}{% endif %}
+    {% else %}
+      {% if field.label %}{{ field.label_tag() }}{% endif %}
+    {% endif %}
+    {% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
+    {{ errors }}
+    {{ field }}
+    {% if field.use_fieldset %}</fieldset>{% endif %}
+    {% if loop.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 %}

+ 1 - 0
django/forms/jinja2/django/forms/formsets/div.html

@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_div() }}{% endfor %}

+ 24 - 0
django/forms/templates/django/forms/div.html

@@ -0,0 +1,24 @@
+{{ errors }}
+{% if errors and not fields %}
+  <div>{% for field in hidden_fields %}{{ field }}{% endfor %}</div>
+{% endif %}
+{% for field, errors in fields %}
+  <div{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
+    {% if field.use_fieldset %}
+      <fieldset>
+      {% if field.label %}{{ field.legend_tag }}{% endif %}
+    {% else %}
+      {% if field.label %}{{ field.label_tag }}{% endif %}
+    {% endif %}
+    {% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
+    {{ errors }}
+    {{ 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 %}

+ 1 - 0
django/forms/templates/django/forms/formsets/div.html

@@ -0,0 +1 @@
+{{ formset.management_form }}{% for form in formset %}{{ form.as_div }}{% endfor %}

+ 4 - 0
django/forms/utils.py

@@ -73,6 +73,10 @@ class RenderableFormMixin(RenderableMixin):
         """Render as <li> elements excluding the surrounding <ul> tag."""
         return self.render(self.template_name_ul)
 
+    def as_div(self):
+        """Render as <div> elements."""
+        return self.render(self.template_name_div)
+
 
 class RenderableErrorMixin(RenderableMixin):
     def as_json(self, escape_html=False):

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

@@ -607,6 +607,55 @@ list using ``{{ form.as_ul }}``.
 Each helper pairs a form method with an attribute giving the appropriate
 template name.
 
+``as_div()``
+~~~~~~~~~~~~
+
+.. attribute:: Form.template_name_div
+
+.. versionadded:: 4.1
+
+The template used by ``as_div()``. Default: ``'django/forms/div.html'``.
+
+.. method:: Form.as_div()
+
+.. versionadded:: 4.1
+
+``as_div()`` renders the form as a series of ``<div>`` elements, with each
+``<div>`` containing one field, such as:
+
+.. code-block:: pycon
+
+    >>> f = ContactForm()
+    >>> f.as_div()
+
+… gives HTML like:
+
+.. code-block:: html
+
+    <div>
+    <label for="id_subject">Subject:</label>
+    <input type="text" name="subject" maxlength="100" required id="id_subject">
+    </div>
+    <div>
+    <label for="id_message">Message:</label>
+    <input type="text" name="message" required id="id_message">
+    </div>
+    <div>
+    <label for="id_sender">Sender:</label>
+    <input type="email" name="sender" required id="id_sender">
+    </div>
+    <div>
+    <label for="id_cc_myself">Cc myself:</label>
+    <input type="checkbox" name="cc_myself" id="id_cc_myself">
+    </div>
+
+.. note::
+
+    Of the framework provided templates and output styles, ``as_div()`` is
+    recommended over the ``as_p()``, ``as_table()``, and ``as_ul()`` versions
+    as the template implements ``<fieldset>`` and ``<legend>`` to group related
+    inputs and is easier for screen reader users to navigate.
+
 ``as_p()``
 ~~~~~~~~~~
 

+ 9 - 0
docs/releases/4.1.txt

@@ -258,6 +258,15 @@ Forms
   :attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
   attribute.
 
+* The new ``div.html`` form template, referencing
+  :attr:`.Form.template_name_div` attribute, and matching :meth:`.Form.as_div`
+  method, render forms using HTML ``<div>`` elements.
+
+  This new output style is recommended over the existing
+  :meth:`~.Form.as_table`, :meth:`~.Form.as_p` and :meth:`~.Form.as_ul` styles,
+  as the template implements ``<fieldset>`` and ``<legend>`` to group related
+  inputs and is easier for screen reader users to navigate.
+
 * 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`.

+ 13 - 3
docs/topics/forms/formsets.txt

@@ -800,12 +800,22 @@ Formsets have the following attributes and methods associated with rendering:
         In older versions ``template_name`` defaulted to the string value
         ``'django/forms/formset/default.html'``.
 
+
+.. attribute:: BaseFormSet.template_name_div
+
+    .. versionadded:: 4.1
+
+    The name of the template used when calling :meth:`.as_div`. By default this
+    is ``"django/forms/formsets/div.html"``. This template renders the
+    formset's management form and then each form in the formset as per the
+    form's :meth:`~django.forms.Form.as_div` method.
+
 .. attribute:: BaseFormSet.template_name_p
 
     .. versionadded:: 4.0
 
     The name of the template used when calling :meth:`.as_p`. By default this
-    is ``'django/forms/formsets/p.html'``. This template renders the formset's
+    is ``"django/forms/formsets/p.html"``. This template renders the formset's
     management form and then each form in the formset as per the form's
     :meth:`~django.forms.Form.as_p` method.
 
@@ -814,7 +824,7 @@ Formsets have the following attributes and methods associated with rendering:
     .. versionadded:: 4.0
 
     The name of the template used when calling :meth:`.as_table`. By default
-    this is ``'django/forms/formsets/table.html'``. This template renders the
+    this is ``"django/forms/formsets/table.html"``. This template renders the
     formset's management form and then each form in the formset as per the
     form's :meth:`~django.forms.Form.as_table` method.
 
@@ -823,7 +833,7 @@ Formsets have the following attributes and methods associated with rendering:
     .. versionadded:: 4.0
 
     The name of the template used when calling :meth:`.as_ul`. By default this
-    is ``'django/forms/formsets/ul.html'``. This template renders the formset's
+    is ``"django/forms/formsets/ul.html"``. This template renders the formset's
     management form and then each form in the formset as per the form's
     :meth:`~django.forms.Form.as_ul` method.
 

+ 5 - 3
docs/topics/forms/index.txt

@@ -566,12 +566,14 @@ Form rendering options
 
 There are other output options though for the ``<label>``/``<input>`` pairs:
 
+* ``{{ form.as_div }}`` will render them wrapped in ``<div>`` tags.
+
 * ``{{ form.as_table }}`` will render them as table cells wrapped in ``<tr>``
-  tags
+  tags.
 
-* ``{{ form.as_p }}`` will render them wrapped in ``<p>`` tags
+* ``{{ form.as_p }}`` will render them wrapped in ``<p>`` tags.
 
-* ``{{ form.as_ul }}`` will render them wrapped in ``<li>`` tags
+* ``{{ form.as_ul }}`` will render them wrapped in ``<li>`` tags.
 
 Note that you'll have to provide the surrounding ``<table>`` or ``<ul>``
 elements yourself.

+ 184 - 0
tests/forms_tests/tests/test_forms.py

@@ -161,6 +161,15 @@ class FormsTestCase(SimpleTestCase):
                 required></td></tr>
             """,
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="id_first_name">First name:</label><input type="text" '
+            'name="first_name" value="John" required id="id_first_name"></div><div>'
+            '<label for="id_last_name">Last name:</label><input type="text" '
+            'name="last_name" value="Lennon" required id="id_last_name"></div><div>'
+            '<label for="id_birthday">Birthday:</label><input type="text" '
+            'name="birthday" value="1940-10-9" required id="id_birthday"></div>',
+        )
 
     def test_empty_dict(self):
         # Empty dictionaries are valid, too.
@@ -219,6 +228,18 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="id_birthday">Birthday:</label>
 <input type="text" name="birthday" id="id_birthday" required></p>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="id_first_name">First name:</label>'
+            '<ul class="errorlist"><li>This field is required.</li></ul>'
+            '<input type="text" name="first_name" required id="id_first_name"></div>'
+            '<div><label for="id_last_name">Last name:</label>'
+            '<ul class="errorlist"><li>This field is required.</li></ul>'
+            '<input type="text" name="last_name" required id="id_last_name"></div><div>'
+            '<label for="id_birthday">Birthday:</label>'
+            '<ul class="errorlist"><li>This field is required.</li></ul>'
+            '<input type="text" name="birthday" required id="id_birthday"></div>',
+        )
 
     def test_empty_querydict_args(self):
         data = QueryDict()
@@ -274,6 +295,15 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="id_birthday">Birthday:</label>
 <input type="text" name="birthday" id="id_birthday" required></p>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="id_first_name">First name:</label><input type="text" '
+            'name="first_name" id="id_first_name" required></div><div><label '
+            'for="id_last_name">Last name:</label><input type="text" name="last_name" '
+            'id="id_last_name" required></div><div><label for="id_birthday">'
+            'Birthday:</label><input type="text" name="birthday" id="id_birthday" '
+            "required></div>",
+        )
 
     def test_unicode_values(self):
         # Unicode values are handled properly.
@@ -323,6 +353,17 @@ class FormsTestCase(SimpleTestCase):
             '<input type="text" name="birthday" value="1940-10-9" id="id_birthday" '
             "required></p>",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="id_first_name">First name:</label>'
+            '<input type="text" name="first_name" value="John" id="id_first_name" '
+            'required></div><div><label for="id_last_name">Last name:</label>'
+            '<input type="text" name="last_name"'
+            'value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" '
+            'id="id_last_name" required></div><div><label for="id_birthday">'
+            'Birthday:</label><input type="text" name="birthday" value="1940-10-9" '
+            'id="id_birthday" required></div>',
+        )
 
         p = Person({"last_name": "Lennon"})
         self.assertEqual(p.errors["first_name"], ["This field is required."])
@@ -439,6 +480,15 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="birthday_id">Birthday:</label>
 <input type="text" name="birthday" id="birthday_id" required></p>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="first_name_id">First name:</label><input type="text" '
+            'name="first_name" id="first_name_id" required></div><div><label '
+            'for="last_name_id">Last name:</label><input type="text" '
+            'name="last_name" id="last_name_id" required></div><div><label '
+            'for="birthday_id">Birthday:</label><input type="text" name="birthday" '
+            'id="birthday_id" required></div>',
+        )
 
     def test_auto_id_true(self):
         # If auto_id is any True value whose str() does not contain '%s', the "id"
@@ -746,6 +796,15 @@ class FormsTestCase(SimpleTestCase):
 <div><label><input type="radio" name="language" value="J" required> Java</label></div>
 </div></li>""",
         )
+        # Need an auto_id to generate legend.
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<div> Name: <input type="text" name="name" required></div><div><fieldset>'
+            'Language:<div><div><label><input type="radio" name="language" value="P" '
+            'required> Python</label></div><div><label><input type="radio" '
+            'name="language" value="J" required> Java</label></div></div></fieldset>'
+            "</div>",
+        )
 
         # Regarding auto_id and <label>, RadioSelect is a special case. Each
         # radio button gets a distinct ID, formed by appending an underscore
@@ -812,6 +871,16 @@ class FormsTestCase(SimpleTestCase):
             </div></p>
             """,
         )
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<div><label for="id_name">Name:</label><input type="text" name="name" '
+            'required id="id_name"></div><div><fieldset><legend>Language:</legend>'
+            '<div id="id_language"><div><label for="id_language_0"><input '
+            'type="radio" name="language" value="P" required id="id_language_0">'
+            'Python</label></div><div><label for="id_language_1"><input type="radio" '
+            'name="language" value="J" required id="id_language_1">Java</label></div>'
+            "</div></fieldset></div>",
+        )
 
     def test_form_with_iterable_boundfield(self):
         class BeatleForm(Form):
@@ -1022,6 +1091,14 @@ class FormsTestCase(SimpleTestCase):
             '<option value="P">Paul McCartney</option>'
             "</select></p>",
         )
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<div><label for="id_name">Name:</label><input type="text" name="name" '
+            'required id="id_name"></div><div><label for="id_composers">Composers:'
+            '</label><select name="composers" required id="id_composers" multiple>'
+            '<option value="J">John Lennon</option><option value="P">Paul McCartney'
+            "</option></select></div>",
+        )
 
     def test_multiple_checkbox_render(self):
         f = SongForm()
@@ -1064,6 +1141,16 @@ class FormsTestCase(SimpleTestCase):
             'id="id_composers_1">Paul McCartney</label></div>'
             "</div></p>",
         )
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<div><label for="id_name">Name:</label><input type="text" name="name" '
+            'required id="id_name"></div><div><fieldset><legend>Composers:</legend>'
+            '<div id="id_composers"><div><label for="id_composers_0"><input '
+            'type="checkbox" name="composers" value="J" id="id_composers_0">'
+            'John Lennon</label></div><div><label for="id_composers_1"><input '
+            'type="checkbox" name="composers" value="P" id="id_composers_1">'
+            "Paul McCartney</label></div></div></fieldset></div>",
+        )
 
     def test_form_with_disabled_fields(self):
         class PersonForm(Form):
@@ -1492,6 +1579,14 @@ class FormsTestCase(SimpleTestCase):
             <li>Password2: <input type="password" name="password2" required></li>
             """,
         )
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<ul class="errorlist nonfield"><li>Please make sure your passwords match.'
+            '</li></ul><div>Username: <input type="text" name="username" '
+            'value="adrian" maxlength="10" required></div><div>Password1: <input '
+            'type="password" name="password1" required></div><div>Password2: <input '
+            'type="password" name="password2" required></div>',
+        )
 
         f = UserRegistration(
             {"username": "adrian", "password1": "foo", "password2": "foo"},
@@ -1649,6 +1744,12 @@ class FormsTestCase(SimpleTestCase):
             "<li>(Hidden field hidden_input) This field is required.</li></ul>"
             '<p><input type="hidden" name="hidden_input" id="id_hidden_input"></p>',
         )
+        self.assertHTMLEqual(
+            f.render(f.template_name_div),
+            '<ul class="errorlist nonfield"><li>Form error</li>'
+            "<li>(Hidden field hidden_input) This field is required.</li></ul>"
+            '<div><input type="hidden" name="hidden_input" id="id_hidden_input"></div>',
+        )
 
     def test_dynamic_construction(self):
         # It's possible to construct a Form dynamically by adding to the self.fields
@@ -1893,6 +1994,13 @@ class FormsTestCase(SimpleTestCase):
             <input type="hidden" name="hidden_text"></p>
             """,
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div>First name: <input type="text" name="first_name" required></div>'
+            '<div>Last name: <input type="text" name="last_name" required></div><div>'
+            'Birthday: <input type="text" name="birthday" required><input '
+            'type="hidden" name="hidden_text"></div>',
+        )
 
         # With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label.
         p = Person(auto_id="id_%s")
@@ -1926,6 +2034,15 @@ class FormsTestCase(SimpleTestCase):
 <input type="text" name="birthday" id="id_birthday" required>
 <input type="hidden" name="hidden_text" id="id_hidden_text"></p>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div><label for="id_first_name">First name:</label><input type="text" '
+            'name="first_name" id="id_first_name" required></div><div><label '
+            'for="id_last_name">Last name:</label><input type="text" name="last_name" '
+            'id="id_last_name" required></div><div><label for="id_birthday">Birthday:'
+            '</label><input type="text" name="birthday" id="id_birthday" required>'
+            '<input type="hidden" name="hidden_text" id="id_hidden_text"></div>',
+        )
 
         # If a field with a HiddenInput has errors, the as_table() and as_ul() output
         # will include the error message(s) with the text "(Hidden field [fieldname]) "
@@ -1976,6 +2093,15 @@ class FormsTestCase(SimpleTestCase):
             <input type="hidden" name="hidden_text"></p>
             """,
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<ul class="errorlist nonfield"><li>(Hidden field hidden_text) This field '
+            'is required.</li></ul><div>First name: <input type="text" '
+            'name="first_name" value="John" required></div><div>Last name: <input '
+            'type="text" name="last_name" value="Lennon" required></div><div>'
+            'Birthday: <input type="text" name="birthday" value="1940-10-9" required>'
+            '<input type="hidden" name="hidden_text"></div>',
+        )
 
         # A corner case: It's possible for a form to have only HiddenInputs.
         class TestForm(Form):
@@ -2811,6 +2937,13 @@ Options: <select multiple name="options" required>
             <br>
             <span class="helptext">Wählen Sie mit Bedacht.</span></td></tr>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div>Username: <div class="helptext">e.g., user@example.com</div>'
+            '<input type="text" name="username" maxlength="10" required></div>'
+            '<div>Password: <div class="helptext">Wählen Sie mit Bedacht.</div>'
+            '<input type="password" name="password" required></div>',
+        )
 
         # The help text is displayed whether or not data is provided for the form.
         p = UserRegistration({"username": "foo"}, auto_id=False)
@@ -3431,6 +3564,21 @@ Password: <input type="password" name="password" required>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
 <input type="number" name="age" id="id_age" required></td></tr>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<div class="required error"><label for="id_name" class="required">Name:'
+            '</label><ul class="errorlist"><li>This field is required.</li></ul>'
+            '<input type="text" name="name" required id="id_name" /></div>'
+            '<div class="required"><label for="id_is_cool" class="required">Is cool:'
+            '</label><select name="is_cool" id="id_is_cool">'
+            '<option value="unknown" selected>Unknown</option>'
+            '<option value="true">Yes</option><option value="false">No</option>'
+            '</select></div><div><label for="id_email">Email:</label>'
+            '<input type="email" name="email" id="id_email" /></div>'
+            '<div class="required error"><label for="id_age" class="required">Age:'
+            '</label><ul class="errorlist"><li>This field is required.</li></ul>'
+            '<input type="number" name="age" required id="id_age" /></div>',
+        )
 
     def test_label_has_required_css_class(self):
         """
@@ -4027,6 +4175,13 @@ Password: <input type="password" name="password" required>
 <input id="id_first_name" name="first_name" type="text" value="John" required>
 <input id="id_last_name" name="last_name" type="hidden"></td></tr>""",
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<ul class="errorlist nonfield"><li>(Hidden field last_name) This field '
+            'is required.</li></ul><div><label for="id_first_name">First name:</label>'
+            '<input id="id_first_name" name="first_name" type="text" value="John" '
+            'required><input id="id_last_name" name="last_name" type="hidden"></div>',
+        )
 
     def test_error_list_with_non_field_errors_has_correct_class(self):
         class Person(Form):
@@ -4076,6 +4231,15 @@ Password: <input type="password" name="password" required>
             </td></tr>
             """,
         )
+        self.assertHTMLEqual(
+            p.as_div(),
+            '<ul class="errorlist nonfield"><li>Generic validation error</li></ul>'
+            '<div><label for="id_first_name">First name:</label><input '
+            'id="id_first_name" name="first_name" type="text" value="John" required>'
+            '</div><div><label for="id_last_name">Last name:</label><input '
+            'id="id_last_name" name="last_name" type="text" value="Lennon" required>'
+            "</div>",
+        )
 
     def test_error_escaping(self):
         class TestForm(Form):
@@ -4271,6 +4435,16 @@ Password: <input type="password" name="password" required>
             '<option value="J">Java</option>'
             "</select></td></tr>",
         )
+        self.assertHTMLEqual(
+            form.render(form.template_name_div),
+            '<div><label for="id_f1">F1:</label><input id="id_f1" maxlength="30" '
+            'name="f1" type="text" required></div><div><label for="id_f2">F2:</label>'
+            '<input id="id_f2" maxlength="30" name="f2" type="text"></div><div><label '
+            'for="id_f3">F3:</label><textarea cols="40" id="id_f3" name="f3" '
+            'rows="10" required></textarea></div><div><label for="id_f4">F4:</label>'
+            '<select id="id_f4" name="f4"><option value="P">Python</option>'
+            '<option value="J">Java</option></select></div>',
+        )
 
     def test_use_required_attribute_false(self):
         class MyForm(Form):
@@ -4322,6 +4496,16 @@ Password: <input type="password" name="password" required>
             '<option value="J">Java</option>'
             "</select></td></tr>",
         )
+        self.assertHTMLEqual(
+            form.render(form.template_name_div),
+            '<div><label for="id_f1">F1:</label> <input id="id_f1" maxlength="30" '
+            'name="f1" type="text"></div><div><label for="id_f2">F2:</label>'
+            '<input id="id_f2" maxlength="30" name="f2" type="text"></div><div>'
+            '<label for="id_f3">F3:</label> <textarea cols="40" id="id_f3" name="f3" '
+            'rows="10"></textarea></div><div><label for="id_f4">F4:</label>'
+            '<select id="id_f4" name="f4"><option value="P">Python</option>'
+            '<option value="J">Java</option></select></div>',
+        )
 
     def test_only_hidden_fields(self):
         # A form with *only* hidden fields that has errors is going to be very unusual.

+ 12 - 0
tests/forms_tests/tests/test_formsets.py

@@ -1597,6 +1597,18 @@ class FormsetAsTagTests(SimpleTestCase):
             ),
         )
 
+    def test_as_div(self):
+        self.assertHTMLEqual(
+            self.formset.as_div(),
+            self.management_form_html
+            + (
+                "<div>Choice: "
+                '<input type="text" name="choices-0-choice" value="Calexico"></div>'
+                '<div>Votes: <input type="number" name="choices-0-votes" value="100">'
+                "</div>"
+            ),
+        )
+
 
 @jinja2_tests
 class Jinja2FormsetAsTagTests(FormsetAsTagTests):