В прошлом уроке мы перевели список и детальную страницу на CBV. Осталась вторая половина типичного CRUD-цикла — создание, редактирование и удаление объектов. Django предоставляет готовые классы для каждого сценария: CreateView, UpdateView, DeleteView. И ещё один — FormView — для форм, не привязанных к конкретной модели.
FormView — базовый класс для форм, которые не создают и не редактируют объекты модели. Хорошие кандидаты — форма поиска, форма обратной связи, форма с несколькими шагами.
В уроке 16 мы написали представление add_review через View:
class AddReviewView(View):
def get(self, request, slug):
film = get_object_or_404(Film, slug=slug)
form = ReviewForm()
return render(request, 'films/add_review.html', {'form': form, 'film': film})
def post(self, request, slug):
film = get_object_or_404(Film, slug=slug)
form = ReviewForm(request.POST)
if form.is_valid():
review = form.save(commit=False)
review.film = film
review.save()
return redirect(film.get_absolute_url())
return render(request, 'films/add_review.html', {'form': form, 'film': film})Перепишем через FormView:
from django.views.generic.edit import FormView
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
class AddReviewView(FormView):
form_class = ReviewForm
template_name = 'films/add_review.html'
def get_film(self):
return get_object_or_404(Film, slug=self.kwargs['slug'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['film'] = self.get_film()
return context
def form_valid(self, form):
film = self.get_film()
review = form.save(commit=False)
review.film = film
review.save()
return redirect(film.get_absolute_url())Разберём ключевые методы:
form_valid(self, form) — вызывается автоматически, когда форма прошла валидацию. Получает готовый объект формы с данными. Заменяет блок if form.is_valid(): из FBV.
get_context_data() — уже знакомый метод, добавляем film в контекст, чтобы шаблон мог показать, к какому фильму относится рецензия.
self.kwargs['slug'] — параметры URL доступны через self.kwargs (именованные) и self.args (позиционные). Это стандартный способ получить URL-параметры внутри любого CBV.
FormView— базовый класс для работы с любой формой: он показывает форму, принимает данные, запускает валидацию и вызываетform_valid(), если данные корректны. Что именно делать с формой вform_valid()— решает разработчик.
Если после успешной отправки всегда нужно перейти на один и тот же URL — можно задать success_url вместо переопределения form_valid():
class AddReviewView(FormView):
form_class = ReviewForm
template_name = 'films/add_review.html'
success_url = reverse_lazy('films:film_list') # всегда на список фильмовreverse_lazy() — это ленивая версия reverse(). Нужна здесь потому, что атрибут класса вычисляется в момент импорта модуля, а маршруты могут быть ещё не загружены. reverse_lazy() откладывает вычисление URL до первого обращения — именно для этого сценария он и создан.
В нашем случае редирект зависит от конкретного фильма, поэтому success_url не подходит — переопределяем form_valid().
CreateView специализирован для создания нового объекта через форму. Это FormView, который сам знает, как сохранить объект модели:
from django.views.generic.edit import CreateView
class FilmCreateView(CreateView):
model = Film
form_class = FilmForm
template_name = 'films/add_film.html'Django автоматически:
- Создаёт пустую форму на GET-запрос
- Принимает данные формы на POST-запрос
- Вызывает
form.save()при успешной валидации - Перенаправляет на
success_urlили наfilm.get_absolute_url(), если он определён
Обрати внимание: CreateView не требует явного success_url, если у модели есть get_absolute_url() — Django сам сделает редирект на возвращаемый URL. Именно поэтому мы добавляли get_absolute_url() в урок 12.
CreateView— специализированный подклассFormView, который автоматически вызываетform.save()вform_valid()для создания нового объекта модели, и знает, как перенаправить пользователя после успешного сохранения.
Если специального класса формы нет, CreateView может сгенерировать её сам прямо из модели:
class FilmCreateView(CreateView):
model = Film
fields = ['title', 'year', 'description', 'director', 'genres', 'poster']
template_name = 'films/add_film.html'Это удобно для простых случаев, но не позволяет настроить виджеты, добавить кастомную валидацию или метки — для этого нужен явный form_class. В нашем проекте у нас уже есть FilmForm с кастомной валидацией из урока 17, поэтому используем form_class.
Если не указывать template_name, CreateView ищет шаблон по имени <app>/<model>_form.html — films/film_form.html. Мы явно указали films/add_film.html, чтобы переиспользовать существующий шаблон, а не создавать новый.
UpdateView идентичен CreateView по настройке, но ищет существующий объект и заполняет форму его текущими данными:
from django.views.generic.edit import UpdateView
class FilmUpdateView(UpdateView):
model = Film
form_class = FilmForm
template_name = 'films/film_edit.html'
context_object_name = 'film'Объект ищется так же, как в DetailView — по pk или slug из URL:
# films/urls.py
path('films/<slug:slug>/edit/', FilmUpdateView.as_view(), name='film_edit'),Шаблон отличается от формы создания только тем, что поля уже заполнены — можно переиспользовать один и тот же шаблон через template_name, но обычно делают отдельный, чтобы легко изменить заголовок и кнопку:
<!-- films/templates/films/film_edit.html -->
{% extends 'base.html' %}
{% block title %}Редактировать: {{ film.title }}{% endblock %}
{% block content %}
<h1>Редактировать фильм «{{ film.title }}»</h1>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Сохранить изменения</button>
</form>
<a href="{{ film.get_absolute_url }}">← Отменить</a>
{% endblock %}После редактирования логично перенаправить пользователя на страницу изменённого объекта. Поскольку slug может измениться при редактировании названия, используем get_success_url():
class FilmUpdateView(UpdateView):
model = Film
form_class = FilmForm
template_name = 'films/film_edit.html'
context_object_name = 'film'
def get_success_url(self):
return self.object.get_absolute_url()self.object — это уже сохранённый объект с обновлёнными данными, включая, возможно, новый slug.
DeleteView находит объект и предлагает пользователю подтвердить удаление:
from django.views.generic.edit import DeleteView
class FilmDeleteView(DeleteView):
model = Film
template_name = 'films/film_confirm_delete.html'
context_object_name = 'film'
success_url = reverse_lazy('films:film_list')Для DeleteView нет форм данных — поэтому success_url вместо get_success_url() вполне уместен: после удаления объекта self.object уже не существует, поэтому self.object.get_absolute_url() невозможен.
<!-- films/templates/films/film_confirm_delete.html -->
{% extends 'base.html' %}
{% block title %}Удалить {{ film.title }}{% endblock %}
{% block content %}
<h1>Удалить фильм?</h1>
<p>Вы уверены, что хотите удалить «{{ film.title }}»? Это действие необратимо.</p>
<form method="POST">
{% csrf_token %}
<button type="submit">Да, удалить</button>
<a href="{{ film.get_absolute_url }}">Отменить</a>
</form>
{% endblock %}Обрати внимание: форма подтверждения не содержит никаких полей — только {% csrf_token %} и кнопку. GET-запрос показывает страницу подтверждения, POST-запрос выполняет удаление.
path('films/<slug:slug>/delete/', FilmDeleteView.as_view(), name='film_delete'),Добавим на страницу фильма ссылки для редактирования и удаления:
<!-- films/templates/films/film_detail.html — добавляем в конец block content -->
<div class="film-actions">
<a href="{% url 'films:film_edit' slug=film.slug %}">Редактировать</a>
<a href="{% url 'films:film_delete' slug=film.slug %}">Удалить</a>
</div>В реальных проектах такие ссылки защищают проверкой прав доступа — только администратор или автор видит кнопки редактирования. Это мы разберём в модуле 7 через LoginRequiredMixin и PermissionRequiredMixin.
# films/urls.py
from django.urls import path
from .views import (
IndexView, AboutView, CatalogStatsView,
FilmListView, FilmDetailView, FilmCreateView, FilmUpdateView, FilmDeleteView,
AddReviewView, DirectorDetailView,
)
from . import views
app_name = 'films'
urlpatterns = [
path('', IndexView.as_view(), name='index'),
path('about/', AboutView.as_view(), name='about'),
path('films/', FilmListView.as_view(), name='film_list'),
path('films/search/', views.search_film, name='search_film'),
path('films/add/', FilmCreateView.as_view(), name='add_film'),
path('films/<slug:slug>/', FilmDetailView.as_view(), name='film_detail'),
path('films/<slug:slug>/edit/', FilmUpdateView.as_view(), name='film_edit'),
path('films/<slug:slug>/delete/', FilmDeleteView.as_view(), name='film_delete'),
path('films/<slug:slug>/review/', AddReviewView.as_view(), name='add_review'),
path('directors/<slug:slug>/', DirectorDetailView.as_view(), name='director_detail'),
path('stats/', CatalogStatsView.as_view(), name='catalog_stats'),
]# TypeError при старте — маршруты ещё не загружены на этапе импорта модуля
class FilmDeleteView(DeleteView):
success_url = reverse('films:film_list')
# reverse_lazy() откладывает вычисление до первого использования
class FilmDeleteView(DeleteView):
success_url = reverse_lazy('films:film_list')Атрибуты класса вычисляются в момент импорта Python-модуля. В этот момент URLconf может быть ещё не загружена, и обычный reverse() выбросит ошибку.
reverse_lazy() откладывает вычисление URL до первого фактического обращения к атрибуту — к тому моменту маршруты уже загружены.
Правило простое: reverse() используется внутри функций и методов, reverse_lazy() — в атрибутах класса.
Шаблоны для CreateView и UpdateView нужно оформлять с enctype="multipart/form-data", если форма содержит поле ImageField или FileField — Django не добавляет этот атрибут автоматически. Шаблон создаёт разработчик, и ответственность за enctype лежит на нём.
Если пользователь меняет название фильма, а в Film.save() slug генерируется из заголовка — после сохранения объект получит новый slug. Тогда get_success_url() вернёт URL с новым slug, и редирект пройдёт корректно. Но если где-то в коде slug захардкожен или закэширован — эта страница потеряет доступ. Напоминаем об этом как о долгосрочном архитектурном решении — не критично для учебного проекта, но важно знать.
- Чем
FormViewотличается отCreateView? - Зачем нужен
reverse_lazy()в атрибутах класса, и чем он отличается отreverse()? - Почему для
DeleteViewнельзя использоватьself.object.get_absolute_url()вget_success_url()? - Как
UpdateViewполучает существующий объект? Нужно ли писатьget_object_or_404вручную? - Чем отличается
form_valid()отget_success_url()и когда нужен каждый?
Тип: расширь проект
Добавь полноценный CRUD для режиссёров, используя CreateView, UpdateView и DeleteView.
Требования:
- Создай
DirectorForm(ModelForm) с полямиname,bio,photo - Создай
DirectorCreateView— форма создания, после успешного сохранения перенаправляет на страницу режиссёра черезget_absolute_url() - Создай
DirectorUpdateView— форма редактирования, тот же редирект - Создай
DirectorDeleteView— страница подтверждения удаления, после успешного удаления перенаправляет наfilms:film_list - Добавь три новых маршрута в
urls.py