Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Generic Class-Based Views
generic_views

Generic Class-Based Views

ListView, DetailView, CreateView, UpdateView, DeleteView, FormView

Generic class-based views

Generic CBV реализуют повторяющийся HTTP-сценарий: показать список, найти объект, отобразить форму, обработать ошибки и перенаправить после успеха. Они сокращают инфраструктурный код, но не принимают за разработчика решения о доступе, составе полей и границах транзакции.

Материал актуален для Django 5.2 LTS и 6.0.

#Как читать generic view

У класса есть несколько точек настройки:

  • атрибуты задают постоянную конфигурацию: model, template_name, paginate_by;
  • get_queryset() определяет видимые объекты;
  • get_context_data() дополняет контекст шаблона;
  • form_valid() выполняет работу после успешной валидации;
  • get_success_url() вычисляет адрес перенаправления.

Переопределяйте наиболее узкую подходящую точку. Полная замена dispatch() или post() часто случайно выбрасывает уже реализованную обработку формы.

#ListView: список начинается с области видимости

from django.views.generic import ListView from .models import Post class PostListView(ListView): template_name = "blog/post_list.html" context_object_name = "posts" paginate_by = 20 def get_queryset(self): return ( Post.objects.published() .select_related("author", "category") .prefetch_related("tags") .order_by("-published_at", "-pk") )

ListView передаст posts, page_obj, paginator и is_paginated. Стабильная сортировка важна для пагинации. get_queryset() вызывается на запрос и подходит для параметров URL и текущего пользователя.

Атрибут queryset тоже поддерживается:

queryset = Post.objects.published()

Django клонирует его через .all(), поэтому это не общий кэш результатов между запросами. Но метод яснее, когда фильтр зависит от self.request или self.kwargs.

#Не теряйте query string при пагинации

Простая ссылка ?page=2 удалит фильтры поиска. Удобно передавать строку параметров без page из view или создать безопасный template tag. Нельзя собирать URL простой конкатенацией неэкранированного ввода.

#DetailView: 404 должен учитывать политику доступа

from django.views.generic import DetailView class PostDetailView(DetailView): template_name = "blog/post_detail.html" context_object_name = "post" slug_field = "slug" slug_url_kwarg = "slug" def get_queryset(self): return ( Post.objects.published() .select_related("author", "category") .prefetch_related("tags") )

Объект ищется внутри get_queryset(). Поэтому черновик не раскроется по прямому URL. Отдельная проверка после get_object() хуже: она может отличаться между GET и POST или выдать лишнюю информацию о существовании записи.

Дополнительный контекст строится от уже найденного self.object:

def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["related_posts"] = ( Post.objects.published() .filter(category=self.object.category) .exclude(pk=self.object.pk)[:4] ) return context

Используйте ту же доменную выборку published(), что и в основном списке. Несогласованные флаги is_published и status создают трудноуловимые утечки.

#CreateView: разрешённые поля и владелец

from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.views.generic import CreateView from .forms import PostForm class PostCreateView(LoginRequiredMixin, CreateView): form_class = PostForm template_name = "blog/post_form.html" def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) def get_success_url(self): return reverse("blog:post_detail", kwargs={"slug": self.object.slug})

CreateView сохранит форму и положит созданную модель в self.object. Для динамического URL внутри метода используют reverse(). reverse_lazy() нужен прежде всего для атрибутов класса, которые вычисляются во время импорта.

Не используйте fields = "__all__" для пользовательской формы. При добавлении служебного поля в модель оно внезапно станет редактируемым. Явный ModelForm фиксирует публичный контракт.

#UpdateView: ограничьте QuerySet, а не только кнопку

from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.views.generic import UpdateView from .models import PostRevision class PostUpdateView(LoginRequiredMixin, UpdateView): form_class = PostForm template_name = "blog/post_form.html" def get_queryset(self): queryset = Post.objects.all() if self.request.user.is_staff: return queryset return queryset.filter(author=self.request.user) def form_valid(self, form): old_body = self.object.body with transaction.atomic(): response = super().form_valid(form) if old_body != self.object.body: PostRevision.objects.create( post=self.object, old_body=old_body, new_body=self.object.body, changed_by=self.request.user, ) return response

Скрытие ссылки в шаблоне не является авторизацией. Фильтр get_queryset() применяется и при показе, и при отправке формы. Транзакция не допускает сохранения статьи без обязательной ревизии.

Этот пример всё ещё использует last-write-wins. Если два редактора могут открыть одну версию, добавьте version-поле и оптимистическую проверку либо блокировку, исходя из продукта.

