Шаг 04. Админка: ModelAdmin, Inline и Actions

📁 Серия: Капстоун C ⏱️ ~35 мин 🎯 Сложность: Средняя
#admin #ModelAdmin #TabularInline #actions

⚡ Кратко: что делаем на этом шаге

Цель: Зарегистрировать все модели в Django Admin с настройками list_display, list_filter, search_fields. Добавить TaskInline в ProjectAdmin. Написать кастомный action «Отметить выполненными».

  • Файлы: apps/projects/admin.py, apps/tasks/admin.py
  • Команды: запустить сервер, открыть Admin
  • Результат: все модели доступны в Admin с удобными фильтрами; задачи видны внутри проекта

🎯 Цель этапа

Django Admin — мощный инструмент управления данными. Настроив его хорошо, можно создавать/редактировать проекты и задачи без API и без frontend. Это особенно полезно при разработке — проще наполнить базу тестовыми данными.

💡 Inline — задачи внутри проекта: TabularInline позволяет видеть и редактировать связанные объекты прямо в форме родительского объекта. Открываем проект — видим его задачи. Это закрывает callout-verify из уроков 19/24 про inline-формы.

После этого шага у нас будет

  • Project, Task, Comment — в Django Admin с настроенными фильтрами
  • Inline: задачи видны внутри проекта
  • Action: выбрать задачи → «Отметить выполненными» (bulk update)

📄 Затрагиваемые файлы

ФайлДействиеОписание
apps/projects/admin.pyЗаполнитьProjectAdmin с TaskInline
apps/tasks/admin.pyЗаполнитьTaskAdmin и CommentAdmin с actions

🔨 Шаги

1. TaskInline и ProjectAdmin

📄 apps/projects/admin.py
# apps/projects/admin.py
from django.contrib import admin
from .models import Project
from apps.tasks.models import Task


class TaskInline(admin.TabularInline):
    """
    Задачи внутри проекта — отображаются прямо в форме проекта.
    TabularInline: компактный табличный вид (vs StackedInline — вертикальный).
    """
    model = Task
    fields = ("title", "assignee", "status", "priority", "due_date")
    extra = 0          # не показывать пустые формы добавления по умолчанию
    show_change_link = True   # ссылка на полную форму редактирования задачи


@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ("name", "owner", "is_active", "task_count", "created_at")
    list_filter = ("is_active", "created_at")
    search_fields = ("name", "description", "owner__email")
    readonly_fields = ("created_at", "updated_at")
    filter_horizontal = ("members",)   # удобный виджет для M2M
    inlines = [TaskInline]

    @admin.display(description="Задач")
    def task_count(self, obj: Project) -> int:
        """Кол-во задач проекта — без лишнего запроса (через annotate в get_queryset)."""
        return obj.tasks.filter(is_deleted=False).count()

2. TaskAdmin и CommentAdmin с action

📄 apps/tasks/admin.py
# apps/tasks/admin.py
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Task, Comment


@admin.action(description="Отметить выбранные задачи как выполненные")
def mark_done(modeladmin, request, queryset):
    """
    Bulk action: устанавливает status=DONE для выбранных задач.
    queryset.update() не вызывает save() на каждом объекте — это быстро,
    но обходит сигналы post_save (важно помнить).
    """
    updated = queryset.update(status=Task.Status.DONE)
    modeladmin.message_user(
        request,
        f"Отмечено выполненными: {updated} задач.",
    )


@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = (
        "title", "project", "assignee", "status", "priority",
        "due_date", "is_deleted", "created_at",
    )
    list_filter = ("status", "priority", "is_deleted", "project", "created_at")
    search_fields = ("title", "description", "assignee__email", "project__name")
    readonly_fields = ("created_at", "updated_at")
    list_editable = ("status", "priority")   # редактирование прямо в списке
    actions = [mark_done]
    date_hierarchy = "created_at"

    def get_queryset(self, request):
        """
        По умолчанию Admin показывает все записи, включая is_deleted=True.
        Для Admin это нормально — нужно видеть и «удалённые» задачи.
        В API (этап 11) добавим менеджер, скрывающий их от обычных пользователей.
        """
        return super().get_queryset(request).select_related("project", "assignee")


@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ("task", "author", "text_preview", "created_at")
    list_filter = ("created_at",)
    search_fields = ("text", "author__email", "task__title")
    readonly_fields = ("created_at", "updated_at")

    @admin.display(description="Текст")
    def text_preview(self, obj: Comment) -> str:
        """Первые 60 символов комментария."""
        return obj.text[:60] + ("..." if len(obj.text) > 60 else "")
💡 select_related в get_queryset: В TaskAdmin.get_queryset() мы добавляем select_related("project", "assignee"). Это оптимизация: без неё для каждой строки в списке Admin делался бы отдельный SQL-запрос за связанным объектом (N+1 проблема). С select_related — один JOIN-запрос.

🧠 Объяснение логики

TabularInline vs StackedInline

Django предоставляет два стиля inline:

  • TabularInline — компактная таблица: поля в одну строку. Хорошо для простых моделей с небольшим числом полей (как Task).
  • StackedInline — вертикальный блок как полноценная форма. Хорош для сложных моделей с большим числом полей.

Для задач внутри проекта TabularInline компактнее и удобнее.

actions — bulk operations

Django Action — функция, которую пользователь Admin может применить к выбранным объектам из списка. queryset.update(status=Task.Status.DONE) делает один SQL UPDATE для всех выбранных строк — это быстро.

Но: queryset.update() не вызывает метод save() на каждом объекте, а значит не срабатывают сигналы post_save. Если вам нужна логика в сигналах (этап 10) — используйте цикл for obj in queryset: obj.save() или вызывайте сигналы вручную.

⚠️ list_editable + list_display: Поля, перечисленные в list_editable, должны также быть в list_display. Иначе Django выбросит ImproperlyConfigured.

✅ Проверка

1. Запускаем сервер

💻 Терминал
python manage.py runserver

2. Открываем Admin

Открываем http://127.0.0.1:8000/admin/

Успех:
  • В разделе «ПРОЕКТЫ» видим Project с колонками: название, владелец, активен, кол-во задач, дата создания
  • Открыв проект, видим раздел «Tasks» с задачами в табличном виде (TaskInline)
  • В разделе «ЗАДАЧИ» видим Task с колонками статус, приоритет, исполнитель; можно фильтровать и искать
  • В Task list выбираем задачи → Action «Отметить как выполненные» → статус меняется

Диагностика: если что-то пошло не так

  • Модели не видны в Admin — не добавили @admin.register или не включили приложение в INSTALLED_APPS
  • ImproperlyConfigured: The value of 'list_editable[0]'... — поле есть в list_editable, но нет в list_display
  • TaskInline пустой — к проекту ещё нет задач; создайте через Admin напрямую или через shell

➡️ Что дальше

На следующем шаге переходим к самому сердцу DRF — сериализаторам. Напишем ModelSerializer для всех моделей, добавим валидацию и вложенные представления.

  • Готово: Django Admin настроен, inline и action работают
  • Далее (шаг 05): apps/tasks/serializers.py, apps/projects/serializers.py — ModelSerializer, validate_, вложенные поля
  • После шага 05 у нас будет слой сериализации для всего API