Revisiting Nested Formsets
It’s been nearly four years since I first wrote about nested formsets . When I wrote about nested formsets, I must have been using Django 1.1 (based on correlating dates in the release notes and the original blog post), which means what I wrote has had four major releases of Django to drift out of date. And yet it’s still one of the most frequently visited posts on my blog, and one of the few that I receive email questions about. Four years later, it seemed like the time to revisit the original post to see if nested formsets still make sense and if so, what they look like now.
Formsets help manage the complexity of maintaining multiple instances of a Form on a single page. For example, if you’re editing a list of items on a single page, each individual item may be a copy of the same form. Formsets help manage things like HTML ID generation, flagging forms for deletion, and validating the entire set of forms together. When used with Models , they allow you to edit the members of a QuerySet all at once.
So what are nested formsets? The example I used previously was something along the lines of Block – Building – Tenant: one Block has many Buildings, and each Building has many Tenants. If you’re editing a Block, you want to see all the Buildings and all the Tenants at once. That’s a fine hypothetical, but one of the questions I get with some frequency is “what’s a good use case for a nested formset?” Four years later — two and a half of them spent doing web development full time — I have yet to encounter a situation where I needed a nested formset. In that time I’ve built some pretty complex forms, including Eventbrite’s event creation flow . That page was complex enough that I built Form Groups to support the interaction, and I think the jury is still out on whether that was a good idea or not. It’s possible that there are use cases for nested formsets in admin-style applications that I haven’t encountered. I think it’s also possible that there are reasons to use a nested formset alongside a Javascript framework to ease the user experience.
Note that if you only have one level of relationships on the page (ie, you’re editing all the Tenants for a single Building in our example) then you don’t need nested formsets: Django’s inline formsets will work just fine.
And why not nested form sets? From the questions people have asked and my experience building Form Groups (which borrowed some ideas), I’ve concluded that they’re difficult to get completely right, have edge cases that can be hard to manage, and create quite complicated user interfaces. In my original blog post I alluded to the fact that I spent most of a three day weekend trying to get the nested formsets to work right. Two thirds of that time was spent on work I eventually threw away, because I couldn’t manage the edge cases. It was only when I started using TDD that I managed to get something working. But I didn’t publish the tests with my previous code example, so no one else was able to benefit from that work.
If you’ve read this far and still think a nested formset is the best solution
for your problem, what would that look like with Django 1.5? The answer is:
simpler. I decided to rewrite my initial implementation using test driven
development. The full implementation of the formset logic only overrides three
methods from BaseInlineFormSet
.
from django.forms.models import (
BaseInlineFormSet,
inlineformset_factory,
)
class BaseNestedFormset(BaseInlineFormSet):
def add_fields(self, form, index):
# allow the super class to create the fields as usual
super(BaseNestedFormset, self).add_fields(form, index)
form.nested = self.nested_formset_class(
instance=form.instance,
data=form.data if self.is_bound else None,
prefix='%s-%s' % (
form.prefix,
self.nested_formset_class.get_default_prefix(),
),
)
def is_valid(self):
result = super(BaseNestedFormset, self).is_valid()
if self.is_bound:
# look at any nested formsets, as well
for form in self.forms:
result = result and form.nested.is_valid()
return result
def save(self, commit=True):
result = super(BaseNestedFormset, self).save(commit=commit)
for form in self:
form.nested.save(commit=commit)
return result
These three method cover the four areas of functionality I called out in the
previous post : validation (is_valid
), saving (both existing and new
objects are handled here by save
), and instantiation (creating the nested
formset instances, handled by add_fields
).
By making it a general purpose baseclass, I’m also able to write a simple factory function, to make using it more in tune with Django’s built-in model formset.
def nested_formset_factory(parent_model, child_model, grandchild_model):
parent_child = inlineformset_factory(
parent_model,
child_model,
formset=BaseNestedFormset,
)
parent_child.nested_formset_class = inlineformset_factory(
child_model,
grandchild_model,
)
return parent_child
You can find the source to this general purpose implementation on GitHub . I wrote tests at each step as I worked on this, so it may be interesting to go back and look at individual commits, as well.
So how would you use this in with Django 1.5? With a class-based view , of course.
from django.views.generic.edit import UpdateView
class EditBuildingsView(UpdateView):
model = models.Block
def get_template_names(self):
return ['blocks/building_form.html']
def get_form_class(self):
return nested_formset_factory(
models.Block,
models.Building,
models.Tenant,
)
def get_success_url(self):
return reverse('blocks-list')
Of course there’s more needed — templates, for one — but this shows just how
easy it is to create the views and leverage a generic abstraction. The real keys
here are specifying model = models.Block
and the definition of
get_form_class
. Django’s UpdateView knows how to implement the basic
form processing idiom (GET
, POST
, redirect), so all you need to do is
tell it which form to use.
You can find a functional, albeit ugly, demo application in the demo
directory
of the git repository .
So that’s it: a general purpose, updated implementation of nested formsets. I advise using them sparingly :).