Найти в Дзене
AERONYTE

Доработка блога на Django 5

Эта статья подготовлена по мотивам книги «Django 5 By Example» (автор: Antonio Melé, издательство: Packt Publishing, 5-е издание, ISBN: 9781805122340). Статья не является официальным переводом книги и не копирует оригинальный текст. Все материалы в публикации — это мои личные переработки, пояснения, примеры и улучшения, сделанные с целью образовательной поддержки русскоязычного сообщества разработчиков. Примеры кода, использованные в статье, основаны на открытом репозитории книги и распространяются по лицензии MIT, допускающей свободное использование с указанием авторства. Все права на оригинальное содержание принадлежат автору книги и издательству Packt Publishing. 📖 Оригинальная книга доступна на официальном сайте:
https://www.packtpub.com/en-us/product/django-5-by-example-9781805122340 В прошлых статьях мы познакомились с основными компонентами Django 5, разработали блог с использованием представлений views.py, HTML шаблонов и маршрутов urls.py. В этой и последующих статьях мы бу
Оглавление

⚠️ Дисклеймер

Эта статья подготовлена по мотивам книги «Django 5 By Example» (автор: Antonio Melé, издательство: Packt Publishing, 5-е издание, ISBN: 9781805122340).

Статья не является официальным переводом книги и не копирует оригинальный текст. Все материалы в публикации — это мои личные переработки, пояснения, примеры и улучшения, сделанные с целью образовательной поддержки русскоязычного сообщества разработчиков.

Примеры кода, использованные в статье, основаны на открытом репозитории книги и распространяются по лицензии MIT, допускающей свободное использование с указанием авторства.

Все права на оригинальное содержание принадлежат автору книги и издательству Packt Publishing.

📖 Оригинальная книга доступна на официальном сайте:
https://www.packtpub.com/en-us/product/django-5-by-example-9781805122340

Введение

В прошлых статьях мы познакомились с основными компонентами Django 5, разработали блог с использованием представлений views.py, HTML шаблонов и маршрутов urls.py.

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

  • Использование канонических URL для моделей;
  • Создание SEO-дружественных URL-адресов для постов;
  • Добавление пагинации в список постов;
  • Построение представлений на основе классов;
  • Отправка писем с помощью Django;
  • Использование форм Django для отправки постов по электронной почте;
  • Добавление комментариев к постам с использованием форм, основанных на моделях.

Использование канонических URL для моделей

Канонический URL

Канонический URL (Canonical URL) — это официальный, основной URL для конкретного ресурса (страницы) на сайте. Он используется, чтобы указать поисковым системам и другим сервисам, какой из возможных дубликатов страницы является «главным» и должен индексироваться.

Зачем нужен канонический URL

Когда один и тот же контент доступен по нескольким URL, это создает проблему дублирования — поисковые роботы не знают, какой вариант считать основным. Это может:

  • понизить рейтинг страницы в поисковой выдаче;
  • разбить SEO-ценность между дубликатами;
  • вызвать путаницу при шаринге в соцсетях или кэшировании.

Пример дубликатов:

https://example.com/post/123
https://example.com/post?id=123
https://example.com/blog/123/

Во всех этих случаях канонический URL может быть:

<link rel="canonical" href="https://example.com/blog/123/">

Плюсы канонических URL:

  1. Улучшает SEO: объединяет ссылочный вес дубликатов в один основной URL.
  2. Предотвращает пессимизацию: поисковики не считают контент копипастом.
  3. Улучшает индексацию: поисковики быстрее и правильнее индексируют сайт.
  4. Упрощает шаринг: пользователи делятся «чистым» URL.
  5. Обеспечивает консистентность: разработчики и редакторы всегда ссылаются на один и тот же URL.

⚠️ Ограничения канонических URL

Когда следует использовать канонические URL:

  • Когда один и тот же пост можно открыть по разным путям (например, по id и по slug);
  • Когда есть UTM-метки или параметры фильтрации в URL (?utm_source=...);
  • Когда один и тот же контент публикуется в нескольких рубриках или разделах;
  • При пагинации или бесконечном скролле — на всех подстраницах указывается канонический URL на первую страницу.

В Django:

Реализуется через метод get_absolute_url() в модели:

-2

В HTML-шаблоне:

-3

Мы будем использовать маршрут post_detail, определённый в шаблоне маршрутов (URL patterns) приложения blog, чтобы построить канонический URL для объектов модели Post.

