Django Inline formsets example: mybook

Daniel Chen
5 min readOct 6, 2016

--

Recently I needed to use django formset for a work project. Django formset allows you to edit a collection of the same forms on the same page. It basically allows you to bulk edit a collection of objects at the same time. Imagine you need to update an address book, you can either add each entry on an address form page individually or you can probably do it faster by having a page that allows you to enter as many new entries as you want. I haven’t used this django feature before so I started by spending some time reading about this feature on the internet. To my surprise, I couldn’t find a good working example that closely matches my requirements. The official django documentation is quite barebone. It didn’t give me a good picture on how to put everything together. After a lot of trial and errors and googling, I was able to put together a solution that I was happy with. This post is my attempt to recreate what I implemented with a much simpler scenario. Hopefully, it will be a better learning experience for the next person that might be looking for a good example. I am sure I need to come back to it at some point myself.

Scenario: Add Family Member to User Profile

We are working on a social networking site called mybook. Like many social networking sites, this app allows you to create user profiles. On the edit profile page, you can also add and remove family members to that profile. Sounds simple enough?

What I would like to achieve at the end:

  1. Conceptually I have two basic django models. (Profile and FamilyMember) The child model (FamilyMember)has a foreign key relationship to the parent model(Profile). I’t is a classic one to many relationship. I would like to have a form that allows me to update this relationship easily.
  2. Layout: Formset is essentially a collection of the same form. You can render the formset easily like {{ form.as_p }} in django template, but the default layout is really not ideal since the labels of the fields are just repeated over and over again. Instead of rendering each form as tables or lists, I would like to render the field labels once and each form in a table row.
  3. I would like to be able add new member/delete member on the page dynamically. Default django templates don’t allow this kind of behavior. We will need some javascript help.
  4. Achieve all the above with as little code as possible. None of this seems complicated. The solution should look very generic. Following Django’s DRY principle, we should strive to reduce the amount of boilerplate code.

Here is what the end result looks like:

list of profiles
edit profile and add family member

Let’s look at different parts of the solution. I will try to point out the important bits.

Models:

class Profile(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
created_date = models.DateTimeField(default=timezone.now)

class FamilyMember(models.Model):
profile = models.ForeignKey(Profile)
name = models.CharField(max_length=100)
relationship = models.CharField(max_length=100)

Notes: It is pretty straightforward here. Just two simple Django models that have a 1:m relationship.

Forms.py

class FamilyMemberForm(ModelForm):
class Meta:
model = FamilyMember
exclude = ()

FamilyMemberFormSet = inlineformset_factory(Profile, FamilyMember,
form=FamilyMemberForm, extra=1)

Notes: With modelForm and inlineformset, we can create all the necessary formset with minimal code. Django scores high points here. If you would like to have custom validation logic on the formset, then you should create a baseformset class that extends the BaseInlineFormSet and override the clean method. The extra param controls the number empty form that will be displayed by default.

Views.py

class ProfileList(ListView):
model = Profile
class ProfileFamilyMemberCreate(CreateView):
model = Profile
fields = ['first_name', 'last_name']
success_url = reverse_lazy('profile-list')

def get_context_data(self, **kwargs):
data = super(ProfileFamilyMemberCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['familymembers'] = FamilyMemberFormSet(self.request.POST)
else:
data['familymembers'] = FamilyMemberFormSet()
return data

def form_valid(self, form):
context = self.get_context_data()
familymembers = context['familymembers']
with transaction.atomic():
self.object = form.save()

if familymembers.is_valid():
familymembers.instance = self.object
familymembers.save()
return super(ProfileFamilyMemberCreate, self).form_valid(form)

Notes: This part is where Django is falling short a little bit. It’s very easy to have a ProfileCreateView without overriding any methods. Like this:

class ProfileCreate(CreateView):
model = Profile
fields = ['first_name', 'last_name']

But what if you also want to edit the foreign key relationship with this form. Things becomes muddy very quickly. The approach I have taken at thend basically treat the inlineformset as an additional form that the generic createview needs to process. We need to initialize the formset with get() and inject it into context. With post(), we need to call the save() method. Finding the right places to do all these is a bit tricky. You can get a better understanding on how to do this from this django doc.

Templates (profile_form.html)

<div class="col-md-4">
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}

<table class="table">
{{ familymembers.management_form }}

{% for form in familymembers.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tr class="{% cycle row1,row2 %} formset_row">
{% 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 }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input type="submit" value="Save"/> <a href="{% url 'profile-list' %}">back to the list</a>
</form>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="{% static 'formset/jquery.formset.js' %}"></script>
<script type="text/javascript">
$('.formset_row').formset({
addText: 'add family member',
deleteText: 'remove',
prefix: 'familymember_set'
});
</script>

notes:

  • The template has the meat of this custom solution. I have bolded the section that worth paying attention to. In order to layout the form in the desired fashion, I have to render the labels and each form manually. It’s very important to include the “management_form” and the hidden fields in the template. It is worth taking a closer at what management_form is. You will need to understand what it does in order to write proper unit tests.
  • For dynamically adding and removing the form, I use this jquery plugin that works very nicely with django formset. It is important to add a special class (formset_row) to the tr row so the plugin can decorate it appropriately. Another thing worth noting is the prefix. This prefix should be the “related_name” of the foreign key relationship in the django world. Django uses the related name as prefix in the rendered form. In this example, the related_name is defaulted to familymember_set. You can override the related_name in models.py. If you do that, you need to make sure the prefix is also updated here. Otherwise, the form processing will be quite buggy.

That’s it for now. Hope you find it useful. The source code can be found here.

--

--