【问题标题】:Django: Can class-based views accept two forms at a time?Django:基于类的视图可以一次接受两种形式吗?
【发布时间】:2013-03-08 00:30:42
【问题描述】:

如果我有两种形式:

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 一次只能接受一个表单。 在基于函数的视图中,虽然我可以轻松地将两个表单发送到我的模板并在 request.POST 中检索两者的内容。

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

这是使用基于类的视图(通用视图)的限制吗?

非常感谢

【问题讨论】:

标签: django django-class-based-views django-1.5


【解决方案1】:

这不是基于类的视图的限制。 Generic FormView 只是不适合接受两种形式(嗯,它是通用的)。您可以对其进行子类化或编写自己的基于类的视图以接受两种形式。

【讨论】:

  • 子类化听起来很有趣。你碰巧知道如何实现这一目标吗?我发现这种新方法非常令人困惑。问题是它是否值得努力。在这种情况下,为什么不只坚持基于功能的视图呢?不是更简单吗?
  • 这取决于您将如何处理它们。您是否有两个单独的成功 URL?两种形式都是 GET/POST,它们是否共享相同的操作属性?了解通用视图如何工作的最直接的方法是查看 Django 代码以了解您需要更改的内容
【解决方案2】:

默认情况下,基于类的视图只支持每个视图一个表单。但是还有其他方法可以完成您所需要的。但同样,这不能同时处理两种形式。这也适用于大多数基于类的视图以及常规表单。

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>

【讨论】:

【解决方案3】:

这是一个可扩展的解决方案。我的出发点是这个要点,

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'字典中的表单名称匹配。这用于确定已提交的单个表单。

【讨论】:

  • 谢谢詹姆斯!这很光滑!一个问题。您的 _form_valid 示例返回 form.
    (),但这似乎不对。那些应该只是返回 forms_valid() 吗?
  • @David 这些方法被forms_valid()调用
  • @james 我正在尝试使用您的解决方案。我知道def get_login_initialdef get_signup_initial 只是将电子邮件字段设置为默认值(为用户保存一些输入)。如果我不想在表单上预填充任何数据,我不必编写这两个方法?例如,我有一个参考表,如果求职者的参考有效,它将更新。所以,我应该有:def get_reference1_initial: passdef get_reference2_initial: passdef get_reference3_initial: pass?谢谢。
  • @james 此代码库不适用于 Django 3.0。我收到以下错误 - /album/add/ 'AlbumForm' 对象的 AttributeError 没有属性 'album' --snipboard.io/D67zIH.jpg
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    一个基于类的视图可以同时接受两种形式。

    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

    取一个外部表单类并将action设置为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 ') ,表单没有被数据初始化,但如果从两个表单中删除前缀,它就可以工作。我不明白这种行为。
    • 最初用于生成表单的前缀。
    • return self.render_to_response(self.get_context_data('contact_form':contact_form, 'social_form':social_form )) --- SyntaxError: invalid syntax 为什么?
    • render_to_response() 在 Django 3.0 中被移除。使用 RequestContext 代替。
    【解决方案6】:

    使用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 %} 为表单添加别名,以避免一直重读/重构表单。

    【讨论】:

      【解决方案7】:

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

      Welp,在查看了 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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多