Django: публикация и граница видимости черновика
Оглавление курса · Скачать все состояния проекта.
Учебная задача: Выбирать только опубликованные заметки в списке и в отдельной карточке.
Входное состояние: Снимок lesson-09: все заметки видимы, обложка необязательна. Выходное состояние: Статусы draft/published, фильтр обоих публичных представлений и редакторская публикация. Эта глава продолжает «Редакционный стол» — небольшой сайт заметок. Нужны основы Python, HTML и устройство HTTP-запроса. Используем Python 3.12 и Django 5.2.18 LTS; номер ветки выбран для этой серии. Полный каталог lesson-10 в архиве содержит собственные настройки, шаблоны и историю миграций. При самостоятельном прохождении выберите один снимок и отдельную локальную базу; смешивание файлов разных глав меняет договор примера.
Исходники подготовлены редакционным чтением. Зависимости, миграции, сервер и команды работы с данными здесь не выполнялись. Все описанные ответы и изменения являются ожидаемыми, example_execution: not_run.
Публичный список пока показывает всё, что хранит таблица. Для редакционной работы это неудобно: текст часто появляется раньше согласованной публикации. Введём состояние заметки — черновик или опубликованная запись. Новые объекты по умолчанию станут черновиками, а выпуск останется действием редактора через административную панель. Цель главы — провести границу на уровне выборки данных, включая отдельную карточку. Скрытой ссылки в списке для такой границы недостаточно.
Статус описывает редакционное состояние, а не свойства пользователя. Одна учётная запись позднее сможет иметь несколько заметок на разных стадиях работы. Не записывайте «черновик» в роль автора и не создавайте отдельные таблицы ради двух состояний одного объекта. Поле позволяет сохранить идентичность заметки при публикации: текст и slug остаются теми же, меняется только правило, по которому объект включается в публичную выдачу.
Согласованные значения статуса
TextChoices связывает машинное значение draft с русским названием «Черновик», а published — с «Опубликовано». В базе сохраняются машинные значения. Подписи предназначены для человека и могут быть переведены без переименования всего набора строк. Код обращается к Note.Status.PUBLISHED, а не разбрасывает литерал по каждому файлу. Так значение и смысл состояния легче читать и менять согласованно.
Новое поле имеет default=Status.DRAFT. Это особенно существенно для существующих строк: при применении миграции они получат состояние черновика. Мы сознательно не объявляем всякую старую запись опубликованной. Редактор должен выбрать, какие тексты уже подходят для публичного списка. В fixture текущего снимка одна запись опубликована, другая оставлена черновиком. Это учебный набор ожидаемых случаев, а не результат выполненной миграции.
Параметр choices поддерживает формы и модельную проверку, но его не стоит путать со всеми возможными ограничениями базы. При прямом обходе обычных механизмов сохранения можно получить значение, не соответствующее редакционному договору, если отдельно не задано ограничение. Поэтому внешние операции должны проверять допустимые состояния. Пока редактор использует Admin, а в будущей авторской форме поле статуса вообще не будет доступно для изменения.
Фильтр списка и карточки
Представление списка добавляет filter(status=Note.Status.PUBLISHED) перед сортировкой. Неопубликованные объекты не передаются шаблону. Это лучше, чем получить все записи и затем скрывать неподходящие в HTML: лишние данные уже оказались бы на более позднем этапе, а дополнительные потребители могли бы забыть повторить фильтр. Здесь выборка сразу отражает назначение публичного экрана.
Отдельная карточка использует тот же опубликованный QuerySet внутри get_object_or_404. Поэтому прямой переход к slug черновика не обходит ограничение списка. Если просто оставить старое получение Note без условия, черновик исчезнет из оглавления, но останется доступен по угадываемому адресу. Система адресов из предыдущего урока не является механизмом секретности. Строка note-2 легко предсказуема; правильное решение — проверять видимость при выборе записи.
Для недоступного объекта ожидается 404. На публичном экране это не сообщает посетителю, хранится ли такой черновик или адрес вообще неизвестен. При этом внутри редакционного процесса сотрудник видит запись в Admin и может изменить статус. Разные представления выбирают разный допустимый набор. Важно не выводить публичному посетителю внутренние признаки черновика только ради более подробного сообщения об ошибке.
Наблюдаемая смена состояния
После самостоятельной загрузки текущих fixtures список показывает только «Расписание интервью». «Фотография мастерской» остаётся в базе, но не проходит публичный фильтр. Прямой запрос её карточки тоже ожидается как 404. Когда редактор изменит статус второй записи на published, она войдёт в список и станет доступна по прежнему slug. Временная дата и порядок продолжают определяться существующими полями; публикация не становится новой датой создания.
Если все записи остаются черновиками, публичная страница показывает пустое состояние, хотя административная панель содержит данные. Это нормальное различие, которое полезно объяснить редактору. Пустой список не всегда означает пустую таблицу. На экране мы показываем коллекцию, удовлетворяющую договору представления. В дальнейшем список «Мои заметки» будет выбирать другой набор и сможет включать черновики конкретного автора.
Для самостоятельного задания проследите одну заметку через три изменения: создать как черновик, опубликовать, вернуть в черновики. Предскажите доступность списка и карточки на каждом шаге. Отдельно ответьте, что произойдёт с URL уже загруженной публичной обложки: фильтр модели не управляет файловым сервером. Такой вопрос не усложняет текущую функцию, а уточняет предел её ответственности. Следующие главы добавят пользователей и возможность видеть собственные черновики без открытия всех чужих материалов.
Разбор связи файлов
Состояние публикации является значением модели, а не выводом из даты или наличия обложки. Заметка без изображения может быть опубликована, а полностью заполненный текст оставаться черновиком. Поэтому не подменяем редакционное решение внешними признаками. Выборка использует точное значение published, которое записано в принятом перечислении.
Проверка только списка оставила бы прямой адрес черновика открытым. Именно поэтому меняются и список, и карточка. Это две независимые точки чтения одного ресурса. Если позже появится поиск или экспорт, его выборка тоже должна получить соответствующее правило. Общая кнопка «скрыть» в интерфейсе не распространяется автоматически на будущие обработчики.
В фикстуре первая заметка опубликована, вторая остаётся черновиком. Так можно объяснить ожидаемое различие без удаления данных: обе строки сохраняются, но публичная страница использует ограниченный набор. Административная панель всё ещё видит редакционные записи по своим правам. Для самостоятельного разбора сравните отсутствие в публичном списке и отсутствие в самой базе. Публикация управляет доступом и состоянием материала, а не уничтожает его содержимое.
Полные файлы этого шага
Следующие файлы показаны полностью. Остальные согласованные файлы находятся в том же снимке архива; переносите весь каталог при выборе исходного состояния.
notes/models.py
from django.db import models
class Note(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Черновик"
PUBLISHED = "published", "Опубликовано"
status = models.CharField(max_length=10, choices=Status.choices, default=Status.DRAFT)
title = models.CharField(max_length=160)
slug = models.SlugField(max_length=180, unique=True)
body = models.TextField()
cover = models.ImageField(upload_to="covers/%Y/%m/", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at", "-pk"]
def __str__(self):
return self.title
notes/views.py
from django.shortcuts import get_object_or_404, render
from .models import Note
def note_list(request):
notes = Note.objects.filter(status=Note.Status.PUBLISHED).order_by("-created_at", "-pk")
return render(request, "notes/list.html", {"notes": notes})
def note_detail(request, slug):
note = get_object_or_404(Note.objects.filter(status=Note.Status.PUBLISHED), slug=slug)
return render(request, "notes/detail.html", {"note": note})
notes/migrations/0004_status.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("notes", "0003_cover")]
operations = [migrations.AddField(model_name="note",
name="status",
field=models.CharField(choices=[("draft",
"Черновик"),
("published",
"Опубликовано")],
default="draft",
max_length=10))]
Документация и исходники
Механизм этой главы описан в официальной документации Django 5.2. При самостоятельной проверке сначала сопоставьте маршрут, представление и состояние данных, затем сравните результат с указанным ожидаемым поведением. Архив учебного проекта содержит все шестнадцать самостоятельных этапов и договор эксплуатации.