Recently I used inline formsets in one of my Django projects and I liked how it worked out very much. I decided to share my example of integration inline formsets with Class-based views, crispy-forms and django-dynamic-formset jQuery plugin. When I researched the topic I didn’t find many examples and Django docs are not really extensive on this matter so I put together this post for those who want to try out this solution and for my future self.
First of all, why to use inline formsets:
to allow a user to create and update related via Foreign Key objects from the create/update views of a referenced object, all on one page.
Suppose that we have a Collection that can have titles in many languages but we don’t know exactly how many title translations user will provide. We want to allow a user to add as many titles as necessary just by clicking ‘add’ button which adds a new row in a Collection create form.
This is how our models look like.
models.py:
from django.db import models
from django.contrib.auth.models import User
class Collection(models.Model):
subject = models.CharField(max_length=300, blank=True)
owner = models.CharField(max_length=300, blank=True)
note = models.TextField(blank=True)
created_by = models.ForeignKey(User,
related_name="collections", blank=True, null=True,
on_delete=models.SET_NULL)
def __str__(self):
return str(self.id)
class CollectionTitle(models.Model):
"""
A Class for Collection titles.
"""
collection = models.ForeignKey(Collection,
related_name="has_titles", on_delete=models.CASCADE)
name = models.CharField(max_length=500, verbose_name="Title")
language = models.CharField(max_length=3)
Now let's create a form for CollectionTitle and a formset (using inlineformset_factory) that includes parent model Collection and FK related model CollectionTitle.
forms.py
from django import forms
from .models import *
from django.forms.models import inlineformset_factory
class CollectionTitleForm(forms.ModelForm):
class Meta:
model = CollectionTitle
exclude = ()
CollectionTitleFormSet = inlineformset_factory(
Collection, CollectionTitle, form=CollectionTitleForm,
fields=['name', 'language'], extra=1, can_delete=True
)
Next, we add this formset to a CollectionCreate view.
views.py:
from .models import *
from .forms import *
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from django.db import transaction
class CollectionCreate(CreateView):
model = Collection
template_name = 'mycollections/collection_create.html'
form_class = CollectionForm
success_url = None
def get_context_data(self, **kwargs):
data = super(CollectionCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['titles'] = CollectionTitleFormSet(self.request.POST)
else:
data['titles'] = CollectionTitleFormSet()
return data
def form_valid(self, form):
context = self.get_context_data()
titles = context['titles']
with transaction.atomic():
form.instance.created_by = self.request.user
self.object = form.save()
if titles.is_valid():
titles.instance = self.object
titles.save()
return super(CollectionCreate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('mycollections:collection_detail', kwargs={'pk': self.object.pk})
A CollectionUpdate view will look similar except that in get_context_data() the instance object should be passed.
views.py:
def get_context_data(self, **kwargs):
data = super(CollectionUpdate, self).get_context_data(**kwargs)
if self.request.POST:
data['titles'] = CollectionTitleFormSet(self.request.POST, instance=self.object)
else:
data['titles'] = CollectionTitleFormSet(instance=self.object)
return data
Next, we have to create CollectionForm with our formset to be rendered as fields inside of it. This is not straightforward because crispy-forms doesn’t have a layout object for a formset like it has it for a Div or HTML.
The best solution (many thanks!) is to create a custom crispy Layout Object.
custom_layout_object.py:
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string
class Formset(LayoutObject):
template = "mycollections/formset.html"
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
self.fields = []
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
formset = context[self.formset_name_in_context]
return render_to_string(self.template, {'formset': formset})
Next step is to add a template to render a formset.
To be precise what I want to render: for each new title - a row with fields 'name' and 'language' and a button 'remove' to remove the row (and delete the data in database when updating the collection), and one more button below the row - to add another row for new title.
I am using django-dynamic-formset jQuery plugin to dynamically add more rows.
I suggest to use prefix (docs) in order to have one formset template for all inline formset cases (e.g. when you add several inline formsets in one Form). Formset prefix is a related_name for referenced Class. So in my case it is ‘has_titles’.
formset.html:
{% load crispy_forms_tags %}
<table>
{{ formset.management_form|crispy }}
{% for form in formset.forms %}
<tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
{% for field in form.visible_fields %}
<td>
{# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field|as_crispy_field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="{% static 'mycollections/libraries/django-dynamic-formset/jquery.formset.js' %}">
</script>
<script type="text/javascript">
$('.formset_row-{{ formset.prefix }}').formset({
addText: 'add another',
deleteText: 'remove',
prefix: '{{ formset.prefix }}',
});
</script>
Finally, we can construct our own form layout for CollectionForm by using existing layout objects and custom Formset object together.
forms.py:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div, HTML, ButtonHolder, Submit
from .custom_layout_object import *
class CollectionForm(forms.ModelForm):
class Meta:
model = Collection
exclude = ['created_by', ]
def __init__(self, *args, **kwargs):
super(CollectionForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3 create-label'
self.helper.field_class = 'col-md-9'
self.helper.layout = Layout(
Div(
Field('subject'),
Field('owner'),
Fieldset('Add titles',
Formset('titles')),
Field('note'),
HTML("<br>"),
ButtonHolder(Submit('submit', 'save')),
)
)
collection_create.html:
{% extends "mycollections/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<div class="card">
<div class="card-header">
Create collection
</div>
<div class="card-body">
{% crispy form %}
</div>
</div>
</div>
{% endblock content %}
Now everything is in place and we can create a Collection and its titles in one form on one page by hitting Save button just once.
The source code for this post is here.
Credits to this awesome blog post which helped me to sort out inline formsets solution.

Latest comments (63)
Great post. Does django-dynamics-formtools even work in Django 5.2? I've been trying to implement this solution on a simple Invoice-InvoiceLines model and for the life of me I can´t get it to work.
is there any better and updated code to Django inline formsets with Class-based views and crispy forms
TypeError: Formset.render() missing 1 required positional argument: 'context'
I'm facing the same error while trying to adapt this solution to my project and can't get past it, even can't render the form.
Hi @zxenia, thanks a lot for the post. Do you have an idea how one could nest a third model and edit all three models at the same time? Using your example with Collections and Titles, suppose that additionally every Title had multiple Editions; so I would want to keep the form as we have it, but add the ability to add/remove/edit Editions under each of the titles. Tks
Formsets are great! However, I've noticed recently they can be time consuming to retrieve when performing updates on them. Has anyone else encountered this issue?
Hey,
thank you for your great tutorial. This helped me a lot. I want to modify it because I want a table layout in my inline formsets. For this I am trying to use
from here: django-crispy-forms.readthedocs.io...
I tired every possibility which I have found on the internet. Among other things this stackoverflow.com/questions/217549...
But it is not working. What would be an proper way to implement it in your code from this tutorial?
Thank you in advance.
Xenia, how to save current user for child formset, when saving it doesn't save?
Hello, how can we display the lines now in the html file?
Hello,
Would you have an idea on this issue? stackoverflow.com/q/66101476/13613237
Many Thanks
Thank you for the example. This works great!
What would be the best way to wrap the formset fields in a DIV? I have in the helper.Layout the below logic.
I would like to Div('Formset_field', css_class="", data-toggle="", etc.)