Django: Can class-based views accept two forms at a time?
Asked Answered
F

7

30

If I have two forms:

class ContactForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

class SocialForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

and wanted to use a class based view, and send both forms to the template, is that even possible?

class TestView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm

It seems the FormView can only accept one form at a time. In function based view though I can easily send two forms to my template and retrieve the content of both within the request.POST back.

variables = {'contact_form':contact_form, 'social_form':social_form }
return render(request, 'discussion.html', variables)

Is this a limitation of using class based view (generic views)?

Many Thanks

Flyer answered 19/3, 2013 at 11:3 Comment(2)
Have you looked into FormSets? docs.djangoproject.com/en/dev/topics/forms/formsets EDIT: Some insight might lie in here: #6276898Poock
unless I have misunderstod formsets, each formset is a collection of the same form. My forms are different. Hence I am don't think I can use a formset. Correct me if I am wrongFlyer
H
49

Here's a scaleable solution. My starting point was this gist,

https://gist.github.com/michelts/1029336

i've enhanced that solution so that multiple forms can be displayed, but either all or an individual can be submitted

https://gist.github.com/jamesbrobb/748c47f46b9bd224b07f

and this is an example usage

class SignupLoginView(MultiFormsView):
    template_name = 'public/my_login_signup_template.html'
    form_classes = {'login': LoginForm,
                    'signup': SignupForm}
    success_url = 'my/success/url'

    def get_login_initial(self):
        return {'email':'[email protected]'}

    def get_signup_initial(self):
        return {'email':'[email protected]'}

    def get_context_data(self, **kwargs):
        context = super(SignupLoginView, self).get_context_data(**kwargs)
        context.update({"some_context_value": 'blah blah blah',
                        "some_other_context_value": 'blah'})
        return context

    def login_form_valid(self, form):
        return form.login(self.request, redirect_url=self.get_success_url())

    def signup_form_valid(self, form):
        user = form.save(self.request)
        return form.signup(self.request, user, self.get_success_url())

and the template looks like this

<form class="login" method="POST" action="{% url 'my_view' %}">
    {% csrf_token %}
    {{ forms.login.as_p }}

    <button name='action' value='login' type="submit">Sign in</button>
</form>

<form class="signup" method="POST" action="{% url 'my_view' %}">
    {% csrf_token %}
    {{ forms.signup.as_p }}

    <button name='action' value='signup' type="submit">Sign up</button>
</form>

An important thing to note on the template are the submit buttons. They have to have their 'name' attribute set to 'action' and their 'value' attribute must match the name given to the form in the 'form_classes' dict. This is used to determine which individual form has been submitted.

Henrie answered 3/6, 2014 at 9:21 Comment(4)
Thanks James! This is pretty slick! A question though. Your examples for _form_valid return form.<form name>(), but that doesn't seem right. Should those just be returning forms_valid()?Biographical
@Biographical Those methods are called by forms_valid()Henrie
@Henrie I'm trying to use your solution. I understand that def get_login_initial and def get_signup_initial are just setting up the email field be default (saving some typing to the user). If I don't want to prepopulate any data on the form, I don't have to write these two methods? For example, I have a Reference form, that will update if the reference of the job applicant is valid. So, I should have: def get_reference1_initial: pass, def get_reference2_initial: pass, def get_reference3_initial: pass? Thank you.Brno
@Henrie This code library is not working with Django 3.0. I am getting following error- AttributeError at /album/add/ 'AlbumForm' object has no attribute 'album' --snipboard.io/D67zIH.jpgSassanid
O
28

By default, class-based views only support a single form per view. But there are other ways to accomplish what you need. But again, this cannot handle both forms at the same time. This will also work with most of the class-based views as well as regular forms.

views.py

class MyClassView(UpdateView):

    template_name = 'page.html'
    form_class = myform1
    second_form_class = myform2
    success_url = '/'

    def get_context_data(self, **kwargs):
        context = super(MyClassView, self).get_context_data(**kwargs)
        if 'form' not in context:
            context['form'] = self.form_class(request=self.request)
        if 'form2' not in context:
            context['form2'] = self.second_form_class(request=self.request)
        return context

    def get_object(self):
        return get_object_or_404(Model, pk=self.request.session['value_here'])

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

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        if 'form' in request.POST:
            form_class = self.get_form_class()
            form_name = 'form'
        else:
            form_class = self.second_form_class
            form_name = 'form2'

        form = self.get_form(form_class)

        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(**{form_name: form})