Django предоставляет различные функции для разрешения маршрутов, которые позволяют динамически строить URL-адреса на основе их имён и необходимых параметров. Мы воспользуемся вспомогательной функцией reverse() из модуля django.urls.

Отредактируем файл models.py в приложении blog, чтобы импортировать функцию reverse() и добавить метод get_absolute_url() в модель Post.

В импортах добавим строку:

from django.urls import reverse

В модели class Post(models.Model) в самом низу после def __str__(self) добавим новый метод def get_absolute_url(self), который будет возврщать канонический URL:

-4

Функция reverse() будет динамически строить URL, используя имя маршрута URL name, определённое в шаблоне маршрутов в urls.py. Мы использовали пространство имён blog, за которым следует двоеточие и имя маршрута post_detail.

Пространство имён blog задаётся в основном файле urls.py проекта при подключении маршрутов из blog.urls. Сам маршрут post_detail определён в файле urls.py приложения blog.

Получившаяся строка blog:post_detail может использоваться в любом месте вашего проекта для ссылки на маршрут страницы с подробной информацией о посте. Этот маршрут требует обязательного параметра — идентификатора (id) поста, который нужно получить.

Мы передали id объекта Post как позиционный аргумент, используя args=[self.id].

Обратите внимание:

  • get_absolute_url() будет возвращать канонический URL, например: /blog/42/, если в urls.py у нас прописан путь:
-5

Если же мы используем slug или дату в маршруте — необходимо указать их в kwargs:

-6

Вы можете узнать больше о вспомогательных функциях для работы с URL по ссылке: https://docs.djangoproject.com/en/5.0/ref/urlresolvers/.

Давайте заменим URL-адреса для перехода к подробному просмотру поста в шаблоне HTML, используя метод get_absolute_url().

В приложении blog откройте файл templates/blog/post/list.html и замените строку:

<a href="{% url 'blog:post_detail' post.id %}">

на строку:

<a href="{{ post.get_absolute_url }}">

Сохраните измнения и проверьте результат.

Запустите тестовый сервер в папке проекта с файлом manage.py:

python manage.py runserver

Откройте в браузере ссылку:

http://127.0.0.1:8000/blog/

Результат:

-7

Кликните в списке по любому посту и посмотрите на арес в браузере.

-8

Адрес поста теперь имеет канонический вид:

http://127.0.0.1:8000/blog/9907/

SEО оптимизация URL-адресов

Сейчас канонический URL для детального просмотра поста выглядит так:

http://127.0.0.1:8000/blog/9907/

Это не информативно для пользователей и не оптимально для SEO. Давайте проведем SEO оптимизацию URL адресов постов, так чтобы канонические URL адреса постов имели понятные информативные названия.

Будем строить URL-адрес поста на основе даты публикации: год, месяц, день и заголовка slug. В результате ссылка на пост будет выглядеть так:

http://127.0.0.1:8000/blog/2025/06/27/esenin-duel-i-pravda/

Это позволит поисковым системам Яндекс, Google лучше понимать, о чём страница, и повысит её видимость в результатах поиска — ведь в адресе будет и заголовок, и дата.

Чтобы всё это работало корректно, нужно исключить дублирование: не должно быть двух постов с одинаковым slug, опубликованных в один и тот же день. Django позволяет это контролировать через параметр unique_for_date, который мы добавим к полю slug.

В приложении blog откройте файл models.py и добавьте параметр unique_for_date='publish' в описание поля slug. Вот как это будет выглядеть:

slug = models.SlugField(max_length=250, unique_for_date='publish')
-9

Добавив параметр unique_for_date='publish', мы указали, что значение поля slug должно быть уникальным в пределах одной даты, хранящейся в поле publish.

publish — это DateTimeField с часами, минутами и секундами, но Django проверяет уникальность только по дате (без учёта времени).

Это значит, что если уже существует пост с slug="esenin-duel-i-pravda" и датой публикации 2025-06-27, то второй пост с таким же slug и той же датой сохранить не получится — Django выдаст ошибку.

Теперь, когда мы исключили возможность дублирования slug в пределах одного дня, мы можем использовать связку publish + slug для создания SEO оптимизированного URL адреса поста.

Создадим файл миграции выполнив команду:

python manage.py makemigrations blog

Результат:

-10

Применим миграции:

python manage.py migrate

Результат:

-11

Это приведёт модели приложения blog в актуальное состояние и зафиксирует изменения, даже если физически схема базы не изменится. Так мы избежим несогласованностей в будущем.

SEO оптимизированные адреса для постов

