Преглед изворни кода

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
     prefix = None
     use_required_attribute = True
     use_required_attribute = True
 
 
+    template_name_div = "django/forms/div.html"
     template_name_p = "django/forms/p.html"
     template_name_p = "django/forms/p.html"
     template_name_table = "django/forms/table.html"
     template_name_table = "django/forms/table.html"
     template_name_ul = "django/forms/ul.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_p = "django/forms/formsets/p.html"
     template_name_table = "django/forms/formsets/table.html"
     template_name_table = "django/forms/formsets/table.html"
     template_name_ul = "django/forms/formsets/ul.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."""
         """Render as <li> elements excluding the surrounding <ul> tag."""
         return self.render(self.template_name_ul)
         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):
 class RenderableErrorMixin(RenderableMixin):
     def as_json(self, escape_html=False):
     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
 Each helper pairs a form method with an attribute giving the appropriate
 template name.
 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()``
 ``as_p()``
 ~~~~~~~~~~
 ~~~~~~~~~~
 
 

+ 9 - 0
docs/releases/4.1.txt

@@ -258,6 +258,15 @@ Forms
   :attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
   :attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
   attribute.
   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
 * The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field
   labels in ``<legend>`` tags via the new ``tag`` argument of
   labels in ``<legend>`` tags via the new ``tag`` argument of
   :meth:`~django.forms.BoundField.label_tag`.
   :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
         In older versions ``template_name`` defaulted to the string value
         ``'django/forms/formset/default.html'``.
         ``'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
 .. attribute:: BaseFormSet.template_name_p
 
 
     .. versionadded:: 4.0
     .. versionadded:: 4.0
 
 
     The name of the template used when calling :meth:`.as_p`. By default this
     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
     management form and then each form in the formset as per the form's
     :meth:`~django.forms.Form.as_p` method.
     :meth:`~django.forms.Form.as_p` method.
 
 
@@ -814,7 +824,7 @@ Formsets have the following attributes and methods associated with rendering:
     .. versionadded:: 4.0
     .. versionadded:: 4.0
 
 
     The name of the template used when calling :meth:`.as_table`. By default
     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
     formset's management form and then each form in the formset as per the
     form's :meth:`~django.forms.Form.as_table` method.
     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
     .. versionadded:: 4.0
 
 
     The name of the template used when calling :meth:`.as_ul`. By default this
     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
     management form and then each form in the formset as per the form's
     :meth:`~django.forms.Form.as_ul` method.
     :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:
 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>``
 * ``{{ 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>``
 Note that you'll have to provide the surrounding ``<table>`` or ``<ul>``
 elements yourself.
 elements yourself.

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

@@ -161,6 +161,15 @@ class FormsTestCase(SimpleTestCase):
                 required></td></tr>
                 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):
     def test_empty_dict(self):
         # Empty dictionaries are valid, too.
         # Empty dictionaries are valid, too.
@@ -219,6 +228,18 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="id_birthday">Birthday:</label>
 <p><label for="id_birthday">Birthday:</label>
 <input type="text" name="birthday" id="id_birthday" required></p>""",
 <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):
     def test_empty_querydict_args(self):
         data = QueryDict()
         data = QueryDict()
@@ -274,6 +295,15 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="id_birthday">Birthday:</label>
 <p><label for="id_birthday">Birthday:</label>
 <input type="text" name="birthday" id="id_birthday" required></p>""",
 <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):
     def test_unicode_values(self):
         # Unicode values are handled properly.
         # Unicode values are handled properly.
@@ -323,6 +353,17 @@ class FormsTestCase(SimpleTestCase):
             '<input type="text" name="birthday" value="1940-10-9" id="id_birthday" '
             '<input type="text" name="birthday" value="1940-10-9" id="id_birthday" '
             "required></p>",
             "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"})
         p = Person({"last_name": "Lennon"})
         self.assertEqual(p.errors["first_name"], ["This field is required."])
         self.assertEqual(p.errors["first_name"], ["This field is required."])
@@ -439,6 +480,15 @@ class FormsTestCase(SimpleTestCase):
 <p><label for="birthday_id">Birthday:</label>
 <p><label for="birthday_id">Birthday:</label>
 <input type="text" name="birthday" id="birthday_id" required></p>""",
 <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):
     def test_auto_id_true(self):
         # If auto_id is any True value whose str() does not contain '%s', the "id"
         # 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><label><input type="radio" name="language" value="J" required> Java</label></div>
 </div></li>""",
 </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
         # Regarding auto_id and <label>, RadioSelect is a special case. Each
         # radio button gets a distinct ID, formed by appending an underscore
         # radio button gets a distinct ID, formed by appending an underscore
