Загрузка данных


Перейти к содержимому















Python8. Урок 6. Хранение файлов, транзакции, админка.  
На предыдущей встрече мы погрузились в инфраструктурные аспекты Django, изучив middleware — промежуточные слои обработки запросов и ответов, которые позволяют внедрять сквозную функциональность в наше приложение. Мы научились создавать собственные middleware для решения задач аутентификации, логирования и других целей. Также мы освоили различные стратегии кеширования, которые помогают значительно повысить производительность веб-приложений за счет сохранения результатов вычислений и запросов к базе данных. Особое внимание было уделено Class-Based Views — объектно-ориентированному подходу к написанию представлений, который позволяет переиспользовать код через наследование и миксины, делая наши приложения более структурированными и поддерживаемыми.

Сегодня мы продолжим изучение Django, обратив внимание на практические аспекты работы с пользовательским контентом и управления данными. Современные веб-приложения редко обходятся без возможности загрузки файлов — будь то аватары пользователей, документы, изображения товаров или любые другие медиафайлы. Правильная организация хранения и обработки файлов является критически важной задачей, которая влияет как на удобство разработки, так и на производительность приложения в продакшене. Кроме того, мы рассмотрим механизмы транзакций в Django, которые обеспечивают целостность данных при выполнении связанных операций с базой данных. Завершим встречу изучением Django Admin — мощного инструмента для управления контентом, который можно гибко настраивать под требования конкретного проекта.

MEDIA_ROOT и MEDIA_URL: основы хранения пользовательских файлов
Прежде чем начать работу с файлами в Django, необходимо понять принципиальную разницу между статическими файлами приложения и пользовательским контентом. Статические файлы — это CSS, JavaScript, изображения интерфейса — то есть файлы, которые создаются в процессе разработки и не изменяются пользователями. Медиафайлы же представляют собой динамический контент, загружаемый пользователями в процессе работы с приложением. Django четко разделяет эти два типа файлов, используя разные настройки и подходы к их обработке.

Настройка MEDIA_ROOT определяет абсолютный путь к директории в файловой системе сервера, где будут храниться загруженные пользователями файлы. Это физическое расположение файлов на диске, куда Django будет сохранять все загруженные данные. Обычно эта настройка выглядит следующим образом: MEDIA_ROOT = os.path.join(BASE_DIR, ‘media’). Такая конфигурация создает папку media в корневой директории проекта, где будут размещаться все пользовательские файлы. Важно понимать, что пользователи вашего приложения не имеют прямого доступа к файловой системе сервера, поэтому необходим механизм, который позволит отдавать эти файлы через веб-интерфейс.

# settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Путь к директории для медиафайлов
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# URL-префикс для доступа к медиафайлам
MEDIA_URL = '/media/'
Настройка settings.py

Именно для этого существует настройка MEDIA_URL, которая определяет URL-префикс, по которому медиафайлы будут доступны в браузере. Типичное значение этой настройки — /media/. Когда пользователь загружает, например, аватар с именем avatar.jpg, Django сохраняет его в директорию, указанную в MEDIA_ROOT, а в базе данных сохраняется относительный путь к файлу. При отображении этого изображения на странице Django формирует полный URL, комбинируя MEDIA_URL с относительным путем, получая что-то вроде /media/avatars/avatar.jpg.

# urls.py (корневой файл проекта)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
]

# Добавляем раздачу медиафайлов только в режиме разработки
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Настройка URLs для разработки

Однако настройки MEDIA_ROOT и MEDIA_URL сами по себе не заставят Django автоматически раздавать файлы. В режиме разработки необходимо явно добавить обработку медиафайлов в URL-конфигурацию проекта. Это делается путем добавления специального URL-паттерна, который использует встроенную функцию Django для обслуживания медиафайлов. В файле urls.py корневого приложения добавляется конструкция, которая проверяет, находится ли приложение в режиме отладки (DEBUG = True), и если да, то регистрирует дополнительный маршрут для раздачи медиафайлов из директории MEDIA_ROOT. Важно помнить, что этот механизм предназначен исключительно для разработки — в продакшене статические и медиафайлы должны обслуживаться веб-сервером типа Nginx или специализированным сервисом.

FileField и ImageField: работа с файлами на уровне моделей
Django предоставляет специальные типы полей для работы с файлами в моделях базы данных. FileField — это универсальное поле для хранения любых типов файлов, будь то документы, архивы, аудио или видео. ImageField является специализированной версией FileField, предназначенной конкретно для работы с изображениями и включающей дополнительную валидацию для проверки, что загруженный файл действительно является корректным изображением. При использовании ImageField в проекте необходимо установить библиотеку Pillow, которая обеспечивает функциональность для обработки изображений.

