Skip to content

Commit

Permalink
Add formset_as_fieldsets template and tests
Browse files Browse the repository at this point in the history
This creates a partial template which allows for ease of
rendering formsets. The template outputs the management
form, and wraps each form in a div with appropriate form
prefix class attributes.
  • Loading branch information
Scott Clark committed Aug 22, 2014
1 parent 79de01f commit 3817362
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 0 deletions.
1 change: 1 addition & 0 deletions 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)
------------------
Expand Down
31 changes: 31 additions & 0 deletions 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 %}
<div class="formSet {{ formset.prefix }}">
{{ formset.management_form }}
{{ formset.non_form_errors }}
{% for form in formset.forms %}
<div class="formSetForm {{ form.prefix }}">
{{ 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 %}
</div>
{% endfor %}
</div>
{% endwith %}
{% endblock %}
8 changes: 8 additions & 0 deletions docs/basics.rst
Expand Up @@ -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.
19 changes: 19 additions & 0 deletions tests/tests/forms.py
Expand Up @@ -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

Expand Down Expand Up @@ -66,17 +68,34 @@ 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 = {
'badge1': BadgeForm,
'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()
Expand Down
225 changes: 225 additions & 0 deletions 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
Expand All @@ -16,6 +23,8 @@
from .forms import (
UserProfileMultiForm, BadgeMultiForm, ErrorMultiForm,
MixedForm, NeedsFileField, ManyToManyMultiForm,
BadgeFormSet, BadgeDeleteFormSet, BadgeOrderFormSet,
BadgeModelFormSet,
)


Expand Down Expand Up @@ -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 = """
<input id="id_badge-TOTAL_FORMS" name="badge-TOTAL_FORMS" type="hidden" value="2" /><input id="id_badge-INITIAL_FORMS" name="badge-INITIAL_FORMS" type="hidden" value="0" /><input id="id_badge-MAX_NUM_FORMS" name="badge-MAX_NUM_FORMS" type="hidden" value="1000" />
"""
else:
self.rendered_management_form = """
<input id="id_badge-TOTAL_FORMS" name="badge-TOTAL_FORMS" type="hidden" value="2" /><input id="id_badge-INITIAL_FORMS" name="badge-INITIAL_FORMS" type="hidden" value="0" /><input id="id_badge-MIN_NUM_FORMS" name="badge-MIN_NUM_FORMS" type="hidden" value="0" /><input id="id_badge-MAX_NUM_FORMS" name="badge-MAX_NUM_FORMS" type="hidden" value="1000" />
"""

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),
"""
<div class="formSet badge">
{0}
<div class="formSetForm badge-0">
<div class="badge-0-name formField required">
<label for="id_badge-0-name">Name</label>
<input id="id_badge-0-name" maxlength="255" name="badge-0-name" type="text" />
</div>
<div class="badge-0-color formField required">
<label for="id_badge-0-color">Color</label>
<input id="id_badge-0-color" maxlength="20" name="badge-0-color" type="text" />
</div>
</div>
<div class="formSetForm badge-1">
<div class="badge-1-name formField required">
<label for="id_badge-1-name">Name</label>
<input id="id_badge-1-name" maxlength="255" name="badge-1-name" type="text" />
</div>
<div class="badge-1-color formField required">
<label for="id_badge-1-color">Color</label>
<input id="id_badge-1-color" maxlength="20" name="badge-1-color" type="text" />
</div>
</div>
</div>
""".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),
"""
<div class="formSet badge">
{0}
<div class="formSetForm badge-0">
<div class="badge-0-name formField required">
<label for="id_badge-0-name">Name</label>
<input id="id_badge-0-name" maxlength="255" name="badge-0-name" type="text" />
</div>
<div class="badge-0-color formField required">
<label for="id_badge-0-color">Color</label>
<input id="id_badge-0-color" maxlength="20" name="badge-0-color" type="text" />
</div>
<div class="badge-0-DELETE formField">
<input id="id_badge-0-DELETE" name="badge-0-DELETE" type="checkbox" />
<label for="id_badge-0-DELETE">Delete</label>
</div>
</div>
<div class="formSetForm badge-1">
<div class="badge-1-name formField required">
<label for="id_badge-1-name">Name</label>
<input id="id_badge-1-name" maxlength="255" name="badge-1-name" type="text" />
</div>
<div class="badge-1-color formField required">
<label for="id_badge-1-color">Color</label>
<input id="id_badge-1-color" maxlength="20" name="badge-1-color" type="text" />
</div>
<div class="badge-1-DELETE formField">
<input id="id_badge-1-DELETE" name="badge-1-DELETE" type="checkbox" />
<label for="id_badge-1-DELETE">Delete</label>
</div>
</div>
</div>
""".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),
"""
<div class="formSet badge">
{0}
<div class="formSetForm badge-0">
<div class="badge-0-name formField required">
<label for="id_badge-0-name">Name</label>
<input id="id_badge-0-name" maxlength="255" name="badge-0-name" type="text" />
</div>
<div class="badge-0-color formField required">
<label for="id_badge-0-color">Color</label>
<input id="id_badge-0-color" maxlength="20" name="badge-0-color" type="text" />
</div>
<div class="badge-0-ORDER formField">
<label for="id_badge-0-ORDER">Order</label>
<input id="id_badge-0-ORDER" name="badge-0-ORDER" type="number" />
</div>
</div>
<div class="formSetForm badge-1">
<div class="badge-1-name formField required">
<label for="id_badge-1-name">Name</label>
<input id="id_badge-1-name" maxlength="255" name="badge-1-name" type="text" />
</div>
<div class="badge-1-color formField required">
<label for="id_badge-1-color">Color</label>
<input id="id_badge-1-color" maxlength="20" name="badge-1-color" type="text" />
</div>
<div class="badge-1-ORDER formField">
<label for="id_badge-1-ORDER">Order</label>
<input id="id_badge-1-ORDER" name="badge-1-ORDER" type="number" />
</div>
</div>
</div>
""".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),
"""
<div class="formSet badge">
{0}
<div class="formSetForm badge-0">
<div class="badge-0-name formField required">
<label for="id_badge-0-name">Name</label>
<input id="id_badge-0-name" maxlength="255" name="badge-0-name" type="text" />
</div>
<div class="badge-0-color formField required">
<label for="id_badge-0-color">Color</label>
<input id="id_badge-0-color" maxlength="20" name="badge-0-color" type="text" />
</div>
<div class="badge-0-ORDER formField">
<label for="id_badge-0-ORDER">Order</label>
<input id="id_badge-0-ORDER" name="badge-0-ORDER" type="text" />
</div>
</div>
<div class="formSetForm badge-1">
<div class="badge-1-name formField required">
<label for="id_badge-1-name">Name</label>
<input id="id_badge-1-name" maxlength="255" name="badge-1-name" type="text" />
</div>
<div class="badge-1-color formField required">
<label for="id_badge-1-color">Color</label>
<input id="id_badge-1-color" maxlength="20" name="badge-1-color" type="text" />
</div>
<div class="badge-1-ORDER formField">
<label for="id_badge-1-ORDER">Order</label>
<input id="id_badge-1-ORDER" name="badge-1-ORDER" type="text" />
</div>
</div>
</div>
""".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),
"""
<div class="formSet badge">
{0}
<div class="formSetForm badge-0">
<div class="badge-0-name formField required">
<label for="id_badge-0-name">Name</label>
<input id="id_badge-0-name" maxlength="255" name="badge-0-name" type="text" />
</div>
<div class="badge-0-color formField required">
<label for="id_badge-0-color">Color</label>
<input id="id_badge-0-color" maxlength="20" name="badge-0-color" type="text" />
</div>
<input id="id_badge-0-id" name="badge-0-id" type="hidden" />
</div>
<div class="formSetForm badge-1">
<div class="badge-1-name formField required">
<label for="id_badge-1-name">Name</label>
<input id="id_badge-1-name" maxlength="255" name="badge-1-name" type="text" />
</div>
<div class="badge-1-color formField required">
<label for="id_badge-1-color">Color</label>
<input id="id_badge-1-color" maxlength="20" name="badge-1-color" type="text" />
</div>
<input id="id_badge-1-id" name="badge-1-id" type="hidden" />
</div>
</div>
""".format(self.rendered_management_form)
)

0 comments on commit 3817362

Please sign in to comment.