Правильное объединение FormView и DetailView

  • Опубликовано:
  • Теги: class-based-views

Иногда на странице какого-либо объекта нужно вывести форму. При этом объект выводится с помощью Class Based View (далее CBV).

Допустим, есть какое-то мероприятие

class Event(models.Model):
    title = models.CharField(max_length=100)
    date = modes.DateTime()
    description = models.TextField()

и на странице этого мероприятия мы хотим добавить форму, чтобы пользователь мог отправить заявку на участие в этом мероприятии, вследствие чего на почту менеджерам приходило бы письмо. Допустим, пользователю нужно заполнить имя и фамилию, тогда простая форма для отправки сообщения выглядит так:

class BookForm(form.Form):
    first_name = forms.CharField()
    last_name = forms.CharField()
    event = forms.CharField(widget=HiddenInput())

Тогда обычный View выглядел бы так:

class EventDetail(DetailView):
    model = Event

Сразу на ум приходит очевидный вариант добавить форму в контекст с помощью get_context_data(), а обработку формы передать какому-нибудь другому CBV.

Но в таком случае отображать ошибки или сообщение об успешной отправке формы нужно будет во втором view, а потом делать редирект на первый. Также надо будет указать другой шаблон. В документации этот вариант указывается как alternative better solution и в в этом есть смысл, если CBV достаточно сложный.

Проблема такого подхода в том, что, если в форме есть ошибки, то отображены они будут на другой странице (не на странице нашего DetailView). Хотя возможно можно попробовать передавать инвалидную форму с контекстом обратно в наш DetailView, но это будет вообще громоздко.

На мой взгляд более правильный и изящный способ — объединить два в одном. Для этого нужно

  1. К DetailView примешать FormMixin
  2. Определить метод post()
  3. Добавить форму в RequestContext

Как и в обычном FormView необходимо также указать form_class и success_url либо переопределить get_form_class() и get_success_url().

Вот как будет выглядеть конечный view:

# -*- coding: utf-8 -*-
from django.contrib import messages
from django.core.mail import mail_managers
from django.urls import reverse
from django.utils import formats


class EventView(FormMixin, DetailView):
    form_class = BookForm

    def get_success_url(self):
        return reverse('event_detail', args=[self.get_object.pk])

    def get_context_data(self, **kwargs):
        ctx = ctx = super(EventView, self).get_context_data(**kwargs)
        ctx['form'] = self.get_form()
        return ctx

    def get_initial(self):
        return ({'event': self.get_object()})

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        cd = form.cleaned_data
        mail_managers(u"{} {} зарегистрировался(лась) на мероприятие {}"
                      u"".format(cd["first_name"], cd["last_name"], self.get_object.title)
        messages.success(self.request, u"Ваша заявка принята")
        return super(EventView, self).form_valid(form)

Поскольку параметр template_name не задан, то DetailView возьмет значение по умолчанию, поэтому наш шаблон должен быть в файле event_detail.html

Заключение

Таким образом наш CBV при get-запросах будет вести себя как обычный DetailView, а при post-запросах — обрабатывать форму.

Cсылки по теме