Form and formset in one html form

I was recently working on a project that required combining a Django model and another model related to it via a ForeignKey in a single form. At first I considered generating fields for the related model in the ModelForm's init, the more I thought about it though, the more I realized that would be messy, brittle and not easy to make changes to in the future.

The Solution

Turns out you can put a formset and a ModelForm (or regular form) inside the same html form in a template and have them be validated individually. Each one only cares about the POST data relevant to them. Something like:

from django.forms.models import modelformset_factory
from django.shortcuts import render

from forms import FormClass
from models import RelatedModel


def handle_form_and_formset(request):
    '''
    Handles displaying, validating and saving a form and related
    model formset
    '''
    form = FormClass()
    RelatedFormset = modelformset_factory(RelatedModel, extra=5)
    formset = RelatedFormset(queryset=RelatedModel.objects.none())
    if request.method == "POST":
        form = FormClass(request.POST)
        formset = RelatedFormset(request.POST,
                                 queryset=RelatedModel.objects.none())
        if form.is_valid() and formset.is_valid():
            # do something with the form data here
            for f_form in formset:
                if f_form.is_valid() and f_form.has_changed():
                    # do something with the formset data
    return render(request, "some/template/name.html",
                  {'form': form, 'formset': formset'})

The corresponding template looks something like this:

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    {% for f_form in formset %}
        {{ f_form.as_p }}
    {% endfor %}
</form>

That's it! I didn't have to worry about form inits, or figuring out how to handle validation or anything like that, I just let the form do what it does best and the formset do what it does best. The astute reader may think this solution is obvious, but to me, it was a revelation.

Epilogue

As you might have guessed, this isn't limited to ModelForm's and the formset's of related models. This would work to combine any form and any formset (or even multiple forms and multiple formsets).

Gotchas

It took me awhile to figure out this line:

if f_form.is_valid() and f_form.has_changed():

Without adding the f_form.has_changed() I kept running into an error because I was trying to save one of the extras that had no data supplied.

credits:

tags

django, python, forms, formsets

Comments