Давайте обновим шаблон маршрута так, чтобы URL детальной страницы поста включал дату публикации: год, месяц, день и slug.

Откройте файл urls.py в приложении blog и замените строку:

path('<int:id>/', views.post_detail, name='post_detail'),

На строку:

path(
'<int:year>/<int:month>/<int:day>/<slug:post>/',
views.post_detail,
name='post_detail'
),

Код в файле blog/urls.py будет иметь вид:

-12

Закоментирванный код не пиши - я его оставил для наглядности: было, стало.

Маршрут для представления post_detail принимает следующие параметры:

  • year — целое число (год публикации),
  • month — целое число (месяц),
  • day — целое число (день),
  • post — slug, то есть строку, состоящую из букв, цифр, дефисов или подчёркиваний.

Для параметров year, month и day используется путь-конвертер int, а для post — путь-конвертер slug. Подробнее о типах путь-конвертеров можно узнать в официальной документации Django:
https://docs.djangoproject.com/en/5.0/topics/http/urls/#path-converters

Теперь наши посты имеют SEO-оптимизированные информативные URL-адреса, включающие дату публикации и slug.

Следующим шагом будет изменение логики представления post_detail, чтобы она соответствовала новой структуре адресов.

SEO оптимизация представления blog/views.py

Теперь мы адаптируем представление post_detail, чтобы оно принимало параметры, соответствующие новой структуре URL — год, месяц, день и slug поста — и использовало их для получения нужного объекта Post.

Откройте файл views.py и обновите функцию post_detail следующим образом:

-13

Что мы изменили:

1. Изменили сигнатуру функции:
Было:

def post_detail(request, id):

Стало:

def post_detail(request, year, month, day, post):

Теперь функция принимает не только идентификатор поста, а разбирает параметры из URL: дату (year, month, day) и slug поста.

2. Обновили выборку объекта:

Было:

get_object_or_404(Post, id=id)

Стало:

get_object_or_404(
Post,
status=Post.Status.PUBLISHED,
slug=post,
publish__year=year,
publish__month=month,
publish__day=day
)

Теперь мы извлекаем пост, который:

  • имеет статус "Опубликовано" (PUBLISHED);
  • совпадает по slug;
  • совпадает по году, месяцу и дню публикации.

Зачем это нужно (выгоды и польза):

1. SEO-оптимизированные URL'ы

URL вида /blog/2024/06/20/django-upgrade/:

  • лучше индексируется поисковыми системами;
  • содержит ключевые слова (slug);
  • отражает хронологию блога — полезно и для людей, и для ботов.

2. Повышение читаемости и доверия

Пользователю проще понять, о чём будет страница, просто взглянув на адрес. Это увеличивает CTR и снижает показатель отказов.

3. Поддержка человекоориентированных маршрутов

Переход от ID к slug+date — шаг к «говорящим» адресам, что делает блог более профессиональным и современным.

4. Гибкая маршрутизация

Можено в будущем сортировать, фильтровать или архивировать посты по дате прямо из URL, без лишнего кода. Пример: /blog/2024/06/ — можно сделать архив месяца.

5. Безопасность и чистота данных

Благодаря unique_for_date='publish' мы гарантируем, что slug не повторяется в рамках одного дня, что делает выборку стабильной и безопасной.

На заметку (уровень senior):

  • Такой подход — best practice во всех современных CMS и блоговых платформах.
  • Это создаёт хорошую основу для реализации RSS-лент, архива по дате, sitemap.xml и удобной навигации.
  • Код легко масштабируется и поддерживается — например, можно позже добавить поддержку черновиков или постов с будущей датой публикации, не ломая URL-структуру.

SEO оптимизация канонического URL в модели Post

Откройте файл models.py в приложении blog и обновите в модели Post метод get_absolute_url() следующим образом:

-14

Почему необходимо использовать kwargs а не args?

args=[self.publish.year, self.publish.month, self.publish.day, self.slug]

Порядок аргументов в args обязан строго соответствовать маршруту. Если поменяешь порядок или забудешь что-то — будет NoReverseMatch.

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

Резюме: когда использовать kwargs, а когда args?

-15

Вывод:

Всегда используйте kwargs в reverse() на production-уровне — это безопаснее, выразительнее и легче в отладке.

Как находить все посты по дате и slug если время поста 00:00:00?

Наш код работает, но если вы укажете точное время создания поста 00:00:00 - пост не будет найден и такие посты если будут в системе нельзя будет посмотреть.

Чтобы исправить эту уязвимость в нашем приложении blog необходимо доработать код представления до следующего вида:

