Django Class-based Views with Multiple Forms

By default some of Django’s class-based views support just a single form per view. This, of course, does not always play nicely with what you’re trying to accomplish. I’ve yet to come across a concrete example of how to accomplish this so after a bit of experimentation and some time spent diving into Django’s code, I’ve created a decent workaround. Please let me know in the comments if there’s a better way to do this.

For this example I’ll use a generic UpdateView and two ModelForms. The same technique should work with most of the class-based views as well as regular forms.

I’ll also mention that this handles the validation and submission of either form, but not both forms at the same time. The user is only submitting a single form, so you only need to handle that one.

There’s nothing special about the models or the templates, so let’s look at the view:

from django.views.generic import UpdateView
from django.shortcuts import get_object_or_404
from django.core.urlresolvers import reverse_lazy
from app.models import Model
from app.forms import Form1, Form2
class MyView(UpdateView):
template_name = 'template.html'
form_class = Form1
second_form_class = Form2
success_url = reverse_lazy('success')
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
if 'form' not in context:
context['form'] = self.form_class(initial={'some_field': context['model'].some_field})
if 'form2' not in context:
context['form2'] = self.second_form_class(initial={'another_field': context['model'].another_field})
return context
def get_object(self):
return get_object_or_404(Model, pk=self.request.session['someval'])
def form_invalid(self, **kwargs):
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
# get the user instance
self.object = self.get_object()
# determine which form is being submitted
# uses the name of the form's submit button
if 'form' in request.POST:
# get the primary form
form_class = self.get_form_class()
form_name = 'form'
# get the secondary form
form_class = self.second_form_class
form_name = 'form2'
# get the form
form = self.get_form(form_class)
# validate
if form.is_valid():
return self.form_valid(form)
return self.form_invalid(**{form_name: form})

view raw

hosted with ❤ by GitHub

The first thing the view does is set the template, success_url and the two forms. Next we define the get_context_data method which adds either form to the context if it is missing.

UPDATE: There was an issue with the get_context_data method in which the form would not be populated with values from the model if a form was submitted with invalid data (and self.form_invalid was called). The current fix is to manually pass the data to the form using the initial argument.

The get_object method overrides the default behavior and gets a model instance based on a session variable. This isn’t necessary if you’re using the standard method of providing a pk or slug in the URL.

The next change is very important – override the default form_invalid method. The default method returns a single form in the context:

return self.render_to_response(self.get_context_data(form=form))

view raw

hosted with ❤ by GitHub

Since we have two forms, with different names, we need to switch to using **kwargs instead.

Finally, we override the post method to determine which form was submitted based on the name of the submit button (you could alternatively use a hidden field), validate the form and call form_valid or form_invalid. These methods will save the form and redirect to the success_url, or return to the view and show any errors, respectively.

And for the sake of clarity, the forms in your templates simply need a named submit button:

<form action="{% url 'myview' %}" method="post" enctype="multipart/form-data">
<input type="submit" name="form" value="Submit" />
<form action="{% url 'myview' %}" method="post" enctype="multipart/form-data">
<input type="submit" name="form2" value="Submit" />

view raw


hosted with ❤ by GitHub

11 thoughts on “Django Class-based Views with Multiple Forms”

  1. Hi from Argentina, Im new in python and django, Im working in my first proyect in django and I want to render multiple forms on a single template. Im working with class based generic views and class based views. Do you have a complete example to help me?

    Thanks a lot!!

  2. I’ve already used some of your insights from another post, and here again you are in my Google search!

    Just a thought to keep this in line with the djang-onic (or is it djangoic?) way of creating model views and how they handle being inherited later;

    You could add:

    def get_second_form_class():
    return self.second_form_class

    And change later instances of:




    That way if the view is inherited by another it still allows you to change the form class of the second form easily with the `second_form_class` variable, just like how Django handle updating form_class. Otherwise if you set `second_form_class` in a child view/class it will have no effect.

      1. On second thought, that doesn’t work how I thought it would – or rather, there wasn’t an inheritance issue.

        I swear I just ran into this though. I need to look back at what I did… and now I think I’m taking crazy pills.

        Your could still refactor to follow their conventions, but that seems somewhat unnecessary as your code should inherit fine as it is.

  3. Hello;
    Where is the MODEL coming from the code in the following line? It represents the model we imported from or is it a given name for example, can we write something different instead of model?I have one more question: will self come to some_field expressions self?for examle, self.first_name or first_name?
    context[‘form’] = self.form_class(initial={‘some_field’: context[‘model’].some_field})
    thanks for helps Chris.

  4. Well done Chris.
    Just a question for clarification.
    Shall MyView inherit from FormMixin or FormView to implement the post method since DetailView class does not have the post method?

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s