Когда вы добавляете FileField или ImageField в модель, Django не хранит сам файл в базе данных — это было бы крайне неэффективно с точки зрения производительности и размера базы данных. Вместо этого Django сохраняет только относительный путь к файлу в виде строки. Сам же файл размещается в файловой системе, в директории, указанной в MEDIA_ROOT. При определении файлового поля в модели можно указать несколько параметров, которые влияют на поведение загрузки и хранения файлов. Параметр upload_to определяет поддиректорию внутри MEDIA_ROOT, куда будут сохраняться файлы. Это может быть как простая строка, так и вызываемая функция, которая динамически генерирует путь на основе данных экземпляра модели или текущей даты.

from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    # Простое использование ImageField
    avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
    # FileField для документов
    resume = models.FileField(upload_to='resumes/', blank=True, null=True)

    def __str__(self):
        return f"Профиль {self.user.username}"
Базовое использование FileField и ImageField

Использование динамических путей через функции является распространенной практикой для организации файлов в логическую структуру. Например, можно создать функцию, которая формирует путь вида user_{id}/avatars/{filename}, где id — это идентификатор пользователя. Такая организация позволяет избежать конфликтов имен файлов и упрощает навигацию по файловой системе при необходимости ручного вмешательства. При работе с файловыми полями в коде можно обращаться к различным атрибутам и методам. Атрибут .url возвращает полный URL для доступа к файлу из браузера, комбинируя MEDIA_URL с относительным путем файла. Атрибут .path возвращает абсолютный путь к файлу в файловой системе. Метод .save() позволяет программно сохранять новые файлы, а метод .delete() удаляет файл как из базы данных, так и из файловой системы.

# models.py
from django.db import models
from django.contrib.auth.models import User
import os
from datetime import datetime

def user_avatar_path(instance, filename):
    """
    Генерирует путь для аватара: user_{id}/avatars/{filename}
    """
    ext = filename.split('.')[-1]
    filename = f"avatar_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"
    return os.path.join('users', f'user_{instance.user.id}', 'avatars', filename)

def user_document_path(instance, filename):
    """
    Организует документы по датам: documents/2025/01/filename
    """
    today = datetime.now()
    return os.path.join(
        'documents',
        str(today.year),
        str(today.month).zfill(2),
        filename
    )

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to=user_avatar_path, blank=True, null=True)
    document = models.FileField(upload_to=user_document_path, blank=True, null=True)
Динамические пути с функцией upload_to

Важной особенностью работы с файловыми полями является то, что Django автоматически генерирует уникальные имена файлов, если файл с таким именем уже существует в целевой директории. Это предотвращает перезапись существующих файлов, но может привести к накоплению «мусорных» файлов, если пользователь многократно загружает новые версии. Поэтому часто требуется реализовывать логику удаления старых файлов при загрузке новых, что можно сделать с помощью сигналов Django или переопределения методов модели.

Обработка файлов и библиотека Pillow
После того как пользователь загрузил файл, часто требуется выполнить с ним какие-то манипуляции перед сохранением или для создания дополнительных версий. Для изображений типичными задачами являются изменение размера, обрезка, наложение водяных знаков, конвертация форматов или оптимизация размера файла. Библиотека Pillow предоставляет богатый функционал для работы с изображениями в Python и является фактическим стандартом в Django-проектах.

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

При обработке изображений в Django удобно делать это в момент загрузки файла, переопределяя метод save() модели. В этом методе можно открыть загруженное изображение с помощью Pillow, применить необходимые трансформации и сохранить как исходный файл, так и созданные миниатюры. Важно помнить о соотношении сторон при изменении размера изображений — простое масштабирование может исказить пропорции, поэтому часто используются методы, которые либо обрезают изображение до нужного соотношения сторон, либо добавляют поля вокруг изображения.

# models.py
from django.db import models
from PIL import Image
import os

class Product(models.Model):
    name = models.CharField(max_length=200)
    image = models.ImageField(upload_to='products/')
    thumbnail = models.ImageField(upload_to='products/thumbnails/', blank=True, null=True)

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        if self.image:
            self.create_thumbnail()

    def create_thumbnail(self, size=(300, 300)):
        """Создает миниатюру изображения"""
        # Открываем изображение
        img = Image.open(self.image.path)

        # Создаем миниатюру с сохранением пропорций
        img.thumbnail(size, Image.Resampling.LANCZOS)

        # Формируем путь для миниатюры
        thumb_name = f"thumb_{os.path.basename(self.image.name)}"
        thumb_path = os.path.join(
            os.path.dirname(self.image.path),
            'thumbnails',
            thumb_name
        )

        # Создаем директорию, если её нет
        os.makedirs(os.path.dirname(thumb_path), exist_ok=True)

        # Сохраняем миниатюру
        img.save(thumb_path, quality=85, optimize=True)

        # Обновляем поле thumbnail
        self.thumbnail = os.path.join(
            os.path.dirname(self.image.name),
            'thumbnails',
            thumb_name
        )
        # Сохраняем без вызова save() снова, чтобы избежать рекурсии
        Product.objects.filter(pk=self.pk).update(thumbnail=self.thumbnail)