-16

Проблема: не находились посты, если время публикации было 00:00:00.

До внесения измнений в код мы использовали фильр:

-17

Почему это не работало?

Потому что DateTimeField (publish) в Django:

  • хранится в UTC (если в settings.py указано USE_TZ = True);
  • а __year, __month, __day — сравниваются в локальной временной зоне (например, Moscow +03:00).

Это значит:

  • мы видим дату 2024-06-20, а в базе она на самом деле сохранена как 2024-06-19 21:00:00+00:00 (в UTC!);
  • в итоге publish__day=20 не совпадает — и пост не находится.

Как мы решили проблему

-18

Что это значит на практике:

start = ... — это 2024-06-20 00:00:00 UTC

Начало нужного дня.

end = start + 1 день — это 2024-06-21 00:00:00 UTC

Конец этого дня (исключительно).

publish__gte=start,

publish__lt=end

Теперь мы ищем все посты, опубликованные в этот день по UTC, независимо от времени:

-19

Почему это надёжно?

Потому что:

  • мы явно работаем в UTC — не зависит от локальной зоны, настроек сервера или браузера;
  • неважно, в каком часовом поясе был создан пост — он будет найден, если publish лежит в пределах суток по UTC.

Это называется: фильтрация по "UTC-диапазону"

Эта техника:

  • используется в продакшн проектах с международной аудиторией;
  • предотвращает "невидимые" баги, когда в одних часовых поясах пост виден, а в других — нет.

Резюме

Как мы решили проблему с отображением постов в разных часовых поясах:

  • Убрали использование __year, __month, __day (ненадёжны при USE_TZ=True).
  • Заменили их на publish__range=(start, end) в UTC.
  • Гарантировали, что пост будет найден, даже если время публикации — 00:00:00, 23:59:59 и т.д.

Результат:

-20

Кликаем по первой строке и получаем результат:

-21

Адрес поста теперь SEO оптимизирован: отображается дата публикации поста и его slug.

http://127.0.0.1:8000/blog/2025/10/17/lermontov-gibel-i-pravda/

Проблема пропадания постов на границах часовых поясов решена.

Пагинация

Когда постов становится много необходимо отображать их в списке по 10 или по 5 штук за раз и предоставлять интерфейс навигации вперед, назад, в начало, в конец, чтобы пролистать весь список постов.

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

Для чего нужна пагинация

  1. Повышение производительности
    Загрузка сотен или тысяч записей за один запрос — это нагрузка на сервер, базу данных и браузер пользователя.
    Пагинация ограничивает количество данных, которые нужно обработать и отрендерить за один раз.
  2. Улучшение UX (удобства для пользователя)
    Пользователю проще воспринимать данные, когда они порционно распределены по страницам.
    Страницы позволяют быстро найти нужную информацию, без бесконечного скролла.
  3. Оптимизация трафика
    Если пользователь открыл только первую страницу, остальные данные даже не загружаются — это экономит трафик и снижает нагрузку.
  4. Контроль за интерфейсом
    Слишком длинные страницы выглядят перегруженными.
    Пагинация позволяет строить чистую, аккуратную навигацию: «Следующая», «Предыдущая», номера страниц и т.п.

Почему пагинация обязательна при большом количестве записей

Без пагинации, если в таблице 10 000 записей, вы рискуете:

  • получить время отклика сервера в десятки секунд,
  • перегрузить память сервера и браузера,
  • испортить впечатление от сайта у пользователя.

Что даёт пагинация в Django

Django предоставляет встроенные классы и утилиты, которые:

  • автоматически делят QuerySet на страницы;
  • обрабатывают параметры запроса ?page=2;
  • позволяют легко отобразить ссылки на страницы в шаблоне.

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

Пагинация в представлении списка постов post list view

В приложении blog отредактируйте файл views.py. Импортируйте класс Paginator из Django и измените представление post_list следующим образом:

-22

Давайте разберём, что делает новый код, который мы добавили в представление:

  1. Мы создаём экземпляр класса Paginator, указывая количество объектов, которые нужно выводить на одной странице. В нашем случае — по три поста на страницу.
  2. Мы получаем GET-параметр page из HTTP-запроса и сохраняем его в переменную page_number. Этот параметр содержит номер страницы, которую запрашивает пользователь.
    Если параметр page отсутствует, то по умолчанию используется значение
    1, чтобы загрузить первую страницу.
  3. Мы получаем объекты для нужной страницы, вызывая метод page() у экземпляра Paginator. Этот метод возвращает объект Page, который мы сохраняем в переменную posts.
  4. Мы передаём объект posts в шаблон, чтобы отобразить соответствующую страницу с постами.

