Django: Могут ли представления на основе классов принимать две формы одновременно?

Если у меня есть две формы:

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)

Является ли это ограничением использования представления на основе классов (общие представления)?

Огромное спасибо


person Houman    schedule 19.03.2013    source источник
comment
Вы смотрели в FormSets? docs.djangoproject.com/en/dev/topics/forms/formsets РЕДАКТИРОВАТЬ: здесь может лежать некоторое понимание: stackoverflow.com/questions/6276398/   -  person Sami N    schedule 19.03.2013
comment
если я неправильно понял наборы форм, каждый набор форм представляет собой набор одной и той же формы. Мои формы другие. Следовательно, я не думаю, что могу использовать набор форм. Поправьте меня, если я ошибаюсь   -  person Houman    schedule 19.03.2013


Ответы (7)


Вот масштабируемое решение. Моей отправной точкой была эта суть,

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':'[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())

а шаблон выглядит так

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

Важно отметить в шаблоне кнопки отправки. У них должен быть установлен атрибут «имя» в «действие», а их атрибут «значение» должен соответствовать имени, данному форме в словаре «form_classes». Это используется для определения того, какая индивидуальная форма была отправлена.

person james    schedule 03.06.2014
comment
Спасибо, Джеймс! Это довольно гладко! Однако вопрос. Ваши примеры для _form_valid возвращают форму.‹имя формы›(), но это не кажется правильным. Должны ли они просто возвращать form_valid()? - person David; 11.03.2015
comment
@David Эти методы вызываются forms_valid() - person james; 12.03.2015
comment
@james Я пытаюсь использовать твое решение. Я понимаю, что def get_login_initial и def get_signup_initial просто настраивают поле электронной почты по умолчанию (сохраняя часть ввода для пользователя). Если я не хочу предварительно заполнять какие-либо данные в форме, мне не нужно писать эти два метода? Например, у меня есть форма для рекомендаций, которая будет обновляться, если рекомендация соискателя действительна. Итак, у меня должно быть: def get_reference1_initial: pass, def get_reference2_initial: pass, def get_reference3_initial: pass? Спасибо. - person Omar Gonzales; 04.01.2018
comment
@james Эта библиотека кода не работает с Django 3.0. Я получаю следующую ошибку: AttributeError в объекте /album/add/ 'AlbumForm' нет атрибута 'album' --snipboard .io/D67zIH.jpg - person Manish Kothiyal; 22.02.2020

По умолчанию представления на основе классов поддерживают только одну форму для каждого представления. Но есть и другие способы добиться того, что вам нужно. Но опять же, это не может обрабатывать обе формы одновременно. Это также будет работать с большинством представлений на основе классов, а также с обычными формами.

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>
person catherine    schedule 19.03.2013
comment
это также решает ту же проблему... chriskief.com/2012/12/30/ - person james; 30.05.2014

Одно представление на основе классов может одновременно принимать две формы.

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>

Удачи

person Naresh Chaudhary    schedule 08.05.2015
comment
Это решение сработало, но единственная проблема заключалась в том, что форма не инициализировалась данными, если я использовал contact_form = ContactForm(self.request.POST, prefix='contact_form') social_form = SocialForm(self.request.POST, prefix='social_form ') , но сработало, если удалить префикс из обеих форм. Я не понял этого поведения. - person javed; 12.03.2017
comment
Префикс, используемый для первоначального создания формы. - person Naresh Chaudhary; 14.03.2017
comment
return self.render_to_response(self.get_context_data('contact_form':contact_form, 'social_form':social_form )) --- SyntaxError: недопустимый синтаксис Почему? - person JopaBoga; 11.03.2021
comment
render_to_response() был удален в Django 3.0. Вместо этого используйте RequestContext. - person Naresh Chaudhary; 17.03.2021

Я использовал следующий общий вид, основанный на 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
person Visgean Skeloru    schedule 27.03.2015

Это не ограничение представлений на основе классов. Просто универсальный FormView не предназначен для приема двух форм (ну, он общий). Вы можете создать подкласс или написать свое собственное представление на основе классов, чтобы принимать две формы.

person Marat    schedule 19.03.2013
comment
Подкласс звучит интересно. Вы случайно не знаете, как этого добиться? Я нахожу этот новый подход довольно запутанным. Вопрос в том, стоит ли это усилий. Почему бы в этом случае просто не придерживаться представления, основанного на функциях? Не было бы проще? - person Houman; 19.03.2013
comment
это зависит от того, как бы вы с ними справились. У вас есть два отдельных URL-адреса успеха? Являются ли обе формы GET/POST, имеют ли они один и тот же атрибут действия? Самый простой способ понять, как работают универсальные представления, — это заглянуть внутрь кода Django, чтобы увидеть, что вам нужно изменить. - person Marat; 19.03.2013

Используйте 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']

Также есть SuperModelForm и ModelFormField для форм-моделей.

В шаблоне вы можете получить доступ к полям формы, используя: {{ form.form1.field }}. Я бы рекомендовал псевдоним формы с использованием {% with form1=form.form1 %}, чтобы избежать повторного чтения/реконструкции формы все время.

person vdboor    schedule 30.01.2017

Напоминает ответ @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 %}

EDIT — о ModelForms

Хорошо, изучив ModelFormView, я понял, что создать MultiModelFormView будет не так просто, мне, вероятно, также потребуется переписать SingleObjectMixin. В то же время вы можете использовать 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
person Alexandre Cox    schedule 26.08.2018