Django:类视图是否可以同时接受两个表单?

30
如果我有两个表单:
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)

我想使用基于类的视图,并将两个表单发送到模板中,这是否可能?

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

看起来FormView一次只能接受一个表单。但在基于函数的视图中,我可以轻松地将两个表单发送到模板,并在请求的POST后检索两个表单的内容。

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

使用基于类的视图(通用视图)是否存在这种限制?

非常感谢


你有研究过FormSets吗?https://docs.djangoproject.com/en/dev/topics/forms/formsets/ 编辑:这里可能有一些见解:https://dev59.com/j2015IYBdhLWcg3w_Q0_ - Sami N
4
除非我误解了表单集,否则每个表单集都是相同表单的集合。我的表单不同。因此,我认为我不能使用表单集。如果我错了,请纠正我。 - Houman
7个回答

49

这是一个可扩展的解决方案。我的起点是这个代码片段,

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

我增强了该解决方案,以便可以显示多个表单,但可以提交所有表单或单个表单。

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

这是一个使用示例。

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':'dave@dave.com'}

    def get_signup_initial(self):
        return {'email':'dave@dave.com'}

    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())

模板看起来像这样

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

在模板中需要注意的一件重要事情是提交按钮。它们必须将其'name'属性设置为'action',并且它们的'value'属性必须与在 'form_classes' 字典中给定的表单名称匹配。这用于确定哪个表单已经被提交。


1
谢谢James!这很不错!但有一个问题。你的_form_valid的示例中返回了form.<form name>(),但那好像不对。它们应该只返回forms_valid()吗? - David
@David 这些方法是由 forms_valid() 调用的。 - james
@james 我正在尝试使用你的解决方案。我理解 def get_login_initialdef get_signup_initial 只是默认设置了电子邮件字段(为用户节省了一些输入时间)。如果我不想在表单上预填任何数据,我就不需要编写这两个方法了吗?例如,我有一个参考表单,如果求职者的参考资料有效,它将进行更新。所以,我应该有:def get_reference1_initial: passdef get_reference2_initial: passdef get_reference3_initial: pass 吗?谢谢。 - Omar Gonzales
@james,这个代码库在Django 3.0上无法工作。我遇到了以下错误- 在/album/add/处出现AttributeError。 'AlbumForm'对象没有属性'album' --https://snipboard.io/D67zIH.jpg - Sunil Kothiyal

28

默认情况下,基于类的视图仅支持每个视图一个表单。但有其他方法可以实现您所需的功能。但是,这不能同时处理两个表单。这也适用于大多数基于类的视图以及常规表单。

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})

模板

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

1
这也解决了同样的问题... http://chriskief.com/2012/12/30/django-class-based-views-with-multiple-forms/ - james

15

一个基于类的视图可以同时处理两个表单。

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

取一个外部表单类,并将动作设置为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>

祝你好运


这个解决方案可行,但唯一的问题是如果我使用contact_form = ContactForm(self.request.POST, prefix='contact_form') social_form = SocialForm(self.request.POST, prefix='social_form ')表单不会初始化数据,但如果从两个表单中删除前缀,则可以正常工作。我不理解这种行为。 - Javed
用于最初生成表单的前缀。 - Naresh Chaudhary
返回 self.render_to_response(self.get_context_data('contact_form':contact_form, 'social_form':social_form )) 时出现 SyntaxError: invalid syntax 错误。为什么? - JopaBoga
render_to_response()在Django 3.0中已被移除。请改用RequestContext。 - Naresh Chaudhary
我在 return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form})) 中遇到了错误 get_context_data() takes 1 positional argument but 2 were given。我做错了什么吗? - Love Putin Not War

2
我使用了基于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

这种方法的优点在于它具有可重复使用性,并且所有验证都在表单本身上完成。

然后按以下方式使用:

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

1

使用django-superform

这是一种不错的方式,将组合表单作为单个对象传递给外部调用者,如Django基于类的视图。

from django_superform import FormField, SuperForm

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

在视图中,您可以使用 form_class = MyClassForm
在表单的 __init__() 方法中,您可以使用以下方式访问表单:self.forms['form1']
还有一个用于模型表单的 SuperModelFormModelFormField
在模板中,您可以使用 {{ form.form1.field }} 访问表单字段。我建议使用 {% with form1=form.form1 %} 别名化表单,以避免一直重新读取/重建表单。

1
这不是类视图的限制,通用的FormView并没有设计成能够接受两个表单(因为它是通用的)。你可以子类化它或者编写自己的类视图来接受两个表单。

子类化听起来很有趣。你知道如何实现吗?我觉得这种新方法相当令人困惑。问题是是否值得这样做。在这种情况下,为什么不坚持基于函数的视图呢?这样不是更简单吗? - Houman
这取决于您如何处理它们。您是否有两个单独的成功URL?这两个表单都是GET / POST吗?它们共享相同的操作属性吗?了解通用视图的最直接方法是查看Django代码,以了解您需要更改什么。 - Marat

0

类似于@james的答案(我有一个类似的起点),但它不需要通过POST数据接收表单名称。相反,它使用自动生成的前缀来确定哪些表单接收了POST数据,分配数据,验证这些表单,最后将它们发送到适当的form_valid方法。如果只有一个绑定的表单,则发送该单个表单,否则发送{"name": bound_form_instance}字典。

它与forms.Form或其他可以分配前缀的“表单行为”类兼容(例如django表单集),但尚未制作ModelForm变体,尽管您可以在此视图中使用模型表单(请参见下面的编辑)。它可以处理不同标签中的表单,一个标签中的多个表单或两者的组合。

代码托管在github上(https://github.com/AlexECX/django_MultiFormView)。有一些使用指南和涵盖一些用例的小演示。目标是拥有一个尽可能接近FormView的类。

这里是一个简单用例的示例:

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 %}

编辑 - 关于ModelForms

好吧,在研究了ModelFormView之后,我意识到创建MultiModelFormView并不那么容易,我可能还需要重写SingleObjectMixin。与此同时,只要您添加一个带有模型实例的“instance”关键字参数,就可以使用ModelForm。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接