Шаг 04. ModelAdmin: список и фильтры
⚡ Кратко: что делаем на этом шаге
Цель: Зарегистрировать все модели в Django Admin через @admin.register, настроить list_display, search_fields, list_filter, ordering. Закрывает callout-verify урока 19.
- Файлы:
catalog/admin.py - Результат: Admin показывает все модели с удобными колонками, фильтрами и поиском
🎯 Цель этапа
На этом шаге мы превращаем Django Admin из простого CRUD-интерфейса в удобный инструмент
управления каталогом. Закрываем callout-verify из урока 19:
показываем полный набор атрибутов ModelAdmin с реальными примерами.
class BookAdmin(ModelAdmin): ... и admin.site.register(Book, BookAdmin)
— используем один декоратор: @admin.register(Book) над классом.
Читабельнее, лаконичнее, не нужно помнить про вторую строку.
После этого шага у нас будет
- Все 5 моделей зарегистрированы в Admin
- Список книг показывает название, авторов, издательство, год, количество экземпляров
- Поиск по названию, автору, ISBN
- Фильтры по жанрам, издательству, году издания
- Кастомный вычисляемый столбец (количество экземпляров)
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
catalog/admin.py | Заполнить | ModelAdmin для всех моделей |
🔨 Шаги
1. Пишем ModelAdmin для всех моделей
# catalog/admin.py
from django.contrib import admin
from django.db.models import QuerySet, Count
from django.http import HttpRequest
from .models import Author, Genre, Publisher, Book, BookCopy
# ─── Author ─────────────────────────────────────────────────────────────────
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
list_display = ["last_name", "first_name", "birth_date", "books_count"]
search_fields = ["last_name", "first_name"]
ordering = ["last_name", "first_name"]
list_per_page = 25
@admin.display(description="Книг в каталоге", ordering="books_count")
def books_count(self, obj: Author) -> int:
"""Количество книг автора — вычисляемый столбец."""
return obj.books.count()
def get_queryset(self, request: HttpRequest) -> QuerySet:
"""Аннотируем queryset для сортировки по books_count."""
return super().get_queryset(request).annotate(
books_count=Count("books")
)
# ─── Genre ──────────────────────────────────────────────────────────────────
@admin.register(Genre)
class GenreAdmin(admin.ModelAdmin):
list_display = ["name", "slug"]
search_fields = ["name", "slug"]
ordering = ["name"]
prepopulated_fields = {"slug": ("name",)} # автозаполнение slug из name
# ─── Publisher ──────────────────────────────────────────────────────────────
@admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin):
list_display = ["name", "website", "founded_year", "books_count"]
search_fields = ["name"]
ordering = ["name"]
list_filter = ["founded_year"]
@admin.display(description="Книг", ordering="books_count")
def books_count(self, obj: Publisher) -> int:
return obj.books.count()
def get_queryset(self, request: HttpRequest) -> QuerySet:
return super().get_queryset(request).annotate(
books_count=Count("books")
)
# ─── Book ───────────────────────────────────────────────────────────────────
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = [
"title",
"get_authors",
"publisher",
"publish_year",
"isbn",
"copies_count",
]
search_fields = [
"title",
"isbn",
"authors__last_name",
"authors__first_name",
]
list_filter = ["genres", "publisher", "publish_year"]
filter_horizontal = ["authors", "genres"] # виджет двойного списка для M2M
ordering = ["title"]
list_per_page = 20
date_hierarchy = None # у нас нет DateField на Book
@admin.display(description="Авторы")
def get_authors(self, obj: Book) -> str:
"""Список авторов через запятую."""
return ", ".join(str(a) for a in obj.authors.all())
@admin.display(description="Экземпляров", ordering="copies_count")
def copies_count(self, obj: Book) -> int:
return obj.copies.count()
def get_queryset(self, request: HttpRequest) -> QuerySet:
return super().get_queryset(request).prefetch_related(
"authors", "genres"
).annotate(
copies_count=Count("copies")
)
# ─── BookCopy ────────────────────────────────────────────────────────────────
@admin.register(BookCopy)
class BookCopyAdmin(admin.ModelAdmin):
list_display = [
"inventory_number",
"book",
"status",
"due_back",
]
list_filter = ["status"]
search_fields = ["inventory_number", "book__title"]
ordering = ["book", "inventory_number"]
list_per_page = 30
# Выделяем просроченные экземпляры цветом через CSS-класс
# (см. объяснение ниже)
list_display_links = ["inventory_number"]
filter_horizontal = ["authors", "genres"] заменяет стандартный
<select multiple> на виджет с двумя списками и кнопками →/←.
Это стандартный способ работы с M2M в Admin — значительно удобнее при большом
количестве вариантов. Альтернатива — filter_vertical.
ordering в @admin.display указывает, по какому полю
(или аннотации) сортировать при клике на заголовок колонки в Admin.
Без него — клик на заголовок не сортирует. Поэтому в get_queryset()
мы аннотируем queryset именем аннотации, совпадающим с ordering.
🧠 Объяснение логики
Кастомные методы в list_display
Django Admin позволяет добавлять в list_display не только поля модели,
но и методы класса AdminModel. Метод получает объект модели (obj)
и возвращает строку (или HTML при mark_safe).
Декоратор @admin.display(description="...", ordering="...")
задаёт заголовок колонки и поле для сортировки.
get_queryset и N+1 в Admin
По умолчанию Django Admin для каждого объекта в списке делает отдельный запрос
для связанных данных — это N+1. Переопределяя get_queryset(),
мы добавляем prefetch_related("authors", "genres") — один запрос
на все связанные объекты вместо N. Аннотация Count("copies")
считается в том же запросе.
prepopulated_fields для slug
prepopulated_fields = {"slug": ("name",)} включает JavaScript-логику
в форме Admin: при вводе name поле slug заполняется
автоматически (транслитерация + замена пробелов на дефисы). Пользователь может
отредактировать slug вручную.
"authors__last_name".
Регистр важен: Django делает ILIKE по умолчанию, но при добавлении
^ в начало ("^authors__last_name") — поиск только с начала слова.
✅ Проверка
1. Запускаем сервер
python manage.py runserver
2. Открываем Admin
http://127.0.0.1:8000/admin/
3. Ожидаемый результат
- В Admin видны разделы: Авторы, Жанры, Издательства, Книги, Экземпляры книг
- В списке книг — колонки «Авторы», «Издательство», «Год», «ISBN», «Экземпляров»
- В правой панели — фильтры по жанрам, издательству, году
- Строка поиска ищет по названию и ISBN
- Форма редактирования книги — виджет двойного списка для авторов и жанров
4. Добавляем тестовые данные
Через Admin создайте несколько записей для проверки отображения:
- 2–3 жанра (например: «Классика», slug:
classika) - 1–2 издательства
- 2–3 автора
- 1–2 книги с авторами и жанрами
- 2–3 экземпляра для одной из книг
Диагностика: если что-то пошло не так
- Модели не появляются в Admin — проверьте импорты в
admin.pyи декораторы@admin.register - AttributeError в вычисляемом столбце — метод
get_querysetне аннотирует queryset; убедитесь, что имя аннотации совпадает сorderingв@admin.display - Ошибка при
prepopulated_fields— slug в спискеprepopulated_fieldsне должен быть вreadonly_fields
➡️ Что дальше
На следующем шаге добавляем «вторую фичу Admin»: inline-формы для экземпляров книг
прямо на странице редактирования книги, и admin actions для массового обновления статуса.
Разберём принципиальную разницу между queryset.update() и сигналами Django.
- Готово: все модели в Admin с фильтрами, поиском и кастомными колонками
- Далее (шаг 05):
TabularInline,@admin.action, queryset.update vs сигналы