@@ -812,6 +871,16 @@ class FormsTestCase(SimpleTestCase):
             </div></p>
             </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):
     def test_form_with_iterable_boundfield(self):
         class BeatleForm(Form):
         class BeatleForm(Form):
@@ -1022,6 +1091,14 @@ class FormsTestCase(SimpleTestCase):
             '<option value="P">Paul McCartney</option>'
             '<option value="P">Paul McCartney</option>'
             "</select></p>",
             "</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):
     def test_multiple_checkbox_render(self):
         f = SongForm()
         f = SongForm()
@@ -1064,6 +1141,16 @@ class FormsTestCase(SimpleTestCase):
             'id="id_composers_1">Paul McCartney</label></div>'
             'id="id_composers_1">Paul McCartney</label></div>'
             "</div></p>",
             "</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):
     def test_form_with_disabled_fields(self):
         class PersonForm(Form):
         class PersonForm(Form):
@@ -1492,6 +1579,14 @@ class FormsTestCase(SimpleTestCase):
             <li>Password2: <input type="password" name="password2" required></li>
             <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(
         f = UserRegistration(
             {"username": "adrian", "password1": "foo", "password2": "foo"},
             {"username": "adrian", "password1": "foo", "password2": "foo"},
@@ -1649,6 +1744,12 @@ class FormsTestCase(SimpleTestCase):
             "<li>(Hidden field hidden_input) This field is required.</li></ul>"
             "<li>(Hidden field hidden_input) This field is required.</li></ul>"
             '<p><input type="hidden" name="hidden_input" id="id_hidden_input"></p>',
             '<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):
     def test_dynamic_construction(self):
         # It's possible to construct a Form dynamically by adding to the self.fields
         # 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>
             <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.
         # With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label.
         p = Person(auto_id="id_%s")
         p = Person(auto_id="id_%s")
@@ -1926,6 +2034,15 @@ class FormsTestCase(SimpleTestCase):
 <input type="text" name="birthday" id="id_birthday" required>
 <input type="text" name="birthday" id="id_birthday" required>
 <input type="hidden" name="hidden_text" id="id_hidden_text"></p>""",
 <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
         # 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]) "
         # 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>
             <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.
         # A corner case: It's possible for a form to have only HiddenInputs.
         class TestForm(Form):
         class TestForm(Form):
@@ -2811,6 +2937,13 @@ Options: <select multiple name="options" required>
             <br>
             <br>
             <span class="helptext">Wählen Sie mit Bedacht.</span></td></tr>""",
             <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.
         # The help text is displayed whether or not data is provided for the form.
         p = UserRegistration({"username": "foo"}, auto_id=False)
         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>
 <td><ul class="errorlist"><li>This field is required.</li></ul>
 <input type="number" name="age" id="id_age" required></td></tr>""",
 <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):
     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_first_name" name="first_name" type="text" value="John" required>
 <input id="id_last_name" name="last_name" type="hidden"></td></tr>""",
 <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):
     def test_error_list_with_non_field_errors_has_correct_class(self):
         class Person(Form):
         class Person(Form):
@@ -4076,6 +4231,15 @@ Password: <input type="password" name="password" required>
             </td></tr>
             </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):
     def test_error_escaping(self):
         class TestForm(Form):
         class TestForm(Form):
@@ -4271,6 +4435,16 @@ Password: <input type="password" name="password" required>
             '<option value="J">Java</option>'
             '<option value="J">Java</option>'
             "</select></td></tr>",
             "</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):
     def test_use_required_attribute_false(self):
         class MyForm(Form):
         class MyForm(Form):
@@ -4322,6 +4496,16 @@ Password: <input type="password" name="password" required>
             '<option value="J">Java</option>'
             '<option value="J">Java</option>'
             "</select></td></tr>",
             "</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):
     def test_only_hidden_fields(self):
         # A form with *only* hidden fields that has errors is going to be very unusual.
         # 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
 @jinja2_tests
 class Jinja2FormsetAsTagTests(FormsetAsTagTests):
 class Jinja2FormsetAsTagTests(FormsetAsTagTests):