Автор заметки и доступ к собственным черновикам
Оглавление курса · Скачать все состояния проекта.
Учебная задача: Связать заметку с пользователем и проверять владельца в серверной выборке.
Входное состояние: Вход, выход и публичный список опубликованных заметок. Выходное состояние: Связь author, личный список и карточка своего черновика. Эта глава продолжает «Редакционный стол» — небольшой сайт заметок. Нужны основы Python, HTML и устройство HTTP-запроса. Используем Python 3.12 и Django 5.2.18 LTS; номер ветки выбран для этой серии. Полный каталог lesson-13 в архиве содержит собственные настройки, шаблоны и историю миграций. При самостоятельном прохождении выберите один снимок и отдельную локальную базу; смешивание файлов разных глав меняет договор примера.
Исходники подготовлены редакционным чтением. Зависимости, миграции, сервер и команды работы с данными здесь не выполнялись. Все описанные ответы и изменения являются ожидаемыми, example_execution: not_run.
Вход отвечает на вопрос, кто прислал запрос. Редакции нужен следующий ответ: какие заметки этот человек вправе читать и менять. У зарегистрированного автора могут быть собственные черновики, а у другого автора — совсем другой набор. Добавим связь между Note и пользователем, затем выразим правила доступа непосредственно в серверных выборках. Условие в шаблоне поможет показать нужную ссылку, но решение о доступе останется у представления.
Наше правило чтения состоит из двух частей. Опубликованную заметку видит любой посетитель. Черновик видит только его автор. Личный список содержит все заметки текущего автора, независимо от статуса. Эти правила различаются: нельзя просто использовать одну выборку «всё опубликованное» во всех представлениях. Сначала назовите сценарий, а уже затем составьте фильтр для него.
Связь с пользовательской моделью
Поле author является внешним ключом на settings.AUTH_USER_MODEL. Так модель не зависит от конкретного класса User, который проект может заменить в будущем. На экземпляре заметки note.author даёт объект связанного пользователя, а author_id хранит его ключ. Не путайте эти представления: сравнить ключ иногда достаточно, а обращение к объекту может потребовать отдельного запроса. В нашем маленьком проекте важнее ясное правило, чем преждевременная оптимизация.
Для удаления автора выбрана политика PROTECT. Заметки принадлежат редакционному архиву; случайное удаление учётной записи не должно незаметно уничтожить опубликованный материал. Такой выбор означает, что операция удаления связанного пользователя будет остановлена, пока существуют заметки. Это продуктовая договорённость, а не обязательный способ описывать все связи. Если редакции понадобится обезличивание, для него потребуется отдельный процесс и другое правило хранения.
В предыдущих снимках заметки уже могли появиться через административную панель и фикстуру. Им нельзя задним числом назначить выдуманного автора с ключом 1: наличие такой учётной записи и её личность неизвестны. Поэтому миграция добавляет временно допускающую пустое значение связь. Старые материалы остаются без владельца, а администратор может осмысленно назначить реального автора. Новые заметки в последующей главе обязательно получат владельца из запроса.
Разрешённый NULL в схеме не равен разрешению клиенту выбирать любого автора. Это два разных уровня. Схема допускает исторические данные; представление создания контролирует новую запись. В форме мы вообще не покажем поле author. Когда старые записи будут распределены, отдельная миграция сможет сделать связь обязательной. В учебном архиве такого подтверждённого распределения нет, поэтому не заявляем, что переход уже завершён.
Две выборки с разными задачами
Представление my_notes защищено login_required. Анонимный запрос ожидаемо будет отправлен ко входу с адресом возвращения. Затем filter(author=request.user) ограничит результат владельцем. Декоратор сам по себе не добавляет этот фильтр: пользователь, успешно вошедший в систему, не получает автоматически все заметки. Именно сочетание известной личности и предметной выборки выражает правило личного списка.
Карточка доступна также анонимным посетителям, поэтому общего требования входа там нет. Для неё строим условие Q(status=published). Если пользователь вошёл, добавляем через логическое ИЛИ Q(author=request.user). В результате опубликованный материал доступен всем, а собственный черновик — автору. Затем get_object_or_404 ищет slug внутри разрешённой выборки. Чужой черновик и отсутствующий адрес дают одинаковое ожидаемое отсутствие карточки.
Обратите внимание на порядок действий. Мы не получаем любую заметку, чтобы сначала отрисовать часть данных, а затем решить, можно ли их показывать. Проверка входит в саму выборку до передачи объекта шаблону. Это упрощает чтение: объект, который дошёл до render, уже принадлежит разрешённому набору. В будущем этот же принцип перенесём в изменение и удаление, где одного разрешения чтения недостаточно.
Маршрут mine/ должен стоять перед маршрутом <slug:slug>/. Иначе слово mine может рассматриваться как slug карточки. Пространство имён notes сохраняется, поэтому шаблон обращается к notes:mine, а не повторяет текст адреса. Так изменение пути личного кабинета не заставит искать строку по всем шаблонам. Отдельно следите за зарезервированными slug: адреса mine и new не подходят для обычной заметки.
Разберите ожидаемый результат
Представьте две учётные записи, Анну и Павла. У Анны есть опубликованная заметка и черновик, у Павла — черновик, а импортированная заметка пока не имеет автора. Анонимный посетитель ожидаемо увидит только опубликованные материалы. Анна в личном списке увидит обе свои заметки. Её прямой запрос к черновику Павла не должен открыть карточку. Импортированный черновик остаётся недоступен обычным авторам, пока администратор не назначит владельца.
Этот пример полезно разобрать на бумаге как таблицу «пользователь — объект — действие». Для чтения опубликованной заметки правило будет одним; для её изменения другим. Не переносите исключение для публичного чтения в форму редактирования. Публикация сообщает, что содержимое можно видеть, но не делает каждого посетителя редактором. Следующий этап использует уже установленную личность, чтобы назначать автора при создании.
Добавление поля и наличие миграции не доказывают, что база обновлена. Если читатель пропустит миграцию, ожидаема ошибка обращения к отсутствующему столбцу. Если миграция выполнена, но старым заметкам не назначены авторы, пустой личный список может быть корректным результатом. Эти две ситуации требуют разных действий. Проверяйте состояние схемы и состояние данных отдельно, не исправляйте их случайным добавлением пользователя с подходящим числом в ключе.
В исходниках этой главы проверка доступа находится в представлениях. Административная панель остаётся инструментом доверенного персонала и использует собственные права Django. Мы не обещаем, что стандартное право change_note автоматически ограничит сотрудника его заметками внутри панели. Если администраторами должны быть обычные авторы, потребуется отдельное проектирование этого интерфейса; в текущем проекте они работают через личные страницы.
Полные файлы этого шага
Следующие файлы показаны полностью. Остальные согласованные файлы находятся в том же снимке архива; переносите весь каталог при выборе исходного состояния.
notes/models.py
from django.conf import settings
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)
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="notes")
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/migrations/0005_author.py
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [("notes", "0004_status"), migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [migrations.AddField(model_name="note",
name="author",
field=models.ForeignKey(blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="notes",
to=settings.AUTH_USER_MODEL))]
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):
from django.db.models import Q
visible = Q(status=Note.Status.PUBLISHED)
if request.user.is_authenticated:
visible |= Q(author=request.user)
note = get_object_or_404(Note.objects.filter(visible), slug=slug)
return render(request, "notes/detail.html", {"note": note})
from django.contrib.auth.decorators import login_required
@login_required
def my_notes(request):
notes = Note.objects.filter(author=request.user).order_by("-created_at", "-pk")
return render(request, "notes/list.html", {"notes": notes})
notes/urls.py
from django.urls import path
from . import views
app_name = "notes"
urlpatterns = [path("",
views.note_list,
name="list"),
path("mine/",
views.my_notes,
name="mine"),
path("<slug:slug>/",
views.note_detail,
name="detail")]
Документация и исходники
Механизм этой главы описан в официальной документации Django 5.2. При самостоятельной проверке сначала сопоставьте маршрут, представление и состояние данных, затем сравните результат с указанным ожидаемым поведением. Архив учебного проекта содержит все шестнадцать самостоятельных этапов и договор эксплуатации.