Создание миниатюр изображений

Другой распространенный сценарий — валидация загружаемых изображений. Хотя ImageField автоматически проверяет, что файл является изображением, иногда требуются дополнительные проверки: ограничение размера файла, минимальные или максимальные размеры изображения в пикселях, допустимые форматы. Эти проверки можно реализовать с помощью кастомных валидаторов Django или в методе clean модели. Pillow позволяет легко получить размеры изображения и формат, что делает такие проверки простыми в реализации.

# validators.py
from django.core.exceptions import ValidationError
from PIL import Image

def validate_image_size(image):
    """Проверяет размер файла (максимум 5MB)"""
    max_size = 5 * 1024 * 1024  # 5MB в байтах
    if image.size > max_size:
        raise ValidationError(f'Размер файла не должен превышать 5MB. Текущий размер: {image.size / 1024 / 1024:.2f}MB')

def validate_image_dimensions(image):
    """Проверяет минимальные размеры изображения"""
    img = Image.open(image)
    width, height = img.size
    min_width = 800
    min_height = 600

    if width < min_width or height < min_height:
        raise ValidationError(
            f'Минимальные размеры изображения: {min_width}x{min_height}px. '
            f'Загруженное изображение: {width}x{height}px'
        )

def validate_image_format(image):
    """Проверяет формат изображения"""
    allowed_formats = ['JPEG', 'PNG', 'WEBP']
    img = Image.open(image)

    if img.format not in allowed_formats:
        raise ValidationError(
            f'Неподдерживаемый формат изображения: {img.format}. '
            f'Разрешенные форматы: {", ".join(allowed_formats)}'
        )

# Использование в модели
class Photo(models.Model):
    title = models.CharField(max_length=200)
    image = models.ImageField(
        upload_to='photos/',
        validators=[
            validate_image_size,
            validate_image_dimensions,
            validate_image_format
        ]
    )
Валидация изображений

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

Хранение файлов в облачных сервисах
По мере роста приложения локальное хранение файлов на сервере приложений становится проблематичным по нескольким причинам. Во-первых, это усложняет масштабирование — при использовании нескольких серверов приложений необходимо обеспечить синхронизацию файлов между ними. Во-вторых, хранение больших объемов файлов на дисках серверов приложений увеличивает стоимость инфраструктуры и усложняет резервное копирование. В-третьих, отдача файлов напрямую с серверов приложений создает дополнительную нагрузку, которую лучше переложить на специализированные сервисы.

Облачные хранилища, такие как Amazon S3, Google Cloud Storage, Microsoft Azure Storage или их аналоги, решают эти проблемы. Они предоставляют надежное, масштабируемое и относительно недорогое хранение файлов с высокой доступностью. Django поддерживает различные бэкенды хранения файлов через систему storage backends, что позволяет абстрагироваться от конкретного способа хранения файлов. Переключение между локальным хранением и облачным в идеале не должно требовать изменений в коде приложения — достаточно изменить настройки.

Для интеграции с популярными облачными хранилищами существуют готовые библиотеки. Например, django-storages предоставляет реализации storage backends для Amazon S3, Google Cloud Storage и других популярных сервисов. После установки библиотеки и настройки параметров доступа к облачному хранилищу Django будет автоматически загружать все новые файлы в облако, а не на локальный диск. При этом интерфейс работы с файлами в коде остается прежним — вы все так же используете FileField и ImageField, обращаетесь к атрибуту .url для получения ссылки на файл, но фактически файл будет находиться в облачном хранилище.

Использование облачных хранилищ также дает дополнительные преимущества. Большинство таких сервисов предлагают встроенные CDN (Content Delivery Networks), которые обеспечивают быструю доставку файлов пользователям по всему миру. Облачные хранилища предоставляют инструменты для автоматического резервного копирования, версионирования файлов и управления жизненным циклом данных. Они также позволяют настраивать права доступа к файлам на гранулярном уровне, генерировать временные подписанные URL для приватных файлов и многое другое.

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

Транзакции в Django: atomic() и select_for_update()
При работе с базами данных одной из ключевых проблем является обеспечение целостности данных при выполнении множественных связанных операций. Представьте ситуацию: пользователь совершает покупку в интернет-магазине, и приложению необходимо создать заказ, уменьшить количество товара на складе, списать деньги с баланса пользователя и создать запись о транзакции. Если одна из этих операций завершится ошибкой после того, как другие уже выполнены, база данных окажется в противоречивом состоянии. Транзакции решают эту проблему, гарантируя, что либо все операции в группе выполнятся успешно, либо ни одна из них не применится к базе данных.

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