Добавление пагинатора в шаблон list.html

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

В каталоге templates/blog создайте новый файл с именем pagination.html.

Добавьтеn в этот файл следующий HTML-код:

-23

Что здесь происходит:

  • page.has_previous и page.has_next — булевы свойства объекта Page, указывающие, есть ли предыдущая или следующая страница.
  • page.previous_page_number и page.next_page_number — номера соответствующих страниц.
  • page.number — текущий номер страницы.
  • page.paginator.num_pages — общее количество страниц.

Этот шаблон можно подключить в любом месте с помощью

{% include "pagination.html" %}

и передать в контекст переменную page.

Это универсальный шаблон пагинации. Он ожидает, что в контексте будет передан объект Page, чтобы отобразить ссылки на предыдущую и следующую страницы, а также показать текущую страницу и общее количество страниц с результатами.

Теперь давайте вернёмся к шаблону blog/post/list.html и подключим шаблон pagination.html внизу блока {% block content %}, следующим образом:

-24

Тег шаблона {% include %} загружает указанный шаблон и рендерит его с использованием текущего контекста шаблона. Мы используем ключевое слово with, чтобы передать дополнительные переменные контекста во включаемый шаблон.

Избавляемся от магических констант в пагинации

В шаблоне пагинации используется переменная page для отображения данных. Однако в нашем представлении мы передаём в шаблон объект Page под именем posts. Чтобы шаблон пагинации получил ожидаемую переменную, мы передаём её явно с помощью with page=posts.

В файле blog/views.py у нас есть строка:

paginator = Paginator(post_list, 3)

Где 3 - это количество постов на странице. Давайте зменим 3 на константу которая будет хранится в settings.py, чтобы мы могли централизовано менять количетво элементов в списке на одной странице пагинации.

Шаг 1: Добавьте константу в settings.py

Откройте файл mysite/settings.py и внизу добавьте строку:

# Количество постов на одной странице блога
BLOG_POSTS_PER_PAGE = 3

Шаг 2: Импортируйте settings в views.py

В файле blog/views.py добавьте импорт:

from django.conf import settings

Шаг 3: Замените число 3 на использование константы BLOG_POSTS_PER_PAGE

Измените строчку с Paginator:

paginator = Paginator(post_list, settings.BLOG_POSTS_PER_PAGE)

Финальный вид views.py:

-25

Запустите тестовый сервер командой:

python manage.py runserver

В браузере перейдите на главную странцу блога по ссылке:

http://127.0.0.1:8000/blog/

Результат:

-26

Стили и адаптивность для пагинации на Bootsrap 5

Пагинация появилась внизу и работает. Но внешний вид пагинации сильно отличаетяс о дизайна в стиле Дзен с темной темой. Давайте это исправим.

С помощью ChatGPT создадим дизайн пагинации в стиле нашего сайта, чтобы пагинация не выбивалась из общего стиля нашей страницы.

1. В базовом шаблоне base.html добавьте импорт иконок Bootsrap 5 с помощью строки кода:

<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">

2. Замените код шаблона пагинации на следующий код:

-27
-28

Много кода получилось, поэтому рекомендую зайти на страницу проекта в GitHub и скопировать код целиком.

Запустите тестовый сервер:

python manage.py runserver

Перейдите на главнуюстраницу блога по ссылке:

http://127.0.0.1:8000/blog/

Результат:

-29

Теперь пагинация выглядит презентабельно и соответcвует стилям нашей темной темы в стиле Дзен.

Ошибки пагинации

Если мы передадим неверный номер страницы числом или вместо числа передадим символы - пагинация сломается, поэтому такие нестандартные ситуации необходимо заранее предусмотреть и обработать корректно.

Запустите тестовый сервер командой:

python manage.py runserver

В браузере перейдите по ссылке:

http://127.0.0.1:8000/blog/

Кликните в пагинации на кнопку следующая страница и скопируйте адрес страницы из браузера. Вы увидите следующую строку:

http://127.0.0.1:8000/blog/?page=2

Давайте отредактируем эту строку и вместо 2 поставим 0.

http://127.0.0.1:8000/blog/?page=0

Введите этот адрес в адресную строку браузера.

Результат:

-30

Мы получили ошибку "EmptyPage at /blog/ Номер страницы меньше 1".

