Recently, I managed to solve this common problem in a general way that may be useful to others. Generally, it's a drop-in replacement for a FormView , that supports multiple forms on the same page (with just a minor change in template).

Use case

I wanted to display dynamic number of forms, each performing specific action. Site administrators were allowed to use every form, whereas moderators could see just a subset of them. All logic was stored in a form, for example sending email to user or performing some other business logic. Thanks to that, I could make Views as stupid as possible - just basic displaying and validation of selected form.

My goals:

I wanted to position all forms on the same page (in rows)

Validate just submitted one

Have the ability to decide which forms should be displayed to user

Easily add new forms

Compatibility with existing FormView

Solution

Firstly, let's take a look how we would like to use our View (repository with example code can be found here):

# forms in forms.py file class ContactForm(forms.Form): name = forms.CharField(max_length=60) message = forms.CharField(max_length=200, widget=forms.TextInput) class SubscriptionForm(forms.Form): email = forms.EmailField() want_spam = forms.BooleanField(required=False) class SuggestionForm(forms.Form): text = forms.CharField(max_length=200, widget=forms.TextInput) type = forms.ChoiceField(choices=[('bug', 'Bug'), ('feature', 'Feature')]) class GlobalMessageForm(forms.Form): staff_only = True global_message = forms.CharField(max_length=200, widget=forms.TextInput) # View with multiple forms, inheriting our Generic MultipleFormsView class MultipleFormsDemoView(MultipleFormsView): template_name = 'forms.html' success_url = '/' # here we specify all forms that should be displayed forms_classes = [ forms.GlobalMessageForm, forms.ContactForm, forms.SubscriptionForm, forms.SuggestionForm ] def get_forms_classes(self): # we hide staff_only forms from not-staff users # our goal no. 3 about dynamic amount list of forms forms_classes = super(MultipleFormsDemoView, self).get_forms_classes() user = self.request.user if not user.is_authenticated() or not user.is_staff: return list(filter(lambda form: not getattr(form, 'staff_only', False), forms_classes)) return forms_classes def form_valid(self, form): print("yay it's valid!") return super(MultipleFormsDemoView, self).form_valid(form)

forms.html file:

<html> <head></head> <body> {% for form in forms %} <form method="post"> {% csrf_token %} {{ form }} <input type="hidden" name="selected_form" value="{{ forloop.counter0 }}"> <button type="submit">Submit</button> </form> {% endfor %} </body> </html>

I think that code above is self-explanatory, except hidden_field part. We need to somehow decide which form was submitted, that's why we're adding it, with proper 0-based index as a value.

Finally, Let's take a look at the MultipleFormsMixin code (it's also available as a gist here):

from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseRedirect from django.utils.encoding import force_text from django.views.generic.base import ContextMixin class MultipleFormsMixin(ContextMixin): """ A mixin that provides a way to show and handle multiple forms in a request. It's almost fully-compatible with regular FormsMixin """ initial = {} forms_classes = [] success_url = None prefix = None active_form_keyword = "selected_form" def get_initial(self): """ Returns the initial data to use for forms on this view. """ return self.initial.copy() def get_prefix(self): """ Returns the prefix to use for forms on this view """ return self.prefix def get_forms_classes(self): """ Returns the forms classes to use in this view """ return self.forms_classes def get_active_form_number(self): """ Returns submitted form index in available forms list """ if self.request.method in ('POST', 'PUT'): try: return int(self.request.POST[self.active_form_keyword]) except (KeyError, ValueError): raise ImproperlyConfigured( "You must include hidden field with field index in every form!") def get_forms(self, active_form=None): """ Returns instances of the forms to be used in this view. Includes provided `active_form` in forms list. """ all_forms_classes = self.get_forms_classes() all_forms = [ form_class(**self.get_form_kwargs()) for form_class in all_forms_classes] if active_form: active_form_number = self.get_active_form_number() all_forms[active_form_number] = active_form return all_forms def get_form(self): """ Returns active form. Works only on `POST` and `PUT`, otherwise returns None. """ active_form_number = self.get_active_form_number() if active_form_number is not None: all_forms_classes = self.get_forms_classes() active_form_class = all_forms_classes[active_form_number] return active_form_class(**self.get_form_kwargs(is_active=True)) def get_form_kwargs(self, is_active=False): """ Returns the keyword arguments for instantiating the form. """ kwargs = { 'initial': self.get_initial(), 'prefix': self.get_prefix(), } if is_active: kwargs.update({ 'data': self.request.POST, 'files': self.request.FILES, }) return kwargs def get_success_url(self): """ Returns the supplied success URL. """ if self.success_url: # Forcing possible reverse_lazy evaluation url = force_text(self.success_url) else: raise ImproperlyConfigured( "No URL to redirect to. Provide a success_url.") return url def form_valid(self, form): """ If the form is valid, redirect to the supplied URL. """ return HttpResponseRedirect(self.get_success_url()) def form_invalid(self, form): """ If the form is invalid, re-render the context data with the data-filled forms and errors. """ return self.render_to_response(self.get_context_data(active_form=form)) def get_context_data(self, **kwargs): """ Insert the forms into the context dict. """ if 'forms' not in kwargs: kwargs['forms'] = self.get_forms(kwargs.get('active_form')) return super(MultipleFormsMixin, self).get_context_data(**kwargs)

Ready to use, base view (notice we're using here standard CBV components for working with forms - our solution is fully compatible!):

from django.views.generic.base import TemplateResponseMixin from django.views.generic.edit import ProcessFormView class MultipleFormsView(TemplateResponseMixin, MultipleFormsMixin, ProcessFormView): pass

How it works? Basically, it's a modified code of FormMixin from Django core. We have a couple of additional methods, like get_forms_classes , get_active_form_number etc. View takes a list of forms, instantiates them and adds to template context.

We need to add this input to forms in template (you should render forms in a loop):

<input type="hidden" name="selected_form" value="{{ forloop.counter0 }}">

Thanks to that, we know which form was submitted (look at get_active_form_number method). Then, we just take proper form, checks if it's valid and run form_valid or form_invalid method (it's actually inside ProcessFormView ).

That's it! Hope it will be useful for you. If yes, tweet about this!