Перейти к содержанию

Автор заметки и доступ к собственным черновикам

Оглавление курса · Скачать все состояния проекта.

Учебная задача: Связать заметку с пользователем и проверять владельца в серверной выборке.

Входное состояние: Вход, выход и публичный список опубликованных заметок. Выходное состояние: Связь 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. При самостоятельной проверке сначала сопоставьте маршрут, представление и состояние данных, затем сравните результат с указанным ожидаемым поведением. Архив учебного проекта содержит все шестнадцать самостоятельных этапов и договор эксплуатации.