Formsets In Django Class-based Views

 Dec. 14, 2020     0 comments

Django offers several out-of-the-box class-based views, including CreateView and UpdateView, that allow you to quckly implement basic CRUD functionality in your web-application without much of boilerplate code. Under the hood CreateView and UpdateView classes create, validate and save a Django ModelForm instance tied to your model class. But sometimes you need to create/edit related model instances along with the main model on the same page. For example, you have Author model that represents a book author and Book model which has a foreign key to Author, and you want to edit information about an author and their books in the same form. Django provides formsets and model formsets for that purpose. But out-of-the-box create and update class-based views deal only with single forms. The below mixin class allows you to add support for multiple formsets in your create and edit views. Working with formsets in Django templates is beyond the scope of this article.

from django.db import transaction

from .forms import BookFormSet

class FormsetMixin:
    formset_classes = {
        'book_formset': BookFormSet,

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        for formset_name, formset_class in self.formset_classes.items():
            if formset_name not in kwargs:
                context[formset_name] = formset_class(instance=self.object)
        return context

    def formsets_valid(self, response):
        return response

    def formsets_invalid(self, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

    def form_valid(self, form):
        with transaction.atomic():
            response = super().form_valid(form)
            formset_instances = {}
            all_formsets_valid = True
            for formset_name, formset_class in self.formset_classes.items():
                formset = formset_class(self.request.POST, instance=self.object)
                formset_instances[formset_name] = formset
                if not formset.is_valid():
                    all_formsets_valid = False
            if all_formsets_valid:
                for formset in formset_instances.values():
                return self.formsets_valid(response)
        return self.formsets_invalid(form=form, **formset_instances)

This example assumes that you have Author and Book models and you have created BookFormSet class using modelformset_factory function but it can be easily extended to support multiple formsets. Formset validation is done in form_valid method that is called after the main model (Author) form has been successfully validated. The whole code is wrapped in a database transaction which ensures that nothing will be saved to the database in case if one or more forms in a formset are not valid. The formsets_valid method simply returns a success response but it can be overridden in your view class to include additional logic, for example, logging successful model instances creation/update.