Proper way to handle multiple forms on one page in Django

Viewed 228188

I have a template page expecting two forms. If I just use one form, things are fine as in this typical example:

if request.method == 'POST':
    form = AuthorForm(request.POST,)
    if form.is_valid():
        form.save()
        # do something.
else:
    form = AuthorForm()

If I want to work with multiple forms however, how do I let the view know that I'm submitting only one of the forms and not the other (i.e. it's still request.POST but I only want to process the form for which the submit happened)?


This is the solution based on the answer where expectedphrase and bannedphrase are the names of the submit buttons for the different forms and expectedphraseform and bannedphraseform are the forms.

if request.method == 'POST':
    if 'bannedphrase' in request.POST:
        bannedphraseform = BannedPhraseForm(request.POST, prefix='banned')
        if bannedphraseform.is_valid():
            bannedphraseform.save()
        expectedphraseform = ExpectedPhraseForm(prefix='expected')
    elif 'expectedphrase' in request.POST:
        expectedphraseform = ExpectedPhraseForm(request.POST, prefix='expected')
        if expectedphraseform.is_valid():
            expectedphraseform.save() 
        bannedphraseform = BannedPhraseForm(prefix='banned')
else:
    bannedphraseform = BannedPhraseForm(prefix='banned')
    expectedphraseform = ExpectedPhraseForm(prefix='expected')
12 Answers

view:

class AddProductView(generic.TemplateView):
template_name = 'manager/add_product.html'

    def get(self, request, *args, **kwargs):
    form = ProductForm(self.request.GET or None, prefix="sch")
    sub_form = ImageForm(self.request.GET or None, prefix="loc")
    context = super(AddProductView, self).get_context_data(**kwargs)
    context['form'] = form
    context['sub_form'] = sub_form
    return self.render_to_response(context)

def post(self, request, *args, **kwargs):
    form = ProductForm(request.POST,  prefix="sch")
    sub_form = ImageForm(request.POST, prefix="loc")
    ...

template:

{% block container %}
<div class="container">
    <br/>
    <form action="{% url 'manager:add_product' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        {{ sub_form.as_p }}
        <p>
            <button type="submit">Submit</button>
        </p>
    </form>
</div>
{% endblock %}

Based on this answer by @ybendana:

Again, we use is_bound to check if the form is capable of validation. See this section of the documentation:

Bound and unbound forms

A Form instance is either bound to a set of data, or unbound.

  • If it’s bound to a set of data, it’s capable of validating that data and rendering the form as HTML with the data displayed in the HTML.
  • If it’s unbound, it cannot do validation (because there’s no data to validate!), but it can still render the blank form as HTML.

We use a list of tuples for form objects and their details allowing for more extensibility and less repetition.

However, instead of overriding get(), we override get_context_data() to make inserting a new, blank instance of the form (with prefix) into the response the default action for any request. In the context of a POST request, we override the post() method to:

  1. Use the prefix to check if each form has been submitted
  2. Validate the forms that have been submitted
  3. Process the valid forms using the cleaned_data
  4. Return any invalid forms to the response by overwriting the context data
# views.py

class MultipleForms(TemplateResponseMixin, ContextMixin, View):

    form_list = [ # (context_key, formcls, prefix)
        ("form_a", FormA, "prefix_a"),
        ("form_b", FormB, "prefix_b"),
        ("form_c", FormC, "prefix_c"),
        ...
        ("form_x", FormX, "prefix_x"),
    ]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # Add blank forms to context with prefixes
        for context_key, formcls, prefix in self.form_list:
            context[context_key] = formcls(prefix=prefix)
        return context

    def post(self, request, *args, **kwargs):
        # Get object and context
        self.object = self.get_object()
        context = self.get_context_data(object=self.object)
        # Process forms
        for context_key, formcls, prefix in self.form_list:
            if prefix in request.POST:
                # Get the form object with prefix and pass it the POST data to \
                # validate and clean etc.
                form = formcls(request.POST, prefix=prefix)
                if form.is_bound:
                    # If the form is bound (i.e. it is capable of validation)  \
                    # check the validation
                    if form.is_valid():
                        # call the form's save() method or do whatever you     \
                        # want with form.cleaned_data
                        form.save()
                    else:
                        # overwrite context data for this form so that it is   \
                        # returned to the page with validation errors
                        context[context_key] = form
        # Pass context back to render_to_response() including any invalid forms
        return self.render_to_response(context)
        

This method allows repeated form entries on the same page, something I found did not work with @ybendana's answer.

I believe it wouldn't be masses more work to fold this method into a Mixin class, taking the form_list object as an attribute and hooking get_context_data() and post() as above.

Edit: This already exists. See this repository.

NB: This method required TemplateResponseMixin for render_to_response() and ContextMixin for get_context_data() to work. Either use these Mixins or a CBV that descends from them.

if request.method == 'POST':
    expectedphraseform = ExpectedphraseForm(request.POST)
    bannedphraseform = BannedphraseForm(request.POST)
    if expectedphraseform.is_valid():
        expectedphraseform.save()
        return HttpResponse("Success")
    if bannedphraseform.is_valid():
        bannedphraseform.save()
        return HttpResponse("Success")
else:
    bannedphraseform = BannedphraseForm()
    expectedphraseform = ExpectedphraseForm()
return render(request, 'some.html',{'bannedphraseform':bannedphraseform, 'expectedphraseform':expectedphraseform})

This worked for me accurately as I wanted. This Approach has a single problem that it validates both the form's errors. But works Totally fine.

I discovered a pretty interesting way to send TWO Forms from a single page using the same view. I tried many options but just wanted something that can just work. So here is something that I discovered. But it only works when there are just TWO Forms on a page.

I am using just try and except method to first try first form and if that doesnt works than try second form. This is quiet interesting to know that it works absolutely fine. Don't use it on scalable application as it can create troublesome or may risk the security of the application, else use Class based view to submit mutiple forms or create seperate views for each form.

def create_profile(request):
    if request.method=='POST':
        try:       
            biograph = Biography(name=name, email=email, full_name=full_name, slug_name=slug_name, short_bio=short_bio)
            biograph.save()

        except:
            social = SocialMedia(twitter=twitter, instagram=instagram, facebook=facebook, linkedin=linkedin, github=github)
            social.save()
Related