Использовать atomic() можно двумя способами. В качестве декоратора функции или метода представления он оборачивает весь код функции в транзакцию. Если в процессе выполнения функции происходит исключение, все изменения базы данных автоматически откатываются. Если функция завершается успешно, все изменения фиксируются одной атомарной операцией. Такой подход удобен для представлений, которые должны выполнять множественные операции с базой данных. В качестве контекстного менеджера atomic() предоставляет более гибкий контроль, позволяя оборачивать в транзакцию только определенные блоки кода внутри функции. Это полезно, когда в функции есть операции, которые не должны быть частью транзакции, или когда необходимо создать несколько независимых транзакций внутри одной функции.

Важно понимать, что транзакции могут быть вложенными. Django поддерживает savepoints — точки сохранения внутри транзакции, к которым можно откатиться, не отменяя всю транзакцию целиком. При использовании вложенных блоков atomic() внутренние блоки создают savepoints, что позволяет откатить часть операций, сохранив изменения из внешних блоков. Это мощный инструмент для реализации сложной логики обработки ошибок.

Однако транзакции сами по себе не решают всех проблем параллельного доступа к данным. Представьте ситуацию: два пользователя одновременно пытаются купить последний товар на складе. Оба читают количество товара (допустим, осталась одна единица), оба видят, что товар доступен, и оба пытаются создать заказ и уменьшить количество на единицу. В результате может получиться, что количество товара стало отрицательным, или оба заказа были успешно созданы для товара, которого было всего один. Для решения таких проблем существует механизм блокировок.

Метод select_for_update() в Django ORM реализует пессимистическую блокировку. Когда вы выполняете запрос с select_for_update(), база данных блокирует выбранные строки до окончания текущей транзакции, предотвращая их изменение другими параллельными транзакциями. Это гарантирует, что пока один процесс работает с данными, другие процессы должны ждать завершения первой транзакции, прежде чем получат доступ к этим же строкам. Использование select_for_update() критически важно для операций, где несколько процессов могут одновременно изменять одни и те же данные.

Типичный паттерн использования выглядит так: начинается транзакция с помощью atomic(), затем выполняется запрос с select_for_update() для получения и блокировки необходимых объектов, после чего выполняются проверки и модификации заблокированных данных. Если все проверки пройдены успешно, изменения сохраняются и транзакция фиксируется, автоматически снимая блокировки. Если что-то идет не так, происходит откат транзакции. Важно отметить, что select_for_update() должен использоваться внутри транзакции — попытка использовать его вне atomic() приведет к ошибке.

Создание кастомной админки Django
Django Admin — это одна из самых известных и любимых функций фреймворка, предоставляющая готовый интерфейс для управления данными приложения. Из коробки Django предоставляет базовый функционал для просмотра, добавления, редактирования и удаления записей моделей. Однако стандартный интерфейс часто требует доработки для соответствия специфическим требованиям проекта. Кастомизация админки позволяет создать удобный и эффективный инструмент для контент-менеджеров и администраторов системы.

Регистрация модели в админке начинается с создания класса, наследующего от admin.ModelAdmin, и декоратора @admin.register(). Этот класс является конфигурацией админки для конкретной модели и определяет, как будут отображаться и редактироваться данные. Базовая кастомизация включает настройку отображаемых полей. Атрибут list_display определяет, какие поля будут показаны в табличном представлении списка объектов. Это могут быть как простые поля модели, так и методы класса админки, которые вычисляют отображаемые значения на основе данных объекта.

Возможность добавлять вычисляемые поля в list_display чрезвычайно полезна. Например, для модели пользователя можно добавить метод, который отображает полное имя, составленное из имени и фамилии, или для модели товара — метод, показывающий наличие на складе цветовым индикатором. Эти методы принимают экземпляр модели в качестве параметра и возвращают строку или HTML-код для отображения. Чтобы Django правильно обрабатывал HTML в таких методах, необходимо установить атрибут метода, указывающий, что возвращаемое значение должно быть отрендерено как HTML, а не экранировано.

Атрибут list_filter добавляет боковую панель с фильтрами, позволяющими быстро отфильтровать список объектов по определенным критериям. Это могут быть простые поля модели, даты, булевы значения или связанные модели. Django автоматически создает подходящий интерфейс фильтрации в зависимости от типа поля. Для более сложных случаев можно создавать кастомные фильтры, наследуясь от admin.SimpleListFilter и определяя логику фильтрации и отображения опций.

