Working with nested forms with Django
One of the biggest advantages of Django is its ability to create CRUDs applications with no effort. You just need to define your models, register them at the admin and that’s it. If you are not going to follow the admin approach, you may use Django’s class based views to define each of the views you would require to manage the objects in your database. For creating and updating model instances, you do not even require to define a form because CreateView and UpdateView will do it for you. In just a couple of hours, you may have a very stable CRUD application with almost no effort.
However, this awesomeness seems to disappear when we start having relationships between our models; such as foreign keys or one to one relationships. Following the previous approach, It is hard to edit the parent object while editing a child, or to edit several children at once.
Django admin solves this situation using admin inlines. But, what may we do when our app will use its own views to manage our CRUD? Well, we use Formsets. In this blog post, we will check how to use them, along with our class based views, in order to achieve an awesome CRUD with several relationships and with a little amount of code. So, Let’s start!
Setting up our models
For this blog post, I will use two small models. Parent and Child. As their name indicates, a parent may have several children, but a child may only have one parent.
from django.db import models class Parent(models.Model): name = models.CharField(max_length=250) class Meta: verbose_name = "Parent" verbose_name_plural = "Parents" def __str__(self): return self.name class Child(models.Model): name = models.CharField(max_length=250) parent = models.ForeignKey(Parent, on_delete=models.CASCADE) class Meta: verbose_name = "Child" verbose_name_plural = "Children" def __str__(self): return self.name
Now, let’s add the create view for the parent, where we want to create the related children as well.
from django.views.generic.list import ListView from django.views.generic.edit import CreateView, UpdateView from .models import Parent class ParentListView(ListView): model = Parent class ParentCreateView(CreateView): model = Parent fields = ["name"]
If we leave it like that, we will not be able to add children for a parent. But if we add child_set to the fields array, a “FieldError: Unknown field(s) (child_set) specified for Parent” will be raised. So, what should we do?
Setting up the forms and the views
In order to create the inline forms, we will use Django’s inline form sets. These kind of form sets allows you to create several child objects from a parent object, all in the same form. These are the base of what Django admin uses when you register inlines instances.
For our example, we are going to go further and be even more abstract. We will use inlineformset_factory method that, in just one line, creates the formset that we need.
from django.forms.models import inlineformset_factory ChildFormset = inlineformset_factory( Parent, Child, fields=('name',) )
Now, we will update our createView to add the inline formset.
class ParentCreateView(CreateView): model = Parent fields = ["name"] def get_context_data(self, **kwargs): # we need to overwrite get_context_data # to make sure that our formset is rendered data = super().get_context_data(**kwargs) if self.request.POST: data["children"] = ChildFormset(self.request.POST) else: data["children"] = ChildFormset() return data def form_valid(self, form): context = self.get_context_data() children = context["children"] self.object = form.save() if children.is_valid(): children.instance = self.object children.save() return super().form_valid(form) def get_success_url(self): return reverse("parents:list")
As you may see, we added the formset to our context in order to be able to render it. As well, on form_valid method, after we save our parent object, we assign it as the instance of the formset, we check its validity and save it; creating the children objects as well.
In order to render the form, we may create a parents/parent_form.html file:
<h1>Parents</h1> <form method="post">{% csrf_token %} {{ form.as_p }} <h2>children</h2> {{ children.as_p }} <input type="submit" value="Save"> </form>
This template will look like this:
As you may see, the form not only allows you to add the parent, but also to create three children.
For the update view, we may follow the same approach. The only difference is that we initiate the ChildFormSet object with the instance argument. That way, our formset will be initialized with the children of the parent we are editing.
class ParentUpdateView(UpdateView): model = Parent fields = ["name"] def get_context_data(self, **kwargs): # we need to overwrite get_context_data # to make sure that our formset is rendered. # the difference with CreateView is that # on this view we pass instance argument # to the formset because we already have # the instance created data = super().get_context_data(**kwargs) if self.request.POST: data["children"] = ChildFormset(self.request.POST, instance=self.object) else: data["children"] = ChildFormset(instance=self.object) return data def form_valid(self, form): context = self.get_context_data() children = context["children"] self.object = form.save() if children.is_valid(): children.instance = self.object children.save() return super().form_valid(form) def get_success_url(self): return reverse("parents:list")
And that’s it! By using just a few methods and objects from Django, we were able to create a nested form to create or edit a parent object and its children at the same time with almost no effort.
I have been working with Django for almost four years and it still manages to amaze me with its simplicity and all the useful tools it provides. For this case, it allows to make a very complex task, that in other cases would require AJAX request or something similar, only using python and pure html.
And this is just a simple case. If you need another level of relationship, for example if your Child model has a Grandchild, you may create a formset that will allow you to edit a Parent, a Child and a Grandchild without using a different approach. You may see an excellent example here.