Skip to content

Instantly share code, notes, and snippets.

@badri
Created January 18, 2018 04:03
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 18 You must be signed in to fork a gist
  • Save badri/4a1be2423ce9353373e1b3f2cc67b80b to your computer and use it in GitHub Desktop.
Save badri/4a1be2423ce9353373e1b3f2cc67b80b to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link

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)

@loudpumpkins
Copy link

Thank you, Badri. You rock! So helpful.

@topiaruss
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

@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
Copy link

Trainwithvinny commented Jun 24, 2020 via email

@johnpearson81
Copy link

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
Copy link

JulieGoldberg commented Jul 6, 2020 via email

@johnpearson81
Copy link

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
Copy link

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
Copy link

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.

@kairych
Copy link

kairych commented Mar 17, 2022

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 with 403. Tried to use your updates, but getting an error:

TypeError: init() got an unexpected keyword argument 'instance'

What could be the problem?

@OtterWebDev
Copy link

@JulieGoldberg Hi, can you help me with a question?

i'm trying to save a form called vendor_form, but he also uses fields from user_form, so my valid method is "if user_form.is_valid() and vendor_form.is_valid()'

since i depend the user to create the vendor user, i'm getting null value collumn error because for some reason the user_form is not getting saved cause the action is 'vendor: vendor_form'

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