template

<form method="post">
    {% csrf_token %}
    .........
    <input type="submit" name="form" value="Submit" />
</form>

<form method="post">
    {% csrf_token %}
    .........
    <input type="submit" name="form2" value="Submit" />
</form>
Omega answered 19/3, 2013 at 12:15 Comment(1)
this also solves the same problem... chriskief.com/2012/12/30/…Henrie
M
15

Its is possible for one class-based view to accept two forms at a time.

view.py

class TestView(FormView):
    template_name = 'contact.html'
    def get(self, request, *args, **kwargs):
        contact_form = ContactForm()
        contact_form.prefix = 'contact_form'
        social_form = SocialForm()
        social_form.prefix = 'social_form'
        # Use RequestContext instead of render_to_response from 3.0
        return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))

    def post(self, request, *args, **kwargs):
        contact_form = ContactForm(self.request.POST, prefix='contact_form')
        social_form = SocialForm(self.request.POST, prefix='social_form ')

        if contact_form.is_valid() and social_form.is_valid():
            ### do something
            return HttpResponseRedirect(>>> redirect url <<<)
        else:
            return self.form_invalid(contact_form,social_form , **kwargs)


    def form_invalid(self, contact_form, social_form, **kwargs):
        contact_form.prefix='contact_form'
        social_form.prefix='social_form'

        return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))

forms.py

from django import forms
from models import Social, Contact
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Button, Layout, Field, Div
from crispy_forms.bootstrap import (FormActions)

class ContactForm(forms.ModelForm):
    class Meta:
        model = Contact
    helper = FormHelper()
    helper.form_tag = False

class SocialForm(forms.Form):
    class Meta:
        model = Social
    helper = FormHelper()
    helper.form_tag = False

HTML

Take one outer form class and set action as TestView Url

{% load crispy_forms_tags %}
<form action="/testview/" method="post">
  <!----- render your forms here -->
  {% crispy contact_form %}
  {% crispy social_form%}
  <input type='submit' value="Save" />
</form>

Good Luck

Mancuso answered 8/5, 2015 at 5:38 Comment(5)
This solution worked but the only problem was the form was not getting initialized with data if I was using contact_form = ContactForm(self.request.POST, prefix='contact_form') social_form = SocialForm(self.request.POST, prefix='social_form ') but it worked if removed prefix from the both forms. I did not understand this behavior.Leonleona
Prefix used to generate the form initially.Mancuso
return self.render_to_response(self.get_context_data('contact_form':contact_form, 'social_form':social_form )) --- SyntaxError: invalid syntax Why?Sialagogue
render_to_response() was removed in Django 3.0. Use RequestContext instead of that.Mancuso
I am getting error get_context_data() takes 1 positional argument but 2 were given in return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form})). Am I doing something wrong?Flown
S
2

I have used a following generic view based on TemplateView:

def merge_dicts(x, y):
    """
    Given two dicts, merge them into a new dict as a shallow copy.
    """
    z = x.copy()
    z.update(y)
    return z


class MultipleFormView(TemplateView):
    """
    View mixin that handles multiple forms / formsets.
    After the successful data is inserted ``self.process_forms`` is called.
    """
    form_classes = {}

    def get_context_data(self, **kwargs):
        context = super(MultipleFormView, self).get_context_data(**kwargs)
        forms_initialized = {name: form(prefix=name)
                             for name, form in self.form_classes.items()}

        return merge_dicts(context, forms_initialized)

    def post(self, request):
        forms_initialized = {
            name: form(prefix=name, data=request.POST)
            for name, form in self.form_classes.items()}

        valid = all([form_class.is_valid()
                     for form_class in forms_initialized.values()])
        if valid:
            return self.process_forms(forms_initialized)
        else:
            context = merge_dicts(self.get_context_data(), forms_initialized)
            return self.render_to_response(context)

    def process_forms(self, form_instances):
        raise NotImplemented

This has the advantage that it is reusable and all the validation is done on the forms themselves.

It is then used as follows:

class AddSource(MultipleFormView):
    """
    Custom view for processing source form and seed formset
    """
    template_name = 'add_source.html'
    form_classes = {
        'source_form': forms.SourceForm,
        'seed_formset': forms.SeedFormset,
    }

    def process_forms(self, form_instances):
        pass # saving forms etc