Теперь давайте отредактируем адрес страницы пагинации еще раз и вместо 0 напишем null.

http://127.0.0.1:8000/blog/?page=null

Введите этот адрес в адресную строку браузера.

Результат:

-31

Мы получили ошибку "PageNotAnInteger at /blog/ Номер страницы не является натуральным числом".

Чтобы устранить ошибки в работе нашего приложения blog, необходимо с помощью конструкции try except обработать все нестандартные ситуации.

В приложении blog откройте файл blog/views.py и добавьте в него следующие строки:

-32

В импорт мы добавили строку:

from django.core.paginator import EmptyPage

В функцию def post_list(request) мы добавили обработчик:

-33

Теперь введите в браузере адрес с неправилным номером страницы:

http://127.0.0.1:8000/blog/?page=0

Результат:

-34

Как видно на скриншоте выше ошибки больше нет - показывается последняя страница пагинации.

В приложении blog снова откройте файл blog/views.py и добавьте в него следующие строки:

-35

В импорт мы добавили строку:

from django.core.paginator import PageNotAnInteger

В функции def post_list(request) мы доработали обработчик - добавили except PageNotAnInteger:

-36

Теперь введите в браузере адрес с null вместо номера страницы:

http://127.0.0.1:8000/blog/?page=null

Результат:

-37

Как видно на скриншоте выше ошибки больше нет - показывается первая страница пагинации.

Что мы сделали:

  • Обернули вызов paginator.page(page_number) в try/except.
  • Если передан некорректный номер страницы — показываем первую.
  • Если передан номер за пределами доступных страниц — показываем последнюю.

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

Подробнее о классе Paginator можно прочитать здесь:
https://docs.djangoproject.com/en/5.0/ref/paginator/

Class-Based Views - CBV

Мы научились писать функции представления. Теперь настало время узнать что такое Class-Based Views - CBV и почему лучше и надежнее использовать CBV нежели самописные функции.

Что такое CBV (Class-Based Views) в Django 5

Class-Based Views (CBV) — это представления в Django, реализованные как классы, а не функции. Вместо одной функции def view(request): ... мы создаём класс, наследующийся от django.views.View или одного из его потомков, и реализуем в нём методы, соответствующие HTTP-методам: get(), post(), put() и т.д.

Для чего нужны CBV

CBV нужны, чтобы:

  • структурировать логику представлений по HTTP-методам (GET, POST и др.);
  • переиспользовать поведение через наследование и миксины;
  • снижать дублирование кода;
  • работать с generic-классами, которые упрощают стандартные CRUD-операции.

Почему CBV нужно использовать

  1. Разделение ответственности
    Каждому HTTP-методу — свой метод класса: get(), post(), put(), вместо if request.method == 'POST'.
  2. Масштабируемость
    В больших проектах проще управлять логикой через наследование классов, чем через вложенные условия в функциях.
  3. Многоразовость (DRY)
    Общая логика (например, LoginRequiredMixin, PermissionRequiredMixin, PaginationMixin) легко подключается через миксины.
  4. Быстрая разработка через generic views
    Django предоставляет мощные классы: ListView, DetailView, CreateView, UpdateView, DeleteView, — которые покрывают 90% CRUD-задач
    без ручного кода.

Преимущества CBV

-38

⚠️ Ограничения и недостатки CBV

-39

Когда использовать CBV, а когда FBV?

  • CBV — когда у вас много логики, несколько HTTP-методов, необходимость переиспользовать поведение через классы/миксины.
  • FBV — когда у вас очень простое представление (например, return render(...)), и нет необходимости усложнять.

Вывод

CBV — это мощный инструмент для построения масштабируемых, расширяемых и структурированных представлений. Они требуют хорошего понимания принципов ООП, MRO и внутренностей Django, но в опытных руках дают огромное преимущество.

Если проект маленький — FBV будет проще.

Если проект растёт —
CBV + Generic Views + миксины — ваш лучший помошник.

Перепишем post_list в PostListView на CBV

1. В приложении blog откройте файл blog/views.py и найдите функцию post_list. Добавьте строку импорта:

from django.views.generic import ListView

Вместо функции post_list запишите класс PostListView:

-40

Объяснение:

-41

2. Внесём измнения в urls.py. Добавим импорт:

from .views import PostListView

Перепишем адрес страницы списка постов на CBV:

path('', PostListView.as_view(), name='post_list'),
-42