Атрибут search_fields включает поисковую строку, позволяющую искать объекты по указанным полям. Django автоматически создает SQL-запрос с условиями LIKE для всех указанных полей. Можно управлять типом поиска, используя специальные префиксы: например, символ ^ в начале имени поля означает поиск по началу значения, а = — точное совпадение. Для текстовых полей с большим объемом данных стоит рассмотреть использование полнотекстового поиска базы данных для улучшения производительности.

Атрибут ordering определяет порядок сортировки объектов по умолчанию. Можно указать как прямую, так и обратную сортировку, используя знак минуса перед именем поля. Атрибут list_per_page контролирует количество объектов на одной странице списка. Настройка readonly_fields позволяет отображать поля в форме редактирования, которые пользователь не может изменить — это полезно для автоматически вычисляемых полей или данных, которые не должны редактироваться напрямую.

Inline-модели для работы со связанными данными
Многие приложения работают с иерархическими или связанными данными. Например, модель заказа имеет связь с моделью позиций заказа, статья блога связана с комментариями, компания — с сотрудниками. Редактирование таких связанных данных по отдельности неудобно — гораздо лучше иметь возможность редактировать связанные объекты прямо на странице редактирования родительского объекта. Для этого Django Admin предоставляет механизм inline-моделей.

Inline-модели бывают двух основных типов: TabularInline и StackedInline. Оба позволяют редактировать связанные объекты на странице родительского объекта, но отличаются визуальным представлением. TabularInline отображает связанные объекты в виде компактной таблицы, где каждая строка — это один связанный объект, а столбцы — это поля. Этот формат удобен, когда связанные объекты имеют небольшое количество полей или когда важно видеть много объектов одновременно. StackedInline отображает каждый связанный объект как отдельный блок полей, расположенный вертикально. Этот формат лучше подходит для объектов с большим количеством полей или когда поля требуют больше пространства для редактирования.

Создание inline класса включает указание модели, которая будет редактироваться inline, и настройку различных параметров отображения. Параметр extra определяет количество пустых форм для новых объектов, которые отображаются по умолчанию. Параметр max_num ограничивает максимальное количество объектов, которые можно создать. Можно настраивать, какие поля отображаются в inline форме, используя атрибуты fields или exclude, аналогично основной форме модели.

Inline модели особенно мощны в сочетании с различными типами связей. Для связи один-ко-многим (ForeignKey) inline позволяет редактировать множество связанных объектов на странице одного родительского. Для связи многие-ко-многим через промежуточную модель можно создать inline для промежуточной модели, что дает полный контроль над дополнительными данными связи. Можно даже создавать вложенные inline, хотя стандартный Django Admin не поддерживает это из коробки и требует использования сторонних пакетов.

Расширенная функциональность: фильтры и действия
Помимо базовой кастомизации отображения и редактирования, Django Admin предоставляет инструменты для добавления дополнительной функциональности, которая делает работу с данными еще более эффективной. Кастомные фильтры и действия позволяют реализовывать сложные сценарии управления данными без написания дополнительных представлений.

Кастомные фильтры создаются путем наследования от admin.SimpleListFilter и реализации двух методов: lookups(), который возвращает список доступных опций фильтрации, и queryset(), который применяет фильтрацию к набору объектов на основе выбранной пользователем опции. Кастомные фильтры могут реализовывать любую логику фильтрации — от простых условий до сложных аналитических запросов. Например, можно создать фильтр, который группирует товары по ценовым категориям, или фильтр, показывающий только объекты, созданные в определенный период времени относительно текущей даты.

Действия (actions) — это функции, которые можно применить к выбранным в списке объектам. Django предоставляет стандартное действие удаления выбранных объектов, но можно создавать собственные действия для любых массовых операций. Действие — это функция, которая принимает три параметра: экземпляр класса админки, объект запроса и queryset выбранных объектов. Внутри функции можно выполнять любые операции с выбранными объектами: изменять их состояние, создавать связанные объекты, экспортировать данные, отправлять уведомления и так далее.

После выполнения действия обычно отображается сообщение пользователю о результате операции. Django предоставляет систему сообщений, которая позволяет выводить информационные, предупреждающие или ошибочные сообщения пользователю. Правильное использование этой системы делает интерфейс более понятным и информативным. Действия также могут возвращать HTTP-ответ напрямую, например, для генерации и скачивания файлов экспорта данных, что позволяет реализовывать функции типа экспорта в CSV или Excel прямо из админки.

Для действий, которые могут иметь серьезные последствия, можно реализов промежуточную страницу подтверждения. Вместо немедленного выполнения действия пользователь перенаправляется на страницу, где должен подтвердить операцию, возможно, заполнив дополнительные параметры. Это реализуется путем возврата из функции действия рендера специальной страницы с формой подтверждения, которая при отправке выполняет фактическое действие.

