Шаг 07. Фикстуры, management-команды, финал
⚡ Кратко: что делаем на этом шаге
Цель: Сохранить тестовые данные в фикстуры (dumpdata), создать management-команду для импорта книг, пройти финальный чеклист проекта.
- Команды:
python manage.py dumpdata catalog --indent 2 -o fixtures/catalog.json - Команды:
python manage.py loaddata fixtures/catalog.json - Файлы:
catalog/management/commands/import_books.py,fixtures/catalog.json
🎯 Цель этапа
Финальный шаг: инструменты для работы с данными и подведение итогов.
Фикстуры позволяют воспроизвести начальное состояние БД на любой машине.
Management-команды — расширение возможностей manage.py без создания
отдельных скриптов.
После этого шага у нас будет
- Фикстуры с тестовыми данными для каталога
- Management-команда
import_booksс аргументами - Пройденный финальный чеклист проекта
- Понимание, как развивать проект дальше
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
fixtures/catalog.json | Создать (dumpdata) | Фикстура с данными каталога |
catalog/management/__init__.py | Создать | Пакет management |
catalog/management/commands/__init__.py | Создать | Пакет commands |
catalog/management/commands/import_books.py | Создать | Команда manage.py import_books |
🔨 Шаги
1. Создаём фикстуры
Убедитесь, что через Admin добавили хотя бы несколько записей (жанры, авторы, книги).
# Создаём папку для фикстур
mkdir fixtures
# Экспортируем данные приложения catalog
python manage.py dumpdata catalog --indent 2 -o fixtures/catalog.json
# Проверяем (должен содержать Author, Genre, Publisher, Book, BookCopy)
python -c "import json; data=json.load(open('fixtures/catalog.json')); print(f'{len(data)} записей')"
catalog— экспортируем только приложение catalog (без auth, admin и пр.)--indent 2— форматируем JSON с отступами (читаемо)-o fixtures/catalog.json— записываем в файл (без -o — выводит в stdout)
2. Проверяем загрузку фикстуры
# Очищаем данные (осторожно! только для теста)
# python manage.py flush --no-input
# Загружаем фикстуру
python manage.py loaddata fixtures/catalog.json
Installed N object(s) from 1 fixture(s)
3. Создаём management-команду
mkdir catalog\management
type nul > catalog\management\__init__.py
mkdir catalog\management\commands
type nul > catalog\management\commands\__init__.py
# catalog/management/commands/import_books.py
"""
Management-команда для импорта книг из JSON-файла.
Запуск: python manage.py import_books books.json [--dry-run]
Пример books.json:
[
{
"title": "Мастер и Маргарита",
"authors": ["Булгаков Михаил"],
"genres": ["классика"],
"publish_year": 1967
}
]
"""
import json
from pathlib import Path
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from catalog.models import Author, Book, Genre
class Command(BaseCommand):
help = "Импортировать книги из JSON-файла"
def add_arguments(self, parser) -> None:
parser.add_argument(
"file",
type=str,
help="Путь к JSON-файлу с книгами",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Режим проверки без реального сохранения",
)
def handle(self, *args, **options) -> None:
file_path = Path(options["file"])
dry_run: bool = options["dry_run"]
if not file_path.exists():
raise CommandError(f"Файл не найден: {file_path}")
try:
data = json.loads(file_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise CommandError(f"Ошибка чтения JSON: {e}")
if not isinstance(data, list):
raise CommandError("JSON должен содержать список книг.")
created = 0
skipped = 0
# Используем транзакцию: если что-то пошло не так — откатываем всё
with transaction.atomic():
for item in data:
title = item.get("title", "").strip()
if not title:
self.stdout.write(self.style.WARNING(" Пропущена книга без названия."))
skipped += 1
continue
# get_or_create: не дублируем если уже есть
book, book_created = Book.objects.get_or_create(
title=title,
defaults={"publish_year": item.get("publish_year")},
)
if not book_created:
self.stdout.write(f" Пропущено (уже есть): {title}")
skipped += 1
continue
# Привязываем авторов (создаём если нет)
for author_str in item.get("authors", []):
parts = author_str.strip().split(maxsplit=1)
last = parts[0] if parts else author_str
first = parts[1] if len(parts) > 1 else ""
author, _ = Author.objects.get_or_create(
last_name=last,
first_name=first,
)
book.authors.add(author)
# Привязываем жанры
for genre_name in item.get("genres", []):
genre, _ = Genre.objects.get_or_create(
name=genre_name.capitalize(),
defaults={"slug": genre_name.lower().replace(" ", "-")},
)
book.genres.add(genre)
created += 1
self.stdout.write(self.style.SUCCESS(f" Создана: {title}"))
if dry_run:
# Откатываем всё — транзакция не сохраняется
transaction.set_rollback(True)
self.stdout.write(self.style.WARNING(
f"\n--dry-run: изменения НЕ сохранены. "
f"Было бы создано: {created}, пропущено: {skipped}"
))
else:
self.stdout.write(self.style.SUCCESS(
f"\nГотово: создано {created}, пропущено {skipped}."
))
BaseCommand.style.SUCCESS/WARNING/ERROR— цветной вывод в терминалеadd_arguments— аргументы CLI через argparsetransaction.atomic()— всё или ничего при ошибкеget_or_create— идемпотентность (повторный запуск не дублирует)--dry-run+transaction.set_rollback(True)— режим проверки
4. Тестируем management-команду
Создайте файл test_books.json в корне проекта:
[
{
"title": "Преступление и наказание",
"authors": ["Достоевский Фёдор"],
"genres": ["классика", "детектив"],
"publish_year": 1866
},
{
"title": "1984",
"authors": ["Оруэлл Джордж"],
"genres": ["антиутопия"],
"publish_year": 1949
}
]
# Сначала dry-run (проверка без сохранения)
python manage.py import_books test_books.json --dry-run
# Реальный импорт
python manage.py import_books test_books.json
🧠 Объяснение логики
Фикстуры vs management-команды
Фикстуры (dumpdata/loaddata) — для воспроизводимых начальных данных. Хранят конкретные PK, поэтому порядок загрузки важен (зависимости по FK). Удобно для тестовых данных разработки и staging-окружений.
Management-команды — для сложной логики импорта/экспорта. Idempotent (get_or_create), с валидацией, --dry-run, транзакциями. Подходят для ETL, периодических задач, CI/CD-пайплайнов.
Почему transaction.atomic() в management-команде
Если в середине импорта файл заканчивается ошибкой (невалидный элемент,
constraint violation) — без транзакции в БД будет неполный импорт.
С transaction.atomic() — всё или ничего. Пользователь видит
чёткую ошибку и может исправить файл.
✅ Финальный чеклист проекта
Функциональность
| Пункт | Проверка |
|---|---|
| Django-проект стартует без ошибок | python manage.py check — 0 issues |
| Миграции применены | python manage.py showmigrations — все [X] |
| Admin открывается | http://127.0.0.1:8000/admin/ — форма входа |
| Все 5 моделей в Admin | Author, Genre, Publisher, Book, BookCopy |
| Inline работает | На странице книги — таблица экземпляров |
| Actions работают | «Отметить доступными» — зелёное сообщение после |
| Список книг доступен | http://127.0.0.1:8000/catalog/ — список |
| Фикстура загружается | python manage.py loaddata fixtures/catalog.json — OK |
| import_books работает | python manage.py import_books test_books.json --dry-run — OK |
| Валидация работает | BookCopy со статусом ON_LOAN без due_back → ValidationError |
Что закрыто из callout-verify
- Урок 15 — full_clean() в save(): реализовано в Author и BookCopy
- Урок 15 — валидаторы полей: RegexValidator для ISBN и slug, MinValueValidator для годов
- Урок 19 — ModelAdmin: @admin.register, list_display, search_fields, list_filter, ordering, get_queryset
- Урок 24 — Inline: TabularInline для BookCopy в BookAdmin
- Урок 24 — Admin actions: @admin.action с queryset.update() и сообщением
- Урок 24 — queryset.update() vs сигналы: таблица сравнения, явное объяснение в коде
- Урок 22 — N+1: select_related для FK, prefetch_related для M2M, annotate для агрегатов
- Урок 22 — Q/F-объекты: демонстрация в shell и view
➡️ Что дальше
Возможные пути развития проекта
- Добавить аутентификацию читателей — кастомная модель User, регистрация, выдача книг конкретным пользователям
- REST API — Django REST Framework для мобильного/frontend-клиента (Капстоун C закрывает это)
- Поиск — django.contrib.postgres полнотекстовый поиск или интеграция с Elasticsearch
- Celery-задача — автоматические email-напоминания о просроченных книгах через сигналы
- Deploy — Gunicorn + Nginx + PostgreSQL вместо SQLite + DEBUG=False