diff --git a/CHANGES b/CHANGES
index ed2ce9b..cc0cb14 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,7 @@
1.2.0 (not yet released)
------------------------
+ - (Feature) Add partial template for rendering formsets [Scott Clark, #18]
1.1.0 (2014-08-04)
------------------
diff --git a/betterforms/templates/betterforms/formset_as_fieldsets.html b/betterforms/templates/betterforms/formset_as_fieldsets.html
new file mode 100644
index 0000000..3b63f27
--- /dev/null
+++ b/betterforms/templates/betterforms/formset_as_fieldsets.html
@@ -0,0 +1,28 @@
+{% block form_head %}
+ {% if not no_head %}
+ {% if not csrf_exempt %}
+ {% csrf_token %}
+ {% endif %}
+ {% endif %}
+{% endblock %}
+
+{% block form_body %}
+ {% with formset=form %}
+ {{ formset.management_form }}
+ {% for form in formset.forms %}
+
+ {{ form.non_field_errors }}
+ {# Hack to allow recursive template inclusion #}
+ {% with fieldset_template_name="betterforms/fieldset_as_div.html" field_template_name="betterforms/field_as_div.html" %}
+ {% for thing in form %}
+ {% if thing.is_fieldset %}
+ {% include fieldset_template_name with fieldset=thing %}
+ {% else %}
+ {% include field_template_name with field=thing %}
+ {% endif %}
+ {% endfor %}
+ {% endwith %}
+
+ {% endfor %}
+ {% endwith %}
+{% endblock %}
diff --git a/docs/basics.rst b/docs/basics.rst
index 545d8f9..79a4873 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -177,3 +177,11 @@ convenient class attribute on the :class:`BetterForm` and
#18134`_.
.. _Django bug #18134: https://code.djangoproject.com/ticket/18134
+
+Betterforms also provides a partial template for rendering formsets.
+``betterforms/formset_as_fieldsets.html`` will render the management form
+for the formset as well as wrap each individual form
+in a ``div`` containing the class attributes of ``formSetForm`` and the
+unique prefix value of each form. Form fields, including additional fields
+for deleting and ordering the forms within the formset, will be rendered
+using the ``betterforms/field_as_div.html`` template.
diff --git a/tests/tests/forms.py b/tests/tests/forms.py
index ad669d8..1853ad2 100644
--- a/tests/tests/forms.py
+++ b/tests/tests/forms.py
@@ -4,6 +4,8 @@
from django.utils.datastructures import SortedDict as OrderedDict # NOQA
from django import forms
+from django.forms.formsets import formset_factory
+from django.forms.models import modelformset_factory
from django.contrib.admin import widgets as admin_widgets
from django.core.exceptions import ValidationError
@@ -66,10 +68,16 @@ class NeedsFileField(MultiForm):
class BadgeForm(forms.ModelForm):
+
class Meta:
model = Badge
fields = ('name', 'color',)
+ # self.label_suffix has to be declared with form instantiation per Django 1.6
+ def __init__(self, *args, **kwargs):
+ super(BadgeForm, self).__init__(*args, **kwargs)
+ self.label_suffix = ''
+
class BadgeMultiForm(MultiModelForm):
form_classes = {
@@ -77,6 +85,17 @@ class BadgeMultiForm(MultiModelForm):
'badge2': BadgeForm,
}
+BadgeFormSet = formset_factory(BadgeForm, extra=2)
+
+BadgeDeleteFormSet = formset_factory(BadgeForm, can_delete=True, extra=2)
+
+BadgeOrderFormSet = formset_factory(BadgeForm, can_order=True, extra=2)
+
+try:
+ BadgeModelFormSet = modelformset_factory(Badge, form=BadgeForm, extra=2)
+except PendingDeprecationWarning:
+ BadgeModelFormSet = modelformset_factory(Badge, form=BadgeForm, fields='__all__', extra=2)
+
class NonModelForm(forms.Form):
field1 = forms.CharField()
diff --git a/tests/tests/tests.py b/tests/tests/tests.py
index 4d4f2f3..9fbdbd3 100644
--- a/tests/tests/tests.py
+++ b/tests/tests/tests.py
@@ -1,11 +1,15 @@
+import unittest
+
try:
from collections import OrderedDict
except ImportError: # Python 2.6, Django < 1.7
from django.utils.datastructures import SortedDict as OrderedDict # NOQA
+import django
from django.test import TestCase
from django.test.client import RequestFactory
from django.views.generic import CreateView
+from django.template.loader import render_to_string
try:
from django.utils.encoding import force_text
@@ -16,6 +20,8 @@
from .forms import (
UserProfileMultiForm, BadgeMultiForm, ErrorMultiForm,
MixedForm, NeedsFileField, ManyToManyMultiForm,
+ BadgeFormSet, BadgeDeleteFormSet, BadgeOrderFormSet,
+ BadgeModelFormSet,
)
@@ -244,3 +250,204 @@ def test_works_with_create_view_post(self):
resp = viewfn(request)
self.assertEqual(resp.status_code, 302)
self.assertEqual(Badge.objects.count(), 2)
+
+
+class FormSetRenderTest(TestCase):
+ def setUp(self):
+ if django.VERSION < (1, 7, 0):
+ self.rendered_management_form = """
+
+ """
+ else:
+ self.rendered_management_form = """
+
+ """
+
+ def test_formset_rendering(self):
+ self.formset = BadgeFormSet(prefix="badge")
+ env = {
+ 'form': self.formset,
+ 'no_head': True,
+ }
+ self.assertHTMLEqual(
+ render_to_string('betterforms/formset_as_fieldsets.html', env),
+ self.rendered_management_form + """
+
+
+ """
+ )
+
+ def test_formset_can_delete_rendering(self):
+ self.formset = BadgeDeleteFormSet(prefix="badge")
+ env = {
+ 'form': self.formset,
+ 'no_head': True,
+ }
+ self.assertHTMLEqual(
+ render_to_string('betterforms/formset_as_fieldsets.html', env),
+ self.rendered_management_form + """
+
+
+ """
+ )
+
+ @unittest.skipIf(django.VERSION < (1, 6, 0), "Order field changed type from text to number in Django 1.6")
+ def test_formset_can_order_rendering_post_16(self):
+ self.formset = BadgeOrderFormSet(prefix="badge")
+ env = {
+ 'form': self.formset,
+ 'no_head': True,
+ }
+ self.assertHTMLEqual(
+ render_to_string('betterforms/formset_as_fieldsets.html', env),
+ self.rendered_management_form + """
+
+
+ """
+ )
+
+ @unittest.skipUnless(django.VERSION < (1, 6, 0), "Order field changed type from text to number in Django 1.6")
+ def test_formset_can_order_rendering_pre_16(self):
+ self.formset = BadgeOrderFormSet(prefix="badge")
+ env = {
+ 'form': self.formset,
+ 'no_head': True,
+ }
+ self.assertHTMLEqual(
+ render_to_string('betterforms/formset_as_fieldsets.html', env),
+ self.rendered_management_form + """
+
+
+ """
+ )
+
+ def test_modelformset_rendering(self):
+ self.formset = BadgeModelFormSet(prefix="badge")
+ env = {
+ 'form': self.formset,
+ 'no_head': True,
+ }
+ self.maxDiff = None
+ self.assertHTMLEqual(
+ render_to_string('betterforms/formset_as_fieldsets.html', env),
+ self.rendered_management_form + """
+
+
+ """
+ )