Django inline formsets with Class-based views and crispy forms
Tags: django, python, webdev
Published: 2019-02-13 21:16:52

---
title: Django inline formsets with Class-based views and crispy forms
published: true
description: 
tags: Django, python, webdev
---
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](https://docs.djangoproject.com/en/2.1/topics/forms/modelforms/#inline-formsets) with Class-based views, [crispy-forms](https://django-crispy-forms.readthedocs.io/en/latest/) and [django-dynamic-formset](https://github.com/elo80ka/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:_

```python
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](https://docs.djangoproject.com/en/2.1/ref/forms/models/#inlineformset-factory)) that includes parent model Collection and FK related model CollectionTitle.

_forms.py_

```python
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:_

```python
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:_

```python
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](https://stackoverflow.com/a/22053952/7101197) (many thanks!) is to create a custom crispy Layout Object.

_custom_layout_object.py:_


```python
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](https://github.com/elo80ka/django-dynamic-formset) jQuery plugin to dynamically add more rows.
I suggest to use _prefix_ ([docs](https://github.com/elo80ka/django-dynamic-formset/blob/master/docs/usage.rst#formset-options)) 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:_

```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:_

```python
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:_

```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.

![inline-formsets](https://thepracticaldev.s3.amazonaws.com/i/olhglncci00mhx9w508t.png "Inline formsets")

The source code for this post is [here](https://github.com/zxenia/example-inline-formsets).

Credits to [this awesome blog post](https://medium.com/@adandan01/django-inline-formsets-example-mybook-420cc4b6225d) which helped me to sort out inline formsets solution.