Sacksen answered 27/3, 2015 at 1:6 Comment(0)
C
1

It is not a limitation of class-based views. Generic FormView just is not designed to accept two forms (well, it's generic). You can subclass it or write your own class-based view to accept two forms.

Cranberry answered 19/3, 2013 at 11:25 Comment(2)
Subclassing sounds interesting. Do you happen to know how to achieve this? I find this new approach quite confusing. The question is if its worth the effort. Why not just stick to function based view in this case? Wouldn't it be simpler?Flyer
it depends on how would you handle them. Do you have two separate success URLs? Are both forms GET/POST, do they share same action attr? The most straitforward way to understand how generic views work is to look inside Django code to see what you need to changeCranberry
G
1

Use django-superform

This is a pretty neat way to thread a composed form as a single object to outside callers, such as the Django class based views.

from django_superform import FormField, SuperForm

class MyClassForm(SuperForm):
    form1 = FormField(FormClass1)
    form2 = FormField(FormClass2)

In the view, you can use form_class = MyClassForm

In the form __init__() method, you can access the forms using: self.forms['form1']

There is also a SuperModelForm and ModelFormField for model-forms.

In the template, you can access the form fields using: {{ form.form1.field }}. I would recommend aliasing the form using {% with form1=form.form1 %} to avoid rereading/reconstructing the form all the time.

Goodtempered answered 30/1, 2017 at 15:20 Comment(0)
B
1

Resembles @james answer (I had a similar starting point), but it doesn't need to receive a form name via POST data. Instead, it uses autogenerated prefixes to determine which form(s) received POST data, assign the data, validate these forms, and finally send them to the appropriate form_valid method. If there is only 1 bound form it sends that single form, else it sends a {"name": bound_form_instance} dictionary.

It is compatible with forms.Form or other "form behaving" classes that can be assigned a prefix (ex. django formsets), but haven't made a ModelForm variant yet, tho you could use a model form with this View (see edit below). It can handle forms in different tags, multiple forms in one tag, or a combination of both.

The code is hosted on github (https://github.com/AlexECX/django_MultiFormView). There are some usage guidelines and a little demo covering some use cases. The goal was to have a class that feels as close as possible like the FormView.

Here is an example with a simple use case:

views.py

    class MultipleFormsDemoView(MultiFormView):
        template_name = "app_name/demo.html"

        initials = {
            "contactform": {"message": "some initial data"}
        }

        form_classes = [
            ContactForm,
            ("better_name", SubscriptionForm),
        ]

        # The order is important! and you need to provide an
        # url for every form_class.
        success_urls = [
            reverse_lazy("app_name:contact_view"),
            reverse_lazy("app_name:subcribe_view"),
        ]
        # Or, if it is the same url:
        #success_url = reverse_lazy("app_name:some_view")

        def get_contactform_initial(self, form_name):
            initial = super().get_initial(form_name)
            # Some logic here? I just wanted to show it could be done,
            # initial data is assigned automatically from self.initials anyway
            return initial

        def contactform_form_valid(self, form):
            title = form.cleaned_data.get('title')
            print(title)
            return super().form_valid(form) 

        def better_name_form_valid(self, form):
            email = form.cleaned_data.get('email')
            print(email)
            if "Somebody once told me the world" is "gonna roll me":
                return super().form_valid(form)
            else:
                return HttpResponse("Somebody once told me the world is gonna roll me")

template.html

{% extends "base.html" %}

{% block content %}

<form method="post">
    {% csrf_token %}
    {{ forms.better_name }}
    <input type="submit" value="Subscribe">
</form>

<form method="post">
    {% csrf_token %}
    {{ forms.contactform }}
    <input type="submit" value="Send">
</form>

{% endblock content %}

EDIT - about ModelForms

Welp, after looking into ModelFormView I realised it wouldn't be that easy to create a MultiModelFormView, I would probably need to rewrite SingleObjectMixin as well. In the mean time, you can use a ModelForm as long as you add an 'instance' keyword argument with a model instance.

def get_bookform_form_kwargs(self, form_name):
    kwargs = super().get_form_kwargs(form_name)
    kwargs['instance'] = Book.objects.get(title="I'm Batman")
    return kwargs
Balzac answered 26/8, 2018 at 3:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.