Практическое применение: создание админки для проекта
Теперь применим все изученные техники кастомизации Django Admin к нашему проекту новостного сайта. У нас есть три основные модели: Article (статья), Tag (тег) и встроенная модель User. Наша задача — создать удобную и функциональную административную панель, которая позволит редакторам эффективно управлять контентом сайта.

Начнем с создания файла admin.py в приложении, где находятся наши модели. Если файл уже существует, откроем его для редактирования. Первым делом импортируем необходимые модули и наши модели.

from django.contrib import admin
from django.utils.html import format_html
from django.db.models import Count
from .models import Article, Tag
Здесь мы импортируем базовый модуль admin, функцию format_html для безопасного отображения HTML в админке, агрегатную функцию Count для подсчета связанных объектов, и наши модели. Функция format_html критически важна — она позволяет отображать HTML-код в админке безопасным образом, автоматически экранируя пользовательские данные и предотвращая XSS-атаки.

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

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug', 'articles_count']
    search_fields = ['name', 'slug']
    prepopulated_fields = {'slug': ('name',)}

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(
            _articles_count=Count('articles', distinct=True)
        )
        return queryset

    def articles_count(self, obj):
        return obj._articles_count

    articles_count.short_description = 'Количество статей'
    articles_count.admin_order_field = '_articles_count'
Разберем каждый элемент этой конфигурации подробно. Декоратор @admin.register(Tag) регистрирует класс TagAdmin как конфигурацию админки для модели Tag. Это современная альтернатива вызову admin.site.register(Tag, TagAdmin), которая делает код более читаемым и компактным.

В атрибуте list_display мы указываем три колонки: название тега, его slug и количество связанных статей. Первые два поля — это прямые поля модели, а третье — вычисляемое поле, которое мы реализуем через метод articles_count. Атрибут search_fields добавляет поисковую строку в верхней части списка, позволяя искать теги по названию или slug.

Особенно интересен параметр prepopulated_fields. Он создает JavaScript-функциональность, которая автоматически заполняет поле slug на основе поля name при создании нового тега. Когда редактор вводит название тега, slug автоматически генерируется, преобразуя кириллицу в транслитерацию и заменяя пробелы на дефисы. Это значительно ускоряет создание тегов и снижает вероятность ошибок.

Метод get_queryset() — это ключевой метод для оптимизации производительности. Мы переопределяем его, чтобы добавить аннотацию, подсчитывающую количество статей для каждого тега. Используя annotate() с Count(), мы выполняем эффективный SQL-запрос с GROUP BY, который считает связанные статьи на уровне базы данных. Результат сохраняется в виртуальном поле _articles_count, которое затем используется методом articles_count() для отображения.

Метод articles_count() принимает объект тега и возвращает количество статей из аннотированного поля. Мы добавляем атрибут short_description, чтобы задать заголовок колонки в таблице, и admin_order_field, чтобы разрешить сортировку по этой колонке. Благодаря тому, что данные уже аннотированы в queryset, сортировка работает эффективно без дополнительных запросов.

Статьи — это основная сущность нашего новостного сайта, поэтому их админка должна быть максимально функциональной и удобной. Создадим класс с множеством настроек.

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        'title',
        'author',
        'status_indicator',
        'tags_display',
        'created_at_formatted',
        'updated_at_formatted'
    ]

    list_filter = [
        'is_published',
        'created_at',
        'author',
        'tags'
    ]

    search_fields = ['title', 'content', 'author__username', 'author__email']

    filter_horizontal = ['tags']

    readonly_fields = ['created_at', 'updated_at', 'preview_content']

    fieldsets = (
        ('Основная информация', {
            'fields': ('title', 'author', 'content')
        }),
        ('Категоризация', {
            'fields': ('tags',)
        }),
        ('Публикация', {
            'fields': ('is_published',)
        }),
        ('Служебная информация', {
            'fields': ('created_at', 'updated_at', 'preview_content'),
            'classes': ('collapse',)
        })
    )

    date_hierarchy = 'created_at'

    list_per_page = 25

    actions = ['publish_articles', 'unpublish_articles', 'export_to_csv']
Это уже гораздо более сложная конфигурация, давайте разберем ее по частям. В list_display мы указали шесть колонок, три из которых — вычисляемые методы, которые мы реализуем далее. Это дает нам полный контроль над отображением данных в списке статей.

Атрибут list_filter создает боковую панель с фильтрами. Мы добавили фильтры по статусу публикации, дате создания, автору и тегам. Django автоматически создает подходящий интерфейс для каждого типа фильтра: булево поле получает простой переключатель да/нет, дата — фильтр по периодам (сегодня, на этой неделе, в этом месяце и т.д.), внешний ключ и многие-ко-многим — список доступных значений.

