Иногда на странице какого-либо объекта нужно вывести форму. При этом объект выводится с помощью 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, но это будет вообще громоздко.
На мой взгляд более правильный и изящный способ — объединить два в одном. Для этого нужно
- К DetailView примешать FormMixin
- Определить метод
post()
- Добавить форму в 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-запросах — обрабатывать форму.