Объяснение:

  1. path('', ...) — определяет маршрут http://.../, по которому будет доступно представление.
  2. PostListView.as_view() — превращает класс-представление (CBV) в обычную функцию, которую Django может вызвать при обработке HTTP-запроса. Это делается методом as_view(), встроенным в View.
  3. name='post_list' — даёт имя этому маршруту, чтобы потом обращаться к нему из шаблонов, других вьюх, тестов и т.д. без хардкода URL-ов.

Что даёт name='post_list'?

1. Обратное разрешение URL. Теперь в шаблоне мы можем написать:

<a href="{% url 'post_list' %}">Все посты</a>

Django сам подставит нужный URL (/blog/) по имени маршрута.

Это защищает вас от ошибок при изменении путей.

Например, если вы поменяете blog/ на posts/, шаблоны не сломаются — потому что они используют имя name='post_list'. Вам не придется переписывать все шаблоны приложения, чтобы поменять адрес ссылки на страницу постов. Это сильно упрощает работу разработчика.

2. Использование в redirect(). В views.py:

from django.shortcuts import redirect
return redirect('post_list')

Это позволяет перенаправлять пользователя на список постов без хардкода URL.

3. Использование в тестах.

reverse('post_list') # даст /blog/
self.client.get(reverse('post_list'))

Зачем мы превращаем класс-представление PostListView (CBV) в обычную функцию?

Зачем вызываем .as_view() у классового представления?

PostListView.as_view()

Мы преобразуем класс PostListView в вызываемый объект, который можно использовать в urls.py. Django ожидает функцию, которая принимает request и возвращает response. А класс сам по себе не является вызываемым — его нужно "обернуть".

Что делает as_view()?

Метод as_view():

  1. создаёт экземпляр класса (например, PostListView()),
  2. вызывает метод dispatch() у этого экземпляра,
  3. возвращает функцию, которую Django использует как представление.

Последовательность работы CBV:

Пример:

path('', PostListView.as_view(), name='post_list')

1. Вызовется PostListView.as_view()

2. Вернётся функция, которую Django вызовет на HTTP-запрос

3. Эта функция создаст экземпляр PostListView

4. Вызовется dispatch(request, *args, **kwargs)

5. dispatch вызовет get() или post(), в зависимости от запроса.

Почему нельзя просто указать PostListView?

path('', PostListView, name='post_list') # ❌ Ошибка

Потому что PostListView — это класс, а не функция. Django не знает, как его вызывать. Он ожидает функцию с сигнатурой:

def view(request, *args, **kwargs):

Только as_view() создаёт такую обёртку.

Аналогия на пальцах:

Класс PostListView — это рецепт.
Метод as_view() — это
шеф-повар, который готовит по рецепту.
Django — это
гость, которому нужно готовое блюдо (HTTP-ответ).

Вы не можете накормить гостя рецептом — только готовым блюдом.

Вывод

  • .as_view() — это ключ к запуску классовых представлений в Django.
  • Он превращает описание поведения (класс) в конкретную исполняемую функцию, которую Django может вызвать.
  • Это делает CBV полностью совместимыми с URL-маршрутизатором Django, который работает с функциями.

Перепишем post_detail в PostDetailView на CBV

Так как у нас в функции много кастомной логики, чтобы без ошибок выполнять поиск по датам, нельзя использовать стандартный DetailView напрямую, потому что фильтрация по дате и slug требует кастомной логики.

1. В приложении blog откроем фаил blog/views.py.

Добавим строку импорта:

from django.views.generic import DetailView

Заменим функцию post_detail на класс PostDetailView:

-43

Объяснение:

model = Post - говорим Django, что работать нужно с моделью Post.

context_object_name = 'post' - в шаблон передаётся объект под именем post, а не object по умолчанию.

template_name = 'blog/post/detail.html' - задаём явный путь к шаблону, который нужно отрендерить.

get_queryset(self) - переопределяем стандартный QuerySet, чтобы возвращались только опубликованные посты, а не все. Это заменяет необходимость фильтрации Post.objects.filter(...) в каждом методе.

get_object(self, queryset=None) - именно здесь реализуется логика поиска поста по slug и дате:

  1. Получаем year, month, day, slug из self.kwargs.
  2. Создаём диапазон времени на указанный день: [start, end).
  3. Применяем фильтрацию по slug и диапазону дат.
  4. Возвращаем объект или 404.

Последовательность перехода от FBV к CBV

  1. Определили, что логика повторяется и можно использовать DetailView.
  2. Указали model, template_name, context_object_name.
  3. Переопределили get_queryset() для ограничения выборки опубликованными постами.
  4. Переопределили get_object() для дополнительной фильтрации по дате и slug.

