Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Django multiple forms code with sample usage
{% extends "base.html" %}
{% block content %}
<form method="post">{% csrf_token %}
{{ forms.subscription }}
<input type="submit" value="Subscribe">
</form>
<form method="post">{% csrf_token %}
{{ forms.contact }}
<input type="submit" value="Send">
</form>
{% endblock content %}
from django import forms
class MultipleForm(forms.Form):
action = forms.CharField(max_length=60, widget=forms.HiddenInput())
class ContactForm(MultipleForm):
title = forms.CharField(max_length=150)
message = forms.CharField(max_length=200, widget=forms.TextInput)
class SubscriptionForm(MultipleForm):
email = forms.EmailField()
from django.views.generic.base import ContextMixin, TemplateResponseMixin
from django.views.generic.edit import ProcessFormView
class MultiFormMixin(ContextMixin):
form_classes = {}
prefixes = {}
success_urls = {}
initial = {}
prefix = None
success_url = None
def get_form_classes(self):
return self.form_classes
def get_forms(self, form_classes):
return dict([(key, self._create_form(key, class_name)) \
for key, class_name in form_classes.items()])
def get_form_kwargs(self, form_name):
kwargs = {}
kwargs.update({'initial':self.get_initial(form_name)})
kwargs.update({'prefix':self.get_prefix(form_name)})
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
return kwargs
def forms_valid(self, forms, form_name):
form_valid_method = '%s_form_valid' % form_name
if hasattr(self, form_valid_method):
return getattr(self, form_valid_method)(forms[form_name])
else:
return HttpResponseRedirect(self.get_success_url(form_name))
def forms_invalid(self, forms):
return self.render_to_response(self.get_context_data(forms=forms))
def get_initial(self, form_name):
initial_method = 'get_%s_initial' % form_name
if hasattr(self, initial_method):
return getattr(self, initial_method)()
else:
return {'action': form_name}
def get_prefix(self, form_name):
return self.prefixes.get(form_name, self.prefix)
def get_success_url(self, form_name=None):
return self.success_urls.get(form_name, self.success_url)
def _create_form(self, form_name, form_class):
form_kwargs = self.get_form_kwargs(form_name)
form = form_class(**form_kwargs)
return form
class ProcessMultipleFormsView(ProcessFormView):
def get(self, request, *args, **kwargs):
form_classes = self.get_form_classes()
forms = self.get_forms(form_classes)
return self.render_to_response(self.get_context_data(forms=forms))
def post(self, request, *args, **kwargs):
form_classes = self.get_form_classes()
form_name = request.POST.get('action')
return self._process_individual_form(form_name, form_classes)
def _process_individual_form(self, form_name, form_classes):
forms = self.get_forms(form_classes)
form = forms.get(form_name)
if not form:
return HttpResponseForbidden()
elif form.is_valid():
return self.forms_valid(forms, form_name)
else:
return self.forms_invalid(forms)
class BaseMultipleFormsView(MultiFormMixin, ProcessMultipleFormsView):
"""
A base view for displaying several forms.
"""
class MultiFormsView(TemplateResponseMixin, BaseMultipleFormsView):
"""
A view for displaying several forms, and rendering a template response.
"""
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from .forms import ContactForm, SubscriptionForm
from .multiforms import MultiFormsView
def form_redir(request):
return render(request, 'pages/form_redirect.html')
def multiple_forms(request):
if request.method == 'POST':
contact_form = ContactForm(request.POST)
subscription_form = SubscriptionForm(request.POST)
if contact_form.is_valid() or subscription_form.is_valid():
# Do the needful
return HttpResponseRedirect(reverse('form-redirect') )
else:
contact_form = ContactForm()
subscription_form = SubscriptionForm()
return render(request, 'pages/multiple_forms.html', {
'contact_form': contact_form,
'subscription_form': subscription_form,
})
class MultipleFormsDemoView(MultiFormsView):
template_name = "pages/cbv_multiple_forms.html"
form_classes = {'contact': ContactForm,
'subscription': SubscriptionForm,
}
success_urls = {
'contact': reverse_lazy('form-redirect'),
'subscription': reverse_lazy('form-redirect'),
}
def contact_form_valid(self, form):
title = form.cleaned_data.get('title')
form_name = form.cleaned_data.get('action')
print(title)
return HttpResponseRedirect(self.get_success_url(form_name))
def subscription_form_valid(self, form):
email = form.cleaned_data.get('email')
form_name = form.cleaned_data.get('action')
print(email)
return HttpResponseRedirect(self.get_success_url(form_name))
@thirumalaraju

