⚠️ Дисклеймер
Эта статья подготовлена по мотивам книги «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:
- Улучшает SEO: объединяет ссылочный вес дубликатов в один основной URL.
- Предотвращает пессимизацию: поисковики не считают контент копипастом.
- Улучшает индексацию: поисковики быстрее и правильнее индексируют сайт.
- Упрощает шаринг: пользователи делятся «чистым» URL.
- Обеспечивает консистентность: разработчики и редакторы всегда ссылаются на один и тот же URL.
⚠️ Ограничения канонических URL
Когда следует использовать канонические URL:
- Когда один и тот же пост можно открыть по разным путям (например, по id и по slug);
- Когда есть UTM-метки или параметры фильтрации в URL (?utm_source=...);
- Когда один и тот же контент публикуется в нескольких рубриках или разделах;
- При пагинации или бесконечном скролле — на всех подстраницах указывается канонический URL на первую страницу.
В Django:
Реализуется через метод get_absolute_url() в модели:
В HTML-шаблоне:
Мы будем использовать маршрут 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:
Функция 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 у нас прописан путь:
Если же мы используем slug или дату в маршруте — необходимо указать их в kwargs:
Вы можете узнать больше о вспомогательных функциях для работы с 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/
Результат:
Кликните в списке по любому посту и посмотрите на арес в браузере.
Адрес поста теперь имеет канонический вид:
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')
Добавив параметр 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
Результат:
Применим миграции:
python manage.py migrate
Результат:
Это приведёт модели приложения 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 будет иметь вид:
Закоментирванный код не пиши - я его оставил для наглядности: было, стало.
Маршрут для представления 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 следующим образом:
Что мы изменили:
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() следующим образом:
Почему необходимо использовать kwargs а не args?
args=[self.publish.year, self.publish.month, self.publish.day, self.slug]
Порядок аргументов в args обязан строго соответствовать маршруту. Если поменяешь порядок или забудешь что-то — будет NoReverseMatch.
При использовании kwargs неважен порядок, мы явно указываем, что чему соответствует. Это читаемее, устойчивее к ошибкам и легче поддерживается.
Резюме: когда использовать kwargs, а когда args?
Вывод:
Всегда используйте kwargs в reverse() на production-уровне — это безопаснее, выразительнее и легче в отладке.
Как находить все посты по дате и slug если время поста 00:00:00?
Наш код работает, но если вы укажете точное время создания поста 00:00:00 - пост не будет найден и такие посты если будут в системе нельзя будет посмотреть.
Чтобы исправить эту уязвимость в нашем приложении blog необходимо доработать код представления до следующего вида:
Проблема: не находились посты, если время публикации было 00:00:00.
До внесения измнений в код мы использовали фильр:
Почему это не работало?
Потому что 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 не совпадает — и пост не находится.
Как мы решили проблему
Что это значит на практике:
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, независимо от времени:
Почему это надёжно?
Потому что:
- мы явно работаем в UTC — не зависит от локальной зоны, настроек сервера или браузера;
- неважно, в каком часовом поясе был создан пост — он будет найден, если publish лежит в пределах суток по UTC.
Это называется: фильтрация по "UTC-диапазону"
Эта техника:
- используется в продакшн проектах с международной аудиторией;
- предотвращает "невидимые" баги, когда в одних часовых поясах пост виден, а в других — нет.
Резюме
Как мы решили проблему с отображением постов в разных часовых поясах:
- Убрали использование __year, __month, __day (ненадёжны при USE_TZ=True).
- Заменили их на publish__range=(start, end) в UTC.
- Гарантировали, что пост будет найден, даже если время публикации — 00:00:00, 23:59:59 и т.д.
Результат:
Кликаем по первой строке и получаем результат:
Адрес поста теперь SEO оптимизирован: отображается дата публикации поста и его slug.
http://127.0.0.1:8000/blog/2025/10/17/lermontov-gibel-i-pravda/
Проблема пропадания постов на границах часовых поясов решена.
Пагинация
Когда постов становится много необходимо отображать их в списке по 10 или по 5 штук за раз и предоставлять интерфейс навигации вперед, назад, в начало, в конец, чтобы пролистать весь список постов.
Пагинация (от англ. pagination) — это механизм разбиения большого списка данных на отдельные страницы. Вместо того чтобы выводить сразу все записи (например, все посты в блоге или все товары в магазине), пользователь видит только часть данных, а для просмотра остальных может перейти на следующую страницу.
Для чего нужна пагинация
- Повышение производительности
Загрузка сотен или тысяч записей за один запрос — это нагрузка на сервер, базу данных и браузер пользователя.
Пагинация ограничивает количество данных, которые нужно обработать и отрендерить за один раз. - Улучшение UX (удобства для пользователя)
Пользователю проще воспринимать данные, когда они порционно распределены по страницам.
Страницы позволяют быстро найти нужную информацию, без бесконечного скролла. - Оптимизация трафика
Если пользователь открыл только первую страницу, остальные данные даже не загружаются — это экономит трафик и снижает нагрузку. - Контроль за интерфейсом
Слишком длинные страницы выглядят перегруженными.
Пагинация позволяет строить чистую, аккуратную навигацию: «Следующая», «Предыдущая», номера страниц и т.п.
Почему пагинация обязательна при большом количестве записей
Без пагинации, если в таблице 10 000 записей, вы рискуете:
- получить время отклика сервера в десятки секунд,
- перегрузить память сервера и браузера,
- испортить впечатление от сайта у пользователя.
Что даёт пагинация в Django
Django предоставляет встроенные классы и утилиты, которые:
- автоматически делят QuerySet на страницы;
- обрабатывают параметры запроса ?page=2;
- позволяют легко отобразить ссылки на страницы в шаблоне.
Применение пагинации — это не просто хорошая практика, а необходимость в любом приложении, где есть длинные списки данных: блоги, каталоги, форумы, таблицы администратора и т.д.
Пагинация в представлении списка постов post list view
В приложении blog отредактируйте файл views.py. Импортируйте класс Paginator из Django и измените представление post_list следующим образом:
Давайте разберём, что делает новый код, который мы добавили в представление:
- Мы создаём экземпляр класса Paginator, указывая количество объектов, которые нужно выводить на одной странице. В нашем случае — по три поста на страницу.
- Мы получаем GET-параметр page из HTTP-запроса и сохраняем его в переменную page_number. Этот параметр содержит номер страницы, которую запрашивает пользователь.
Если параметр page отсутствует, то по умолчанию используется значение 1, чтобы загрузить первую страницу. - Мы получаем объекты для нужной страницы, вызывая метод page() у экземпляра Paginator. Этот метод возвращает объект Page, который мы сохраняем в переменную posts.
- Мы передаём объект posts в шаблон, чтобы отобразить соответствующую страницу с постами.
Добавление пагинатора в шаблон list.html
Создадим шаблон для отображения ссылок пагинации и сделаем его универсальным, чтобы его можно было повторно использовать для любой пагинации на сайте.
В каталоге templates/blog создайте новый файл с именем pagination.html.
Добавьтеn в этот файл следующий HTML-код:
Что здесь происходит:
- 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 %}, следующим образом:
Тег шаблона {% 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:
Запустите тестовый сервер командой:
python manage.py runserver
В браузере перейдите на главную странцу блога по ссылке:
http://127.0.0.1:8000/blog/
Результат:
Стили и адаптивность для пагинации на 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. Замените код шаблона пагинации на следующий код:
Много кода получилось, поэтому рекомендую зайти на страницу проекта в GitHub и скопировать код целиком.
Запустите тестовый сервер:
python manage.py runserver
Перейдите на главнуюстраницу блога по ссылке:
http://127.0.0.1:8000/blog/
Результат:
Теперь пагинация выглядит презентабельно и соответ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
Введите этот адрес в адресную строку браузера.
Результат:
Мы получили ошибку "EmptyPage at /blog/ Номер страницы меньше 1".
Теперь давайте отредактируем адрес страницы пагинации еще раз и вместо 0 напишем null.
http://127.0.0.1:8000/blog/?page=null
Введите этот адрес в адресную строку браузера.
Результат:
Мы получили ошибку "PageNotAnInteger at /blog/ Номер страницы не является натуральным числом".
Чтобы устранить ошибки в работе нашего приложения blog, необходимо с помощью конструкции try except обработать все нестандартные ситуации.
В приложении blog откройте файл blog/views.py и добавьте в него следующие строки:
В импорт мы добавили строку:
from django.core.paginator import EmptyPage
В функцию def post_list(request) мы добавили обработчик:
Теперь введите в браузере адрес с неправилным номером страницы:
http://127.0.0.1:8000/blog/?page=0
Результат:
Как видно на скриншоте выше ошибки больше нет - показывается последняя страница пагинации.
В приложении blog снова откройте файл blog/views.py и добавьте в него следующие строки:
В импорт мы добавили строку:
from django.core.paginator import PageNotAnInteger
В функции def post_list(request) мы доработали обработчик - добавили except PageNotAnInteger:
Теперь введите в браузере адрес с null вместо номера страницы:
http://127.0.0.1:8000/blog/?page=null
Результат:
Как видно на скриншоте выше ошибки больше нет - показывается первая страница пагинации.
Что мы сделали:
- Обернули вызов 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 нужно использовать
- Разделение ответственности
Каждому HTTP-методу — свой метод класса: get(), post(), put(), вместо if request.method == 'POST'. - Масштабируемость
В больших проектах проще управлять логикой через наследование классов, чем через вложенные условия в функциях. - Многоразовость (DRY)
Общая логика (например, LoginRequiredMixin, PermissionRequiredMixin, PaginationMixin) легко подключается через миксины. - Быстрая разработка через generic views
Django предоставляет мощные классы: ListView, DetailView, CreateView, UpdateView, DeleteView, — которые покрывают 90% CRUD-задач без ручного кода.
Преимущества CBV
⚠️ Ограничения и недостатки CBV
Когда использовать 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:
Объяснение:
2. Внесём измнения в urls.py. Добавим импорт:
from .views import PostListView
Перепишем адрес страницы списка постов на CBV:
path('', PostListView.as_view(), name='post_list'),
Объяснение:
- path('', ...) — определяет маршрут http://.../, по которому будет доступно представление.
- PostListView.as_view() — превращает класс-представление (CBV) в обычную функцию, которую Django может вызвать при обработке HTTP-запроса. Это делается методом as_view(), встроенным в View.
- 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():
- создаёт экземпляр класса (например, PostListView()),
- вызывает метод dispatch() у этого экземпляра,
- возвращает функцию, которую 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:
Объяснение:
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 и дате:
- Получаем year, month, day, slug из self.kwargs.
- Создаём диапазон времени на указанный день: [start, end).
- Применяем фильтрацию по slug и диапазону дат.
- Возвращаем объект или 404.
Последовательность перехода от FBV к CBV
- Определили, что логика повторяется и можно использовать DetailView.
- Указали model, template_name, context_object_name.
- Переопределили get_queryset() для ограничения выборки опубликованными постами.
- Переопределили 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'
),
Перейдите по ссылке:
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.
Что мы изменили:
page_obj — это объект Page, который ListView автоматически добавляет в шаблон.
Таким образом:
- page_obj.number — текущий номер страницы
- page_obj.has_next / .has_previous — логика навигации
- page_obj.paginator.num_pages — общее число страниц
Почему пагинация теперь работает
- Отказались от передачи page=posts вручную.
- Стали использовать переменную page_obj, которая автоматически добавляется в шаблон при использовании ListView.
- Шаблон теперь точно знает, что вы обращаетесь к объекту страницы (Page), а не к списку постов или другой переменной.
Это делает код:
- проще,
- совместимым с Django best practices,
- менее подверженным ошибкам.
Перейдите по ссылке:
http://127.0.0.1:8000/blog/
Результат:
Все хорошо, теперь пагинация отображается и работает.
Проверим как 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:
Перейдем по ссылкам:
http://127.0.0.1:8000/blog/?page=0
http://127.0.0.1:8000/blog/?page=NULL
Теперь неверный ввод номеров страниц обрабатывается корректно, приложение blog работает без сбоев.
Оптимизация структуры шаблонов блога
Реализуем следующую структру шаблонов приложения blog:
Создадим переиспользуемый шаблон _item.html карточки поста.
_item.html — карточка поста (переиспользуемый компонент)
В приложении blog создайте файл переиспользуемого шаблона templates/blog/post/_item.html:
Отредактируйете шаблон списка постов list.html:
Отредактируйете шаблон отдельного поста detail.html:
Чтобы проверить работу приложения blog перейдите по ссылкам:
http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/?page=2
Протестируйте работу списка постов, пагинацию, прокликайте посты в списке. Если все хорошо идем дальше.