В search_fields мы указали не только поля самой модели Article, но и связанные поля модели User через двойное подчеркивание. Это позволяет искать статьи по имени автора или его email. Django автоматически создаст JOIN в SQL-запросе для поиска по этим полям.

Параметр filter_horizontal создает удобный виджет для выбора тегов. Вместо стандартного многострочного select с зажатым Ctrl, пользователь получает две колонки: доступные теги слева и выбранные справа, с возможностью перемещать элементы между ними двойным кликом или кнопками. Это значительно удобнее при работе с большим количеством тегов.

Атрибут readonly_fields делает указанные поля доступными только для чтения. Это полезно для автоматически заполняемых полей, таких как временные метки, или для вычисляемых полей. Мы добавили preview_content — метод, который покажем ниже.

Параметр fieldsets организует форму редактирования в логические разделы. Это особенно важно для моделей с большим количеством полей. Каждый fieldset — это кортеж из названия раздела и словаря с настройками. В словаре ключ ‘fields’ указывает, какие поля включить в раздел, а ключ ‘classes’ позволяет добавить CSS-классы. Класс ‘collapse’ делает раздел сворачиваемым — он будет скрыт по умолчанию, а пользователь сможет развернуть его при необходимости. Это полезно для служебной информации, которая нужна редко.

Атрибут date_hierarchy добавляет в верхнюю часть списка навигацию по датам — удобные ссылки для быстрого перехода к статьям, созданным в определенном году, месяце или дне. Это особенно полезно для новостных сайтов, где важна хронология.

Параметр list_per_page контролирует количество объектов на странице. Значение по умолчанию — 100, но для статей с большим содержимым имеет смысл уменьшить это число для ускорения загрузки страницы.

Теперь реализуем методы, которые мы указали в list_display и других атрибутах:

def get_queryset(self, request):
    queryset = super().get_queryset(request)
    queryset = queryset.select_related('author').prefetch_related('tags')
    return queryset

def status_indicator(self, obj):
    if obj.is_published:
        color = 'green'
        text = 'Опубликовано'
    else:
        color = 'red'
        text = 'Черновик'

    return format_html(
        '<span style="color: {}; font-weight: bold;">●</span> {}',
        color,
        text
    )

status_indicator.short_description = 'Статус'

def tags_display(self, obj):
    tags = obj.tags.all()
    if not tags:
        return '-'

    tag_links = []
    for tag in tags:
        tag_links.append(
            format_html(
                '<span style="background-color: #e3f2fd; padding: 2px 8px; '
                'border-radius: 3px; margin-right: 5px; display: inline-block;">{}</span>',
                tag.name
            )
        )

    return format_html(' '.join(tag_links))

tags_display.short_description = 'Теги'

def created_at_formatted(self, obj):
    return obj.created_at.strftime('%d.%m.%Y %H:%M')

created_at_formatted.short_description = 'Создана'
created_at_formatted.admin_order_field = 'created_at'

def updated_at_formatted(self, obj):
    return obj.updated_at.strftime('%d.%m.%Y %H:%M')

updated_at_formatted.short_description = 'Обновлена'
updated_at_formatted.admin_order_field = 'updated_at'

def preview_content(self, obj):
    if len(obj.content) > 200:
        preview = obj.content[:200] + '...'
    else:
        preview = obj.content

    return format_html(
        '<div style="padding: 10px; background-color: #f5f5f5; '
        'border-left: 3px solid #2196F3;">{}</div>',
        preview
    )

preview_content.short_description = 'Предпросмотр содержания'
Метод get_queryset() критически важен для производительности. Без оптимизации Django выполнил бы отдельный запрос к базе данных для получения автора каждой статьи и еще один запрос для получения тегов. При выводе 25 статей это привело бы к проблеме N+1 запросов. Используя select_related(‘author’), мы заставляем Django выполнить JOIN и получить данные автора в том же запросе. Используя prefetch_related(‘tags’), мы получаем все теги для всех статей всего двумя запросами вместо 25. Это драматически улучшает производительность.

Метод status_indicator() создает визуальный индикатор статуса публикации. Вместо простого отображения True/False мы показываем цветной кружок и текстовую метку. Функция format_html() безопасно внедряет HTML, экранируя все переменные. Это важно для безопасности — если бы мы использовали простую конкатенацию строк или f-string с пометкой mark_safe, злоумышленник мог бы внедрить вредоносный JavaScript через данные модели.

Метод tags_display() отображает теги статьи в виде стилизованных бирок. Мы получаем все теги статьи (они уже предзагружены благодаря prefetch_related, поэтому дополнительного запроса не будет), создаем для каждого HTML-элемент и объединяем их. Если у статьи нет тегов, возвращаем просто дефис.