This comment has been minimized.

Copy link

@thirumalaraju thirumalaraju commented Apr 2, 2018

Hi Ravi,
I tried this but this is not getting saved in to database though tables are there but when I click on subscription with valid mail ID

@thirumalaraju

This comment has been minimized.

Copy link

@thirumalaraju thirumalaraju commented Apr 2, 2018

have create Models.py as follows

from django.db import models

Create your models here.

class Subscription(models.Model):
email = models.CharField(max_length=100)

class Contact(models.Model):
title = models.CharField(max_length=150)
message = models.CharField(max_length=200)

@willer2k

This comment has been minimized.

Copy link

@willer2k willer2k commented Jul 14, 2018

Thank you, Badri. You rock! So helpful.

@topiaruss

This comment has been minimized.

Copy link

@topiaruss topiaruss commented Apr 12, 2019

Thank you. Very useful.
Two small additions. First the instance feature...:

def get_form_kwargs(self, form_name):
    kwargs = {}
    kwargs.update({'instance': self.get_instance(form_name)})  # helps when updating records
    kwargs.update({'initial': self.get_initial(form_name)})
    kwargs.update({'prefix': self.get_prefix(form_name)})

and the associated method

def get_instance(self, form_name):
    instance_method = 'get_%s_instance' % form_name
    if hasattr(self, instance_method):
        return getattr(self, instance_method)()
    else:
        return None

Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.

def get_initial(self, form_name):
    initial_method = 'get_%s_initial' % form_name
    if hasattr(self, initial_method):
        attrs = getattr(self, initial_method)()
        attrs['action'] = form_name
        return attrs
    else:
        return {'action': form_name}

Thanks again.

@42force

This comment has been minimized.

Copy link

@42force 42force commented Jun 5, 2019

I just had this idea in my mind,..this is a good one.. i read your code.. it's very good but the thing I need is different.
but this is very helpful in the future...

@JulieGoldberg

This comment has been minimized.

Copy link

@JulieGoldberg JulieGoldberg commented May 15, 2020

This is very helpful.

I do have one change that I'd strongly suggest.

When I got a validation error submitting one form, it would try to populate all the other forms from the submitted form, clearing out their values and leading to errors on forms that weren't submitted. It's better to populate all but the submitted form with their initial data when you come back with an invalid submission.

I solved this by adding in a check for form_name in self.request.POST.get('action') in your get_form_kwargs function.

This won't be an issue if you rely on client-side validation for all your forms, but sometimes you need back-end validation.

    def get_form_kwargs(self, form_name):
        kwargs = {}
        kwargs.update({'initial':self.get_initial(form_name)})
        kwargs.update({'prefix':self.get_prefix(form_name)})
        if (self.request.method in ('POST', 'PUT')) and (form_name in self.request.POST.get('action')):
            kwargs.update({
                'data': self.request.POST,
                'files': self.request.FILES,
            })
        return kwargs
    
@Trainwithvinny

This comment has been minimized.

Copy link

@Trainwithvinny Trainwithvinny commented Jun 22, 2020

Thank you. Very useful.
Two small additions. First the instance feature...:

def get_form_kwargs(self, form_name):
    kwargs = {}
    kwargs.update({'instance': self.get_instance(form_name)})  # helps when updating records
    kwargs.update({'initial': self.get_initial(form_name)})
    kwargs.update({'prefix': self.get_prefix(form_name)})

and the associated method

def get_instance(self, form_name):
    instance_method = 'get_%s_instance' % form_name
    if hasattr(self, instance_method):
        return getattr(self, instance_method)()
    else:
        return None

Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.

