Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
Основные возможности:
|
||||
- Веб-интерфейс для ввода текста и настройки параметров типографики.
|
||||
"""
|
||||
__version__ = "0.2.7"
|
||||
__version__ = "0.2.8"
|
||||
__author__ = "Sergei Erjemin"
|
||||
__email__ = "erjemin@gmail.com"
|
||||
__license__ = "MIT"
|
||||
|
||||
@@ -32,145 +32,21 @@
|
||||
<hr>
|
||||
<div class="post-content mt-4">
|
||||
|
||||
<h2>Что это такое?</h2>
|
||||
<p><strong>Management Command</strong> — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через <code>python manage.py ...</code>.</p>
|
||||
<h2>Зачем это нужно?</h2>
|
||||
<p>Представьте ситуации:</p>
|
||||
<ul>
|
||||
<li>Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).</li>
|
||||
<li>Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).</li>
|
||||
<li>Нужно отправить e-mail рассылку пользо­вателям.</li>
|
||||
<li>Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.</li>
|
||||
</ul>
|
||||
<p>Делать это через <tt>views.py</tt> (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом <tt>manage.py</tt> — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.</p>
|
||||
<h2>Как это работает?</h2>
|
||||
<p>Django использует систему «автообна­ружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, <tt>web</tt>):</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">web/
|
||||
├── __init__.py
|
||||
├── models.py
|
||||
└── management/ <-- 1. Создаем папку management
|
||||
├── __init__.py
|
||||
└── nginx/
|
||||
├── commands/ <-- 2. Внутри неё папку commands
|
||||
├── __init__.py
|
||||
└── my_cool_script.py <-- 3. Наш файл с командой</pre>
|
||||
<p>Файл <tt>my_cool_script.py</tt> автома­тически превратится в команду: <code>python manage.py my_cool_script</code>. Django будет искать его в папках <tt>management/commands/</tt> внутри каждого устано­вленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.</p>
|
||||
<p><strong>Для справки:</strong> Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.</p>
|
||||
<h2>Анатомия команды</h2>
|
||||
<p>Вот пример простейшей команды. Мы наследуемся от класса <code>BaseCommand</code> и переопре­деляем метод <code>handle</code>:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
# web/management/commands/hello.py
|
||||
from django.core.management.base import BaseCommand
|
||||
<h4>[0.1.6.post1] - 2026–03–19</h4>
|
||||
<h5>Изменено</h5>
|
||||
<ul>
|
||||
<li>Новый алгоритм «висячей пунктуации» (<em>HangingPunctuationProcessor</em>). Добавлены компенсирующие пробелы для висячих символов, чтобы избежать наложения на соседние слова. Теперь «висячие символы» (кавычки, скобки и т. п.) оборачиваются в <code><span></code> вместе с ближайшим словом и пробелом, что обеспечивает отсутствие визуальных смещений внутри строки. Режим <code>both</code> (для одновременного вывешивания в обе стороны) отключен из-за потенциальных конфликтов компенсирующих пробелов и проблем с выравниванием при использовании CSS <code>text-justify</code>.</span></li>
|
||||
</ul>
|
||||
<h4>Исправлено</h4>
|
||||
<ul>
|
||||
<li>Автоматическая замена символов <code>x</code>, <code>X</code>, <code>х</code>, <code>Х</code>, стоящих между числами, на знак умножения <code>×</code>, чтобы выражения вида <code>100x100</code> или <code>100 х 100` корректно обрабатывались и выглядели типографски правильными (<code>100×100</code> или <code>100&nbsp;×&nbsp;100</code>).</li>
|
||||
</ul>
|
||||
|
||||
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}!")</pre>
|
||||
<p>Запуск:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py hello Иван</pre>
|
||||
<p>Увидим в консоли:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
Привет, Иван!</pre>
|
||||
<h2>Реальный пример: Массовая типографика через etpgrf</h2>
|
||||
<p>В моём проекте, <a href="https://dq.cube2.ru/" target="_blank">DQ – коллекция цитат: место для вдумчивого чтения</a>, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типогра­фируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересо­хранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.</p>
|
||||
<p>Вот как я это реализовал. В <tt>web/management/commands/reprocess_typography.py</tt>:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
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Готово!"))</pre>
|
||||
<p>Возможно вы заметили, что в моем проекте для контента есть два поля: <code>szContent</code> и <code>szContentHTML</code>. В первом хранится «сырой» текст, во втором — результат типогра­фирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.</p>
|
||||
<p>Еще интересные фишки, которые исполь­зовались при типогра­фировании:</p>
|
||||
<ol>
|
||||
<li><code>self.stdout.write</code> вместо <code>print</code> – позволяет Django перехва­тывать вывод (например, для тестов) и корректно работать с кодировками.</li>
|
||||
<li><code>self.style.SUCCESS</code> и <code>self.style.ERROR</code> – раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.</li>
|
||||
<li>Аргумент <tt>--dry-run</tt> — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.</li>
|
||||
<li>Аргументы <tt>--limit</tt> и <tt>--offset</tt> — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными <tt>offset</tt>).</li>
|
||||
<li>Используется <code>update_fields</code> — позволяет переза­писывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.</li>
|
||||
</ol>
|
||||
<p>Теперь, чтобы типогра­фировать весь контент, нам достаточно одной строку в терминале, и Django сделает всю грязную работу за нас (не забудьте инициировать виртуальное окружение вашего проекта).</p>
|
||||
<p>Тестовый прогон (безопасно, ничего не сохраняет):</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py reprocess_typography --dry-run</pre>
|
||||
<p>Боевой запуск, с изменениями данные в базе:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
python manage.py reprocess_typography</pre>
|
||||
<p>Если у вас очень много записей, можно запускать по частям:</p>
|
||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||||
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</pre>
|
||||
## [0.1.6] - 2024-03-19
|
||||
### Изменено
|
||||
- Н
|
||||
###
|
||||
-
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small nowrap me-2">© Sergei Erjemin, 2025–{% now 'Y' %}.</span>
|
||||
<nobr class="text-muted small mx-2">
|
||||
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.7</a>
|
||||
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.8</a>
|
||||
</nobr>
|
||||
{# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
|
||||
...
|
||||
|
||||
Reference in New Issue
Block a user