#DeleteView и мягкое удаление

Стандартный DeleteView показывает подтверждение на GET и удаляет на POST. Изменять данные по GET нельзя:

<form method="post"> {% csrf_token %} <p>Удалить «{{ object.title }}»?</p> <button type="submit">Удалить</button> </form>

Доступ снова ограничивается через get_queryset(). Для физического удаления достаточно success_url:

from django.urls import reverse_lazy from django.views.generic import DeleteView class PostDeleteView(LoginRequiredMixin, DeleteView): template_name = "blog/post_confirm_delete.html" success_url = reverse_lazy("blog:post_list") def get_queryset(self): return Post.objects.filter(author=self.request.user)

#Актуальное переопределение soft delete

В современном DeleteView POST проходит через форму, а удаление выполняется в form_valid(). Поэтому мягкое удаление переопределяет именно его:

from django.http import HttpResponseRedirect from django.utils import timezone class PostArchiveView(LoginRequiredMixin, DeleteView): model = Post template_name = "blog/post_confirm_archive.html" success_url = reverse_lazy("blog:post_list") def get_queryset(self): return Post.objects.filter(author=self.request.user, deleted_at__isnull=True) def form_valid(self, form): success_url = self.get_success_url() self.object.deleted_at = timezone.now() self.object.save(update_fields=["deleted_at"]) return HttpResponseRedirect(success_url)

Soft delete требует согласованного QuerySet/Manager, правил уникальности и поведения связей. Один флаг не решает эти вопросы.

#FormView: форма не обязана сохранять модель

FormView подходит поиску, импорту, контакту или другой форме, где нет одного стандартного ModelForm.save():

from django.urls import reverse_lazy from django.views.generic import FormView class ContactView(FormView): template_name = "contact/form.html" form_class = ContactForm success_url = reverse_lazy("contact_done") def form_valid(self, form): form.enqueue_message() return super().form_valid(form)

Метод формы здесь может создать запись outbox или поставить задачу после commit. Отправка письма прямо в HTTP-запросе увеличивает задержку и усложняет повтор при сбое.

#Две формы требуют общего жизненного цикла

Generic views рассчитаны прежде всего на одну форму. Если регистрация сохраняет пользователя и профиль, обе формы должны:

  1. быть bound на POST;
  2. вернуться в контекст со своими ошибками;
  3. сохраниться в одной транзакции;
  4. перенаправить только после успеха обеих.
from django.db import transaction from django.http import HttpResponseRedirect class RegistrationView(FormView): template_name = "accounts/register.html" form_class = UserRegistrationForm profile_form_class = ProfileForm success_url = reverse_lazy("accounts:register_done") def get_profile_form(self): return self.profile_form_class( self.request.POST or None, self.request.FILES or None, ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.setdefault("profile_form", self.get_profile_form()) return context def post(self, request, *args, **kwargs): user_form = self.get_form() profile_form = self.get_profile_form() if not (user_form.is_valid() and profile_form.is_valid()): return self.render_to_response(self.get_context_data( form=user_form, profile_form=profile_form, )) with transaction.atomic(): user = user_form.save() profile = profile_form.save(commit=False) profile.user = user profile.save() return HttpResponseRedirect(self.get_success_url())

Если такая orchestration разрастается, отдельная view или сервис регистрации читается лучше, чем попытка заставить FormView изображать специализированный workflow.

#Date-based views

YearArchiveView, MonthArchiveView и другие date-based generic views остаются актуальны. Они удобны для простого архива публикаций:

from django.views.generic.dates import MonthArchiveView class PostMonthArchiveView(MonthArchiveView): queryset = Post.objects.published() date_field = "published_at" month_format = "%m" allow_future = False make_object_list = True

Проверьте timezone и индекс поля даты. Для сложной аналитики календарный generic view не заменяет отдельный отчёт.

#Legacy и актуальность

ListView, DetailView, edit views и date views актуальны в Django 5.2/6.0. В legacy-коде можно встретить переопределение DeleteView.delete() для soft delete. После переработки DeleteView в Django 4.0 POST обрабатывается через FormMixin, поэтому такую логику переносят в form_valid().

Другие частые legacy-запахи — success_url = "/posts/", повторный запрос объекта в test_func() и две формы, где при ошибке теряется вторая. Исправляйте их с тестами GET, валидного POST, невалидного POST и доступа к чужому объекту.

#Документация

  • Generic display views
  • Generic editing views
  • Built-in class-based views API

Далее: URL Routing