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

Django: обложка заметки и пользовательские файлы

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

Учебная задача: Добавить необязательную обложку и различить статические ресурсы и media.

Входное состояние: Снимок lesson-08: карточка заметки с текстом и датой. Выходное состояние: ImageField cover, MEDIA_ROOT и MEDIA_URL, условное изображение на карточке. Эта глава продолжает «Редакционный стол» — небольшой сайт заметок. Нужны основы Python, HTML и устройство HTTP-запроса. Используем Python 3.12 и Django 5.2.18 LTS; номер ветки выбран для этой серии. Полный каталог lesson-09 в архиве содержит собственные настройки, шаблоны и историю миграций. При самостоятельном прохождении выберите один снимок и отдельную локальную базу; смешивание файлов разных глав меняет договор примера.

Исходники подготовлены редакционным чтением. Зависимости, миграции, сервер и команды работы с данными здесь не выполнялись. Все описанные ответы и изменения являются ожидаемыми, example_execution: not_run.

У карточки заметки появится обложка. Она отличается от CSS и логотипа оформления: файл выбирает редактор, а приложение сохраняет ссылку на него вместе с записью. Такие файлы обычно называют media. Статические ресурсы принадлежат исходному оформлению проекта; media появляются в процессе его использования. Если смешать эти группы, сборка оформления может случайно затронуть пользовательские материалы, а резервная копия кода не будет содержать всё, что нужно для восстановления сайта.

Добавим cover как необязательное ImageField. Для обработки изображения Django использует Pillow; в requirements указан диапазон зависимости, который ещё не разрешался установкой. Наличие поля в модели не означает, что файл уже загружен. Исходные fixtures остаются без обложек. Это позволяет проверить важный нормальный случай: заметка без изображения должна иметь рабочую карточку, а не ошибку обращения к отсутствующему адресу.

Значение поля и место хранения

В базе поле хранит имя файла относительно используемого хранилища. Само содержимое изображения не становится автоматически байтами строки модели. upload_to="covers/%Y/%m/" задаёт организацию пути внутри хранилища. MEDIA_ROOT в нашем примере указывает локальный каталог, а MEDIA_URL — публичный префикс обращения. Эти настройки не являются взаимозаменяемыми: одна описывает файловую систему, другая адрес для клиента.

Разделение по году и месяцу удобно для небольшого редакционного архива, однако не является механизмом прав. Зная адрес media, посетитель может получить файл независимо от доступности страницы, если сервер отдаёт его публично. Поэтому обложку конфиденциального черновика нельзя считать защищённой только благодаря фильтру заметок. Для закрытых файлов потребуется отдельное хранилище и проверяемая выдача. В этом курсе обложки рассматриваются как публичные редакционные материалы.

Сохранение записи и сохранение файла имеют разные последствия. Удаление заметки из базы не обязательно автоматически удалит все ранее связанные файлы. Это разумно учитывать при обслуживании: один и тот же ресурс может иметь другие ссылки, а удаление файла требует собственной политики. Наш пример не вводит автоматическую очистку media. Мы фиксируем границу, чтобы читатель не ожидал, что команда удаления строки сама решает весь жизненный цикл файлов.

Отображение необязательной обложки

Шаблон проверяет {% if note.cover %} перед чтением note.cover.url. Проверка отвечает на вопрос, есть ли значение поля. Без неё обращение к URL пустого файла может закончиться ошибкой. Не нужно подменять отсутствие обложки пустым атрибутом src: браузер может интерпретировать его нежелательным образом. Если изображение не задано, просто не создаём элемент img, а основной текст остаётся на своём месте.

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

CSS из базовой главы ограничивает изображение шириной контейнера и сохраняет пропорции. Это влияет только на отображение. Если редактор загрузит огромный файл, уменьшенная картинка на экране всё равно может требовать большой передачи данных. Оптимизация размера файла и создание вариантов — отдельная работа с ресурсом. Наше последующее ограничение формы в две МиБ задаст верхнюю границу загрузки, но не станет полноценным конвейером обработки изображений.

Разработка и выпуск

В главном URLconf подключаем static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) только при DEBUG. Это удобный помощник для локальной разработки, а не способ обслуживать пользовательские файлы производственного сайта. Когда отладка выключена, media должен отдавать настроенный сервер или хранилище. Исчезнувшая обложка после выпуска может означать именно отсутствие этой внешней настройки, а не потерю записи в базе.

Доверие к загрузкам тоже требует отдельного решения. ImageField в форме проверяет содержимое как изображение, однако расширение файла и название не стоит считать достаточным доказательством безопасности. Для открытой регистрации понадобятся ограничения, изоляция происхождения media и политика проверки. Здесь загружают доверенные авторы учебной редакции. Не используйте пример как готовую публичную систему приёма произвольных пользовательских файлов.

Ожидаемый результат для заметки без обложки совпадает с прежней карточкой. После самостоятельной загрузки через Admin изображение появляется перед текстом и имеет адрес из media. Если база указывает файл, которого нет в хранилище, наличие значения поля не восстановит потерянные байты: сервер может ответить 404 для картинки. Сравните эти три случая. В следующей главе разделим опубликованные заметки и черновики, сохранив это различие между видимостью страницы и выдачей её ресурса.

Разбор связи файлов

Сохранённое имя обложки обычно является относительным путём внутри хранилища. Свойство url преобразует его в адрес для браузера. Использовать путь файловой системы в src изображения нельзя: компьютер посетителя не читает каталог сервера по локальному пути. Не подставляйте в шаблон MEDIA_ROOT, где речь идёт о публичном адресе.

Условие наличия обложки перед обращением к её URL необходимо для необязательного поля. Оно подтверждает, что в модели указано имя файла, но ещё не доказывает фактическое наличие самого файла в хранилище. Если файл потерян, браузер ожидаемо не сможет получить изображение даже при корректной ссылке. Восстановление базы без медиа может привести именно к такому состоянию.

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

Полные файлы этого шага

Следующие файлы показаны полностью. Остальные согласованные файлы находятся в том же снимке архива; переносите весь каталог при выборе исходного состояния.

notes/migrations/0003_cover.py

from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [("notes", "0002_slug")]
    operations = [migrations.AddField(model_name="note",
        name="cover",
        field=models.ImageField(blank=True,
        upload_to="covers/%Y/%m/"))]

notes/templates/notes/detail.html

{% extends "base.html" %}
{% block title %}{{ note.title }} · Редакционный стол{% endblock %}
{% block content %}
<article>
<h1>{{ note.title }}</h1>
{% if note.cover %}
<img src="{{ note.cover.url }}" alt="Обложка: {{ note.title }}">
{% endif %}
<p>{{ note.body|linebreaksbr }}</p>
<time datetime="{{ note.created_at|date:'c' }}">{{ note.created_at|date:'d.m.Y' }}</time>
</article>
{% endblock %}

desk/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [path("admin/", admin.site.urls), path("notes/", include("notes.urls"))]

from django.conf import settings
from django.conf.urls.static import static
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

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

Механизм этой главы описан в официальной документации Django 5.2. При самостоятельной проверке сначала сопоставьте маршрут, представление и состояние данных, затем сравните результат с указанным ожидаемым поведением. Архив учебного проекта содержит все шестнадцать самостоятельных этапов и договор эксплуатации.