'Is it possible to submit multiple forms at once using Django views?

What I want to accomplish is to be able to submit multiple forms contained on one page. What I have currenlty done is supplied a view that returns 8 forms. Is it possible, within the template, to have lets say a button that will submit all of the forms contained on the page in one POST request?

Here is some code from my view:

def get_all_forms(request):
    context = {}
    if request.method == 'POST':
        return
    else:
        for x in range(8):
            context[x] = Form()
    return render(request, 'app/all_forms.html', {'form': context})


Solution 1:[1]

Some issues with your code:

  1. It lacks input validation.

  2. You probably won't be able to differentiate between the data submitted by the 8 instances of Form(), because all instances will have the same HTML name attribute for the <input .../> elements.

    One way to make the instances distinct is to use the prefix keyword argument. For example:

    def get_all_forms(request):
        NUM_FORMS = 8
        if request.method == 'POST':
            forms = [Form(request.POST, prefix=i) for i in range(NUM_FORMS)]
            if all((form.is_valid() for form in forms)):
                # Do something with the submitted data here ...
                return redirect('somewhere ......')
        else:
            forms = [Form(prefix=i) for i in range(NUM_FORMS)]
        return render(request, 'app/all_forms.html', {'forms': forms})
    

    But a much better solution is to use formsets (see below).

Solution

Formsets allow you to have multiple copies of the same form on one page, and process all of them in one POST request. Using a formset, Django is able to handle validation for all the forms. Example:

If your form is:

from django import forms

class MyForm(forms.Form):
    # ...

Create a formset that has 8 empty forms:

from django.forms import formset_factory

MyFormset = formset_factory(MyForm, extra=8)  # Create 8 forms.

Then you can use the formset in your view:

from django.shortcuts import redirect, render

def get_all_forms(request):
    if request.method == 'POST':
        formset = MyFormset(request.POST)
        if formset.is_valid():  # Validates all 8 forms.
            # ... (submitted data is in formset.cleaned_data)
            return redirect('somewhere ......')
    else:
        formset = MyFormset()
    return render(request, 'app/all_forms.html', {'formset': formset})

And you can display all forms in your template using just this:

<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ formset }}
    </table>
    <input type="submit"/>
</form>

Further information from the documentation:

Solution 2:[2]

Yes, it is possible. In your HTML template you should put all form data in one <form> tag.

In Django view you should use prefixes for forms https://docs.djangoproject.com/en/3.0/ref/forms/api/#prefixes-for-forms

Solution 3:[3]

Yes. Lin Troll's answer includes two important points.

You will probably find this easiest using Function-based views (the Generic FormView and derived classes really don't work well with >1 form). OI also don't find the canonical form of an FBV helpful, especially not with two forms.

Here is a two-form view I wrote earlier. The code has been refactored into what I regard as a better pattern, to validate both forms and redisplay if anything is wrong at the top, and then you just do the actual work to create and save the objects at the bottom.

def receive_uncoated( request): #Function based view

    # let's put form instantiation in one place not two, and reverse the usual test. This
    # makes for a much nicer layout with actions not sandwiched by "boilerplate" 
    # note any([ ]) forces invocation of both .is_valid() methods 
    # so errors in second form get shown even in presence of errors in first

    args = [request.POST, ] if request.method == "POST" else []
    batchform = CreateUncWaferBatchForm( *args )
    po_form =  CreateUncWaferPOForm(     *args, prefix='po')
    if request.method != "POST" or any(  
        [ not batchform.is_valid(), not po_form.is_valid() ]):

        return render(request, 'wafers/receive_uncoated.html',   # can get this out of the way at the top
            {'batchform': batchform,  
            'po_form': po_form, 
        })

    #POST, everything is valid, do the work here
    # create and save some objects based on the validated forms ... 

    return redirect( 'wafers:ok' )   

Solution 4:[4]

I made a class that inherits from CreateView class. Formsets didn't work for me because I wanted different forms to be submitted at the same time with validation. NOTE: SuccessMessageMixin does not work in this class

Usage:

class IndexView(MultipleCreateView): 
    template_name = 'index.html' 
    form_class ={'form' : PersonForm, 'form2' : CarForm} 
    success_url = '/'

In template:

<form method="POST">
    {% csrf_token %}
    {{ form }}       
    {{form2}}
    <input type="submit" value="Submit">
</form>

Class:

class MultipleCreateView(CreateView): 

    def get_context_data(self, **kwargs): 
        for key, value in self.form_class.items(): 
            if key not in kwargs: 
                kwargs[key] = value
        return super().get_context_data(**kwargs)  

    def post(self,  request, *args, **kwargs):
        self.object = None 
        forms = {}
        for key, value in self.form_class.items(): 
            forms[key] = value(**self.get_form_kwargs()) 
        for form in forms.values(): 
            if not form.is_valid():
                return self.form_invalid(**forms) 
        return self.form_valid(**forms) 

    def form_valid(self, **forms):
        for i, validForm in enumerate(forms.values()) : 
            if i == 0:
                self.object = validForm.save()
            validForm.save()  
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self,**forms):
        return self.render_to_response(self.get_context_data(**forms))

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Flux
Solution 3
Solution 4 Leonardo Paz Estevam