Преимущества CBV в этом контексте

  • Повторное использование логики DetailView без явного рендера.
  • Удобство кастомизации (например, добавление логики в get_context_data).
  • Чистый и читаемый код.
  • Лёгкая интеграция с mixins, авторизацией и кешированием.

2. Внесём измнения в urls.py.

Добавим импорт:

from .views import PostDetailView

Перепишем адрес страницы детального описания поста на CBV:

path(
'<int:year>/<int:month>/<int:day>/<slug:post>/',
PostDetailView.as_view(),
name='post_detail'
),
-44

Перейдите по ссылке:

http://127.0.0.1:8000/blog/

Все хорошо, только вот шаблон пагинации не отображается. Нужно решить эту проблему.

Настройка пагинации для CBV

Проблема:

❌ Проблема:

  • переменная page явно передавалась как posts, но posts — это Page-объект, не Paginator.
  • шаблон не знал об этом явно, и это привело к ошибке.
  • при использовании CBV (как ListView) Django по умолчанию сам добавляет в шаблон переменные:
    page_obj — текущая страница
    paginator — объект пагинатора
    is_paginated — логический флаг
    object_list и твой context_object_name, например posts

posts — это список объектов (Page), а Django ListView уже автоматически передаёт объект пагинации в переменную page_obj. Это стандартный паттерн CBV.

Решение: при использовании CBV необходимо использовать page_obj вместо posts.

В шаблоне list.html — подключение шаблона пагинации pagination.html должно выглядеть так:

{% include "blog/pagination.html" %}

В pagination.html нужно вместо page. везде написать page_obj.

-45
-46

Что мы изменили:

page_obj — это объект Page, который ListView автоматически добавляет в шаблон.

Таким образом:

  • page_obj.number — текущий номер страницы
  • page_obj.has_next / .has_previous — логика навигации
  • page_obj.paginator.num_pages — общее число страниц

Почему пагинация теперь работает

  1. Отказались от передачи page=posts вручную.
  2. Стали использовать переменную page_obj, которая автоматически добавляется в шаблон при использовании ListView.
  3. Шаблон теперь точно знает, что вы обращаетесь к объекту страницы (Page), а не к списку постов или другой переменной.

Это делает код:

  • проще,
  • совместимым с Django best practices,
  • менее подверженным ошибкам.

Перейдите по ссылке:

http://127.0.0.1:8000/blog/

Результат:

-47

Все хорошо, теперь пагинация отображается и работает.

Проверим как CBV выполняет обработку ошибок ввода номера страницы пагинации: число больше числа страниц, набор символов вместо номера.

Перейдем по ссылкам:

http://127.0.0.1:8000/blog/?page=0
http://127.0.0.1:8000/blog/?page=NULL

Ошибки вновь вернулись:

  • EmptyPage
  • PageNotAnInteger
  • иногда даже 404 или пустая страница.

Наше приложение их не обрабатывает корректно.

Причина

ListView внутри использует Paginator, который выбрасывает исключения:

  • PageNotAnInteger — если передано page=abc
  • EmptyPage — если страница выходит за пределы диапазона (page=99999)

По умолчанию ListView никак их не обрабатывает, а просто отдаёт пустую страницу или выбрасывает ошибку.

Решение — переопределить метод paginate_queryset()

Нужно явно обернуть пагинацию в try-except, как это делается в Function-Based Views.

Вот готовое решение:

В приложении blog в файле blog/views.py добавьте строку импорта:

from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage

В класс PostListView необходимо добавить переопределенный метод paginate_queryset:

-48

Перейдем по ссылкам:

http://127.0.0.1:8000/blog/?page=0
http://127.0.0.1:8000/blog/?page=NULL

Теперь неверный ввод номеров страниц обрабатывается корректно, приложение blog работает без сбоев.

Оптимизация структуры шаблонов блога

Реализуем следующую структру шаблонов приложения blog:

-49

Создадим переиспользуемый шаблон _item.html карточки поста.

_item.html — карточка поста (переиспользуемый компонент)

В приложении blog создайте файл переиспользуемого шаблона templates/blog/post/_item.html:

-50

Отредактируйете шаблон списка постов list.html:

-51

Отредактируйете шаблон отдельного поста detail.html:

-52

Чтобы проверить работу приложения blog перейдите по ссылкам:

http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/?page=2

Протестируйте работу списка постов, пагинацию, прокликайте посты в списке. Если все хорошо идем дальше.