Методы форматирования дат created_at_formatted() и updated_at_formatted() преобразуют datetime объекты в читаемый формат. Важно, что мы устанавливаем admin_order_field, чтобы при клике на заголовок колонки сортировка работала по настоящему полю даты, а не по отформатированной строке.

Метод preview_content() создает предпросмотр содержания статьи на странице редактирования. Это полезно, когда редактор хочет быстро вспомнить, о чем статья, не прокручивая большое текстовое поле. Мы ограничиваем длину превью 200 символами и оборачиваем текст в стилизованный блок.

Теперь реализуем 3 действия. Действия позволяют выполнять массовые операции с выбранными статьями.

def publish_articles(self, request, queryset):
    updated = queryset.update(is_published=True)
    self.message_user(
        request,
        f'Опубликовано статей: {updated}',
        level='success'
    )

publish_articles.short_description = 'Опубликовать выбранные статьи'

def unpublish_articles(self, request, queryset):
    updated = queryset.update(is_published=False)
    self.message_user(
        request,
        f'Снято с публикации статей: {updated}',
        level='warning'
    )

unpublish_articles.short_description = 'Снять с публикации выбранные статьи'

def export_to_csv(self, request, queryset):
    import csv
    from django.http import HttpResponse

    response = HttpResponse(content_type='text/csv; charset=utf-8')
    response['Content-Disposition'] = 'attachment; filename="articles.csv"'
    response.write('\ufeff')  # BOM для корректного отображения кириллицы в Excel

    writer = csv.writer(response)
    writer.writerow(['Заголовок', 'Автор', 'Статус', 'Дата создания', 'Теги'])

    for article in queryset.select_related('author').prefetch_related('tags'):
        tags = ', '.join([tag.name for tag in article.tags.all()])
        status = 'Опубликовано' if article.is_published else 'Черновик'

        writer.writerow([
            article.title,
            article.author.username,
            status,
            article.created_at.strftime('%d.%m.%Y %H:%M'),
            tags
        ])

    return response

export_to_csv.short_description = 'Экспортировать в CSV'
Первые два действия — publish_articles() и unpublish_articles() — изменяют статус публикации выбранных статей. Метод queryset.update() выполняет массовое обновление одним SQL-запросом, что гораздо эффективнее, чем итерация по объектам и вызов save() для каждого. Метод self.message_user() отображает сообщение пользователю после выполнения действия. Параметр level определяет тип сообщения: ‘success’ для успешных операций, ‘warning’ для предупреждений, ‘error’ для ошибок.

Действие export_to_csv() более сложное — оно экспортирует выбранные статьи в CSV-файл. Мы создаем HTTP-ответ с типом content-type для CSV и устанавливаем заголовок Content-Disposition, чтобы браузер предложил скачать файл. Запись BOM (\ufeff) в начале файла критически важна для корректного отображения кириллицы в Microsoft Excel. Затем мы создаем CSV writer и записываем заголовки колонок и данные статей. Обратите внимание на использование select_related() и prefetch_related() для оптимизации запросов при экспорте большого количества статей.

И также добавим кастомный фильтр, который позволит фильтровать статьи по количеству тегов:

class TagCountFilter(admin.SimpleListFilter):
title = 'количество тегов'
parameter_name = 'tag_count'

def lookups(self, request, model_admin):
    return (
        ('0', 'Без тегов'),
        ('1-3', 'От 1 до 3'),
        ('4+', '4 и больше'),
    )

def queryset(self, request, queryset):
    queryset = queryset.annotate(tag_count=Count('tags'))

    if self.value() == '0':
        return queryset.filter(tag_count=0)
    elif self.value() == '1-3':
        return queryset.filter(tag_count__gte=1, tag_count__lte=3)
    elif self.value() == '4+':
        return queryset.filter(tag_count__gte=4)

    return queryset
Теперь добавим этот фильтр в ArticleAdmin:

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_filter = [
        'is_published',
        'created_at',
        'author',
        'tags',
        TagCountFilter,  # Добавляем наш кастомный фильтр
    ]
    # ... остальной код
Кастомный фильтр наследуется от admin.SimpleListFilter и требует определения двух методов. Метод lookups() возвращает список опций фильтра в виде кортежей (значение, отображаемый текст). Метод queryset() применяет фильтрацию на основе выбранного значения. Мы используем annotate() для подсчета тегов и затем фильтруем по этому вычисленному полю. Такой фильтр полезен для редакторов, чтобы быстро находить статьи, которым нужно добавить больше тегов для лучшей категоризации.

Ссылка на текущий исходный код приложения — репозиторий на GitHub