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..fd12f89
--- /dev/null
+++ b/betterforms/templates/betterforms/formset_as_fieldsets.html
@@ -0,0 +1,31 @@
+{% block form_head %}
+ {% if not no_head %}
+ {% if not csrf_exempt %}
+ {% csrf_token %}
+ {% endif %}
+ {% endif %}
+{% endblock %}
+
+{% block form_body %}
+ {% with formset=form %}
+
+ {% endwith %}
+{% endblock %}
diff --git a/docs/basics.rst b/docs/basics.rst
index 545d8f9..b708dc4 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..eca2ab3 100644
--- a/tests/tests/tests.py
+++ b/tests/tests/tests.py
@@ -1,11 +1,18 @@
+try:
+ from unittest import skipIf, skipUnless
+except ImportError: # Python 2.6, Django < 1.7
+ from django.utils.unittest import skipIf, skipUnless
+
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 +23,8 @@
from .forms import (
UserProfileMultiForm, BadgeMultiForm, ErrorMultiForm,
MixedForm, NeedsFileField, ManyToManyMultiForm,
+ BadgeFormSet, BadgeDeleteFormSet, BadgeOrderFormSet,
+ BadgeModelFormSet,
)
@@ -244,3 +253,219 @@ 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),
+ """
+
+ """.format(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),
+ """
+
+ """.format(self.rendered_management_form)
+ )
+
+ @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),
+ """
+
+ """.format(self.rendered_management_form)
+ )
+
+ @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),
+ """
+
+ """.format(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),
+ """
+
+ """.format(self.rendered_management_form)
+ )