'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:
It lacks input validation.
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 HTMLname
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 |