Что это такое?

Management Command — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через python manage.py ....

Зачем это нужно?

Представьте ситуации:

  • Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).
  • Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).
  • Нужно отправить e-mail рассылку пользо­вателям.
  • Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.

Делать это через views.py (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом manage.py — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.

Как это работает?

Django использует систему «автообна­ружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, web):

web/
├── __init__.py
├── models.py
└── management/                 <-- 1. Создаем папку management
    ├── __init__.py
    └── nginx/
        ├── commands/           <-- 2. Внутри неё папку commands
        ├── __init__.py
        └── my_cool_script.py   <-- 3. Наш файл с командой

Файл my_cool_script.py автома­тически превратится в команду: python manage.py my_cool_script. Django будет искать его в папках management/commands/ внутри каждого устано­вленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.

Для справки: Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.

Анатомия команды

Вот пример простейшей команды. Мы наследуемся от класса BaseCommand и переопре­деляем метод handle:

# web/management/commands/hello.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = 'Выводит приветствие'  # Описание для --help

    def add_arguments(self, parser):
        # Можно добавлять аргументы, как в argparse
        parser.add_argument('name', type=str, help='Имя пользователя')

    def handle(self, *args, **options):
        # Главная логика
        name = options['name']
        self.stdout.write(f"Привет, {name}!")

Запуск:

python manage.py hello Иван

Увидим в консоли:

Привет, Иван!

Реальный пример: Массовая типографика через etpgrf

В моём проекте, DQ – коллекция цитат: место для вдумчивого чтения, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типогра­фируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересо­хранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.

Вот как я это реализовал. В web/management/commands/reprocess_typography.py:

from django.core.management.base import BaseCommand
from web.models import TbDictumAndQuotes
# Импорты библиотеки etpgrf
from etpgrf.typograph import Typographer
from etpgrf.sanitizer import SanitizerProcessor
# ... другие процессоры, если нужно ...

class Command(BaseCommand):
    help = 'Переобрабатывает все цитаты'

    def add_arguments(self, parser):
        # Добавляем флаг "сухой прогон"
        parser.add_argument(
            '--dry-run',
            action='store_true',
            help='Запустить без сохранения изменений',
        )
        # Ограничение количества
        parser.add_argument('--limit', type=int, help='Сколько записей обработать')
        # Смещение (для пагинации)
        parser.add_argument('--offset', type=int, default=0, help='Сколько пропустить')

    def handle(self, *args, **options):
        # 1. Настраиваем типограф
        # ... инициализация процессоров ...
        settings = {
             # ...
             'sanitizer': SanitizerProcessor(mode="html"), # Режим HTML-очистки
             'hanging_punctuation': 'left',                # Режим висячей пунктуации
             # ...
        }
        typographer = Typographer(**settings)

        # 2. Получаем записи из базы с учетом limit/offset
        qs = TbDictumAndQuotes.objects.all().order_by('id')

        start = options['offset']
        if options['limit']:
             qs = qs[start : start + options['limit']]
        else:
             qs = qs[start:]

        self.stdout.write(f"Найдено {qs.count()} цитат...")

        # 3. Бежим по циклу с прогресс-баром (tqdm если есть)
        try:
            from tqdm import tqdm
            iterator = tqdm(qs)
        except ImportError:
            iterator = qs

        for dq in iterator:
            try:
                # Обрабатываем текст
                new_html = typographer.process(dq.szContent)

                if options['dry_run']:
                    self.stdout.write(f"[{dq.id}] Предпросмотр: {new_html[:50]}...")
                else:
                    dq.szContentHTML = new_html
                    dq.save(update_fields=['szContentHTML'])

            except Exception as e:
                self.stdout.write(self.style.ERROR(f"Ошибка id={dq.id}: {e}"))


        self.stdout.write(self.style.SUCCESS(f"\nГотово!"))

Возможно вы заметили, что в моем проекте для контента есть два поля: szContent и szContentHTML. В первом хранится «сырой» текст, во втором — результат типогра­фирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.

Еще интересные фишки, которые исполь­зовались при типогра­фировании:

  1. self.stdout.write вместо print – позволяет Django перехва­тывать вывод (например, для тестов) и корректно работать с кодировками.
  2. self.style.SUCCESS и self.style.ERROR – раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.
  3. Аргумент --dry-run — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.
  4. Аргументы --limit и --offset — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными offset).
  5. Используется update_fields — позволяет переза­писывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.

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

Тестовый прогон (безопасно, ничего не сохраняет):

python manage.py reprocess_typography --dry-run

Боевой запуск, с изменениями данные в базе:

python manage.py reprocess_typography

Если у вас очень много записей, можно запускать по частям:

python manage.py reprocess_typography --limit 1000 --offset 0
python manage.py reprocess_typography --limit 1000 --offset 1000
python manage.py reprocess_typography --limit 1000 --offset 2000