def get_initial(self, form_name):
    initial_method = 'get_%s_initial' % form_name
    if hasattr(self, initial_method):
        attrs = getattr(self, initial_method)()
        attrs['action'] = form_name
        return attrs
    else:
        return {'action': form_name}

Thanks again.

Hi,

I had the same issue that you mentioned in the latter part, that form_classes was being returned as an empty list. I replaced the get_initial function as you suggested, but it's still giving the same issue.

Did you do anything else to fix the problem?

@JulieGoldberg

This comment has been minimized.

Copy link

@JulieGoldberg JulieGoldberg commented Jun 22, 2020

@Trainwithvinny, I'm not sure if your comments are about my change to the gist or @topiaruss's suggested changes. I didn't need to modify get_initial, only get_form_kwargs. That said, I put a value tag in each submit button on my template. e.g. <button name="action" value="create_foo"> if my form_classes contained 'create_foo': FooCreateForm

@Trainwithvinny

This comment has been minimized.

Copy link

@Trainwithvinny Trainwithvinny commented Jun 24, 2020

@johnpearson81

This comment has been minimized.

Copy link

@johnpearson81 johnpearson81 commented Jul 6, 2020

I'm getting the following error on the html page against each of the forms, can anyone help with this? I can move past the error and the webpage will generate, but it will only give me the 2 buttons not the input fields.
I'm getting up to speed with Django so might be a newb error.

Failed lookup for key [forms] in [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: 'Bbw1lraYBgwPlfyZWu9IwmzI1AbigK78jzXaGkBOhjJYPzU79USlnydVo3xaE7eR'>, 'request': <WSGIRequest: GET '/multiple_forms/'>, 'user': <SimpleLazyObject: <User: admin3>>, 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x0000017CB3840D48>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x0000017CB3822908>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}, {'contact_form': , 'subscription_form': }, {'block': <Block Node: content. Contents: [<TextNode: '\n

'>, <django.template.defaulttags.CsrfTokenNode object at 0x0000017CB3843288>, <TextNode: '\n '>, , <TextNode: '\n <input type="submit"'>, <django.template.defaulttags.CsrfTokenNode object at 0x0000017CB3843988>, <TextNode: '\n '>, , <TextNode: '\n <input type="submit"'>]>}]

@JulieGoldberg

This comment has been minimized.

Copy link

@JulieGoldberg JulieGoldberg commented Jul 6, 2020

@johnpearson81

This comment has been minimized.

Copy link

@johnpearson81 johnpearson81 commented Jul 7, 2020

I'm just using the example at the moment;

####### forms.py - Multiple Forms Test########################
class MultipleForm(forms.Form):
action = forms.CharField(max_length=60, widget=forms.HiddenInput())

class ContactForm(MultipleForm):
title = forms.CharField(max_length=150)
message = forms.CharField(max_length=200, widget=forms.TextInput)

############################ MultiForm Views #########################
def form_redir(request):
return render(request, 'pages/form_redirect.html')

def multiple_forms(request):
if request.method == 'POST':
contact_form = ContactForm(request.POST)
subscription_form = SubscriptionForm(request.POST)
if contact_form.is_valid() or subscription_form.is_valid():
# Do the needful
return HttpResponseRedirect(reverse('form-redirect') )
else:
contact_form = ContactForm()
subscription_form = SubscriptionForm()

return render(request, 'multiple_forms.html', {
    'contact_form': contact_form,
    'subscription_form': subscription_form,
})

class MultipleFormsDemoView(MultiFormsView):
template_name = "multiple_forms.html"
form_classes = {'contact': ContactForm,
'subscription': SubscriptionForm,
}

success_urls = {
    'contact': reverse_lazy('form-redirect'),
    'subscription': reverse_lazy('form-redirect'),
}

def contact_form_valid(self, form):
    title = form.cleaned_data.get('title')
    form_name = form.cleaned_data.get('action')
    print(title)
    return HttpResponseRedirect(self.get_success_url(form_name))

def subscription_form_valid(self, form):
    email = form.cleaned_data.get('email')
    form_name = form.cleaned_data.get('action')
    print(email)
    return HttpResponseRedirect(self.get_success_url(form_name))
@hiro-jp

This comment has been minimized.

Copy link

@hiro-jp hiro-jp commented Oct 22, 2020

Thans for cool job, Lakshmi!
I mixed your idea up with generic.UpdateView.

# multiforms.py

class ModelMultiFormMixin(ModelFormMixin, MultiFormMixin):
    def get_form_kwargs(self, form_name):
        kwargs = {}
        kwargs.update({'initial': self.get_initial(form_name)})
        kwargs.update({'prefix': self.get_prefix(form_name)})
        if self.request.method in ('POST', 'PUT'):
            kwargs.update({
                'data': self.request.POST,
                'files': self.request.FILES,
            })
        if hasattr(self, 'object'):
            kwargs.update({'instance': self.object})
        return kwargs

    def get_initial(self, form_name):
        initial_method = 'get_%s_initial' % form_name
        if hasattr(self, initial_method):
            return getattr(self, initial_method)()
        else:
            return {'action': form_name}

    def get_prefix(self, form_name):
        return self.prefixes.get(form_name, self.prefix)

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

    def get_success_url(self, form_name=None):
        return self.success_urls.get(form_name, self.success_url)

    def forms_valid(self, forms, form_name):
        """If the forms are valid, save the associated model."""
        obj = forms.get(form_name)
        obj.save()
        return HttpResponseRedirect(self.get_success_url(form_name))


class BaseMultipleFormsUpdateView(ModelMultiFormMixin, ProcessMultipleFormsView):
    """
    Base view for updating an existing object.

    Using this base class requires subclassing to provide a response mixin.
    """
    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)


class MultiFormsUpdateView(SingleObjectTemplateResponseMixin, BaseMultipleFormsUpdateView):
    pass

also we need to add:

# forms.py

class ModelMultipleForm(forms.ModelForm):
    action = forms.CharField(max_length=60, widget=forms.HiddenInput())

then, now we can code like:

# view.py

class MultipleFormsDemoView(MultiFormsUpdateView):  # superclass replaced
    template_name = "pages/cbv_multiple_forms.html"
    form_classes = {'contact': ContactForm,
                    'subscription': SubscriptionForm,
                    }

    success_urls = {
        'contact': reverse_lazy('form-redirect'),
        'subscription': reverse_lazy('form-redirect'),
    }
@dennohpeter

This comment has been minimized.

Copy link

@dennohpeter dennohpeter commented Jan 29, 2021

Thank you. Very useful.
Two small additions. First the instance feature...:

def get_form_kwargs(self, form_name):
    kwargs = {}
    kwargs.update({'instance': self.get_instance(form_name)})  # helps when updating records
    kwargs.update({'initial': self.get_initial(form_name)})
    kwargs.update({'prefix': self.get_prefix(form_name)})

and the associated method

def get_instance(self, form_name):
    instance_method = 'get_%s_instance' % form_name
    if hasattr(self, instance_method):
        return getattr(self, instance_method)()
    else:
        return None

Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.

def get_initial(self, form_name):
    initial_method = 'get_%s_initial' % form_name
    if hasattr(self, initial_method):
        attrs = getattr(self, initial_method)()
        attrs['action'] = form_name
        return attrs
    else:
        return {'action': form_name}

Thanks again.

Hi,

I had the same issue that you mentioned in the latter part, that form_classes was being returned as an empty list. I replaced the get_initial function as you suggested, but it's still giving the same issue.

Did you do anything else to fix the problem?

Thank you this has been very insightful 😃

One little Fix though to the form_is_valid

Change forms_valid method to

def forms_valid(self, forms, form_name):
        form_valid_method = '%s_valid' % form_name
        if hasattr(self, form_valid_method):
            return getattr(self, form_valid_method)(forms[form_name])
        else:
            form = forms.get(form_name)
            form.save()
            return HttpResponseRedirect(self.get_success_url(form_name))

Explanation
What has changed is form_valid_method = '%s_form_valid' % form_name to form_valid_method = '%s_valid' % form_name.
Assuming you have a form called user_form the former case form_valid_method would be form_valid_method = 'user_form_form_valid'
with that you would have ended in the else block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment