add: Django Management Command для "склейки" корневых серий типового строительства

This commit is contained in:
2026-05-14 00:27:17 +03:00
parent 998e6caf8f
commit b27d6f0546
2 changed files with 414 additions and 8 deletions

View File

@@ -9,10 +9,11 @@
## Каталог команд ## Каталог команд
1. `generate_sitemaps` — оффлайн генерация sitemap-файлов. 1. `regenerate_seria_roots` — пересчет корневых серий (иерархия и консолидация).
2. `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`. 2. `generate_sitemaps` — оффлайн генерация sitemap-файлов.
3. `populate_seo_fields` — автозаполнение SEO-полей блога из существующих данных. 3. `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`.
4. `make_rating` — пересчёт рейтингов профилей и стеклопакетов методом Манна-Уитни. 4. `populate_seo_fields` — автозаполнение SEO-полей блога из существующих данных.
5. `make_rating` — пересчёт рейтингов профилей и стеклопакетов методом Манна-Уитни.
## Общие правила запуска ## Общие правила запуска
@@ -28,7 +29,210 @@ cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py <command> [args] poetry run python oknardia/manage.py <command> [args]
``` ```
## 1) Команда `generate_sitemaps` ## 1) Команда `regenerate_seria_roots`
Назначение:
- пересчитать корневые серии (root) для всей иерархии серий домов.
- консолидировать различные написания одной и той же серии в одну "корневую" серию.
### Контекст
На разных источниках данные о типовых сериях домов были записаны с разными орфографическими вариантами.
Например серия **II-57** могла быть обозначена как:
- `2-57` (цифра вместо кириллицы)
- `И-57` (кириллица И вместо латинской II)
- `П-57` (опечатка)
- и т.п.
При парсинге данных эти варианты могли оказаться в БД как отдельные серии, хотя на самом деле это одна и та же серия.
Функция `regenerate_seria_roots` связывает все эти варианты (алиасы) с одной **корневой** серией.
### Когда это нужно
При добавлении новых адресов и серий типового строительства, при ручном редактировании иерархии серий в админке,
при загрузке новых данных с разными орфографическими вариантами серий. Кроме того, если для уже существующих в базе
серий будут получены данные о типовых размерах оконных проёмов и типов квартирах (сейчас в базе около 4500 серий, из
них всего 31 корневых серий и 1957 серий с найденным корнем... ещё 2502 неописанных серии без корня, т.е. больше
половины, могут пополнить каталог Окнардии.
### Как это работает
1. **Этап 1**: Находит "корневые" серии (root series) — те, что реально используются в таблице `Apartment_Type`
(т.е. у которых есть квартиры). Для каждой такой серии устанавливает `kRoot_id = own_id`.
2. **Этап 2**: Для всех остальных серий:
- Движется вверх по дереву иерархии (`kParent_id`)
- Ищет корневую серию (ту, которая либо не имеет родителя, либо она в списке корневых)
- Устанавливает найденную корневую серию в поле `kRoot_id`
- Если не находит корневую серию → `kRoot_id = None`
### Пример структуры после обработки
```
Таблица: Seria_Info
id | sSeriaName | kParent_id | kRoot_id | Комментарий
----|------------|------------|-----------|-------------------
123 | II-57 | NULL | 123 | Корневая серия
124 | 2-57 | 123 | 123 | Алиас (через родителя)
125 | И-57 | 123 | 123 | Алиас (через родителя)
126 | П-57 | 123 | 123 | Алиас (через родителя)
```
### Базовый запуск
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_roots
```
### Параметры запуска
**`--verbosity 0`** — только ошибки (минимум информации):
```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 0
```
**`--verbosity 1`** — точки/плюсы (стандартный режим):
```bash
poetry run python oknardia/manage.py regenerate_seria_roots
# или явно
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 1
```
**`--verbosity 2`** — подробный вывод (названия серий):
```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 2
```
**`--verbosity 3`** — очень подробный вывод в виде таблицы:
```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 3
```
### Примеры вывода
**Verbosity 0 (только ошибки - чистый вывод):**
```
✅ Пересчет завершен! Время: 2.18с
```
**Verbosity 1 (магический режим - точки и символы):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
...............................
Этап 2: Главная магия - обрабатываем все серии в иерархии...
-----++..--.++--...--.+.++++.-+++++++-.-.++.-+--++--++++-+++++++-++.+--.+++-+-..++++++-++++++++++--.-++++++-++++
+--+-+++++++++-++++++++++-++++--+++.+++--+++++++++++++++++++---++.+-+-+++++-++++++++++-+++-+----+.-+++-+--++++++
+++----+--+-+++++-+--+++--+++-.+++++-++++++++-+---++-+++++---+++------++----++-+--++----+--++--++++--+++++++++++
+++++++-------++++---+++-[... очень много символов ...]
=== РЕЗУЛЬТАТЫ ===
✓ Корневых серий (обработаны на этапе 1): 31
✓ Серий с найденным корнем: 1957
⚠ Серий без корня: 2502
✅ Пересчет завершен! Время: 2.18с
```
**Легенда магического режима:**
- `.` = корневая серия (обработана на этапе 1)
- `+` = серия с найденным корнем
- `-` = серия без найденного корня
- `E` = ошибка при обработке
**Verbosity 2 (подробный):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
✓ 0008 П-44
✓ 0009 П-3
✓ 0012 II-49
✓ 0017 КОПЭ
...
Этап 2: Главная магия - обрабатываем все серии в иерархии...
0001: Нет корня
0002: Нет корня
0006: корень → 12
0007: корень → 9
0008: корневая
...
```
**Verbosity 3 (очень подробный - таблица):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
✓ 0008 | П-44 | корневая
✓ 0009 | П-3 | корневая
✓ 0012 | II-49 | корневая
...
Этап 2: Главная магия - обрабатываем все серии в иерархии...
--------------------------------------------------------------------------------------------------------------
ID | Название | Родитель | Путь | Результат
--------------------------------------------------------------------------------------------------------------
...
...
2565 | Г-ЗИ | 9 | 9 | ✓ Корень #9
2566 | I-528КП-809/69 | - | (нет) | ✗ Нет корня
2567 | 464Д-0154 | 3339 | 3339 → 963 → 375 | ✓ Корень #375
2568 | УЛГ-507-4/64 | 2105 | 2105 | ✓ Корень #2105
2569 | ЛГ-507-4 | 2105 | 2105 | ✓ Корень #2105
2570 | 1ЛГ-600-И-1 | - | (нет) | ✗ Нет корня
2571 | 464Д-0154 Новополоцкого ДСК | 3339 | 3339 → 963 → 375 | ✓ Корень #375
2572 | 121-0142,13,87 | - | (нет) | ✗ Нет корня
...
...
--------------------------------------------------------------------------------------------------------------
=== РЕЗУЛЬТАТЫ ===
✓ Корневых серий (обработаны на этапе 1): 31
✓ Серий с найденным корнем: 1957
⚠ Серий без корня: 2502
✅ Пересчет завершен! Время: 2.18с
```
### Когда запускать
- **После первого развертывания** — консолидировать иерархию серий.
- **После ручного редактирования иерархии** (добавления родитель-потомков в админку).
- **После загрузки новых данных** с разными орфографическими вариантами серий.
- **По расписанию** (опционально, например раз в месяц):
```bash
0 2 * * 1 cd /home/user/app-path/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_roots >> /var/log/oknardia-seria-roots.log 2>&1
```
### Откат и безопасность
- **Безопасна для повторного запуска** — просто пересчитывает все `kRoot_id`.
- **Откат через SQL** — если нужно очистить поле (перед запуском рекомендуется бэкап):
```sql
UPDATE oknardia_seria_info SET kRoot_id = NULL;
```
- **Проверка результатов** — после запуска можно проверить:
```bash
poetry run python oknardia/manage.py shell -c "
from oknardia.models import Seria_Info
count_null = Seria_Info.objects.filter(kRoot_id__isnull=True).count()
count_with_root = Seria_Info.objects.filter(kRoot_id__isnull=False).count()
print(f'Серий без корня: {count_null}')
print(f'Серий с корнем: {count_with_root}')
"
```
## 2) Команда `generate_sitemaps`
Назначение: Назначение:
- пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`. - пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`.
@@ -76,7 +280,7 @@ location = /sitemap.xml {
} }
``` ```
## 2) Команда `regenerate_seria_prerender` ## 3) Команда `regenerate_seria_prerender`
Назначение: Назначение:
- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`. - пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`.
@@ -114,7 +318,7 @@ poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 -
- после массового обновления данных серий/окон/квартир; - после массового обновления данных серий/окон/квартир;
- после очистки `seria_info/prepared/`. - после очистки `seria_info/prepared/`.
## 3) Команда `populate_seo_fields` ## 4) Команда `populate_seo_fields`
Назначение: Назначение:
- автозаполнить SEO-поля (`sSlug`, `sMetaDescription`, `sMetaKeywords`) для всех существующих записей блога. - автозаполнить SEO-поля (`sSlug`, `sMetaDescription`, `sMetaKeywords`) для всех существующих записей блога.
@@ -226,7 +430,7 @@ print(f'Пусто sMetaKeywords: {posts.filter(sMetaKeywords=\"\").count()}')
- ✅ **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';` - ✅ **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';`
- ✅ **Всегда используй `--dry-run`** перед первым запуском для проверки. - ✅ **Всегда используй `--dry-run`** перед первым запуском для проверки.
## 4) Команда `make_rating` ## 5) Команда `make_rating`
Назначение: Назначение:
- пересчитать рейтинги оконных профилей, стеклопакетов и наборов услуг используя адаптированный метод Манна-Уитни (Mann-Whitney U Step Rank). - пересчитать рейтинги оконных профилей, стеклопакетов и наборов услуг используя адаптированный метод Манна-Уитни (Mann-Whitney U Step Rank).

View File

@@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
"""
Django management command: regenerate_seria_roots
Пересчет корневых серий (root series) для всех серий домов.
Идея:
Серии домов могут иметь сложную иерархию (дерево потомственности) из-за того,
что на разных сайтах одна и та же серия обозначалась по-разному.
Например серия II-57 записывалась как 2-57, И-57, П-57 и т.п.
Эта команда:
1. Находит "корневые" серии - те, что используются в Apartment_Type (у них есть квартиры)
2. Для всех остальных серий ищет их корневую серию, двигаясь вверх по дереву иерархии
3. Устанавливает найденную корневую серию в поле kRoot_id каждой серии
Результат:
- Все серии-алиасы (синонимы) указывают на одну корневую серию
- Это позволяет консолидировать данные по сериям дома
"""
from django.core.management.base import BaseCommand
from oknardia.models import Seria_Info, Apartment_Type
class Command(BaseCommand):
help = 'Пересчитывает корневые серии для всей иерархии серий домов'
def add_arguments(self, parser):
# Django уже добавляет --verbosity автоматически
# 0 = минимум (только ошибки)
# 1 = нормально (точки)
# 2 = подробно (информация)
# 3 = очень подробно (таблица)
pass
def handle(self, *args, **options):
verbose = int(options.get('verbosity', 1))
self.stdout.write(self.style.SUCCESS('=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===\n'))
time_start = self.get_time()
# ========== ЭТАП 1: Находим корневые серии ==========
if verbose >= 1:
self.stdout.write('Этап 1: Ищем корневые серии в таблице квартир...\n')
# Получаем все УНИКАЛЬНЫЕ серии, которые используются в квартирах
root_series_ids = list(
Apartment_Type.objects.values_list('kSeria_id', flat=True).distinct()
)
# Получаем объекты корневых серий (для вывода их названий)
root_series = Seria_Info.objects.filter(id__in=root_series_ids)
if verbose >= 1:
self.stdout.write(f'✓ Найдено корневых серий: {len(root_series_ids)}\n')
# Устанавливаем для корневых серий kRoot_id = own_id
for seria in root_series:
seria.kRoot_id = seria.id # Серия сама себе корень
seria.save()
if verbose >= 3:
# Очень подробный вывод - таблица
self.stdout.write(
f'{seria.id:04d} | {seria.sName:<30} | корневая'
)
elif verbose >= 2:
# Подробный - с названием
self.stdout.write(f'{seria.id:04d} {seria.sName}')
elif verbose >= 1:
# Нормально - точки
self.stdout.write('.', ending='')
if verbose < 2:
self.stdout.write('\n')
self.stdout.write('')
# ========== ЭТАП 2: Обрабатываем все серии ==========
if verbose >= 1:
self.stdout.write('\nЭтап 2: Главная магия - обрабатываем все серии в иерархии...\n')
if verbose >= 3:
# Заголовок таблицы для очень подробного режима
self.stdout.write('-' * 110)
self.stdout.write(
f'{"ID":>5} | {"Название":<35} | {"Родитель":>10} | '
f'{"Путь":<40} | {"Результат":<20}'
)
self.stdout.write('-' * 110)
all_series = Seria_Info.objects.all()
count_with_root = 0
count_without_root = 0
count_errors = 0
count_root = 0
for seria in all_series:
try:
# Если это уже корневая серия, пропускаем
if seria.id in root_series_ids:
count_root += 1
if verbose >= 3:
self.stdout.write(
f'{seria.id:>5} | {seria.sName:<35} | '
f'{str(seria.kParent_id or "-"):>10} | '
f'(корневая) | ✓'
)
elif verbose >= 2:
self.stdout.write(f' {seria.id:04d}: корневая')
elif verbose >= 1:
self.stdout.write('.', ending='')
continue
# Движемся вверх по дереву потомок → предок
current_id = seria.kParent_id
path_trace = []
# Рекурсивно ищем корневую серию
while current_id is not None:
path_trace.append(current_id)
try:
parent_seria = Seria_Info.objects.get(id=current_id)
# Проверяем: либо у родителя нет родителя, либо родитель - корневая
if parent_seria.kParent_id is None or current_id in root_series_ids:
break
current_id = parent_seria.kParent_id
except Seria_Info.DoesNotExist:
current_id = None
break
# Проверяем, что нашли корневую серию
if current_id and current_id in root_series_ids:
seria.kRoot_id = current_id
seria.save()
if verbose >= 3:
path_str = ''.join(str(i) for i in path_trace)
self.stdout.write(
f'{seria.id:>5} | {seria.sName:<35} | '
f'{str(seria.kParent_id or "-"):>10} | '
f'{path_str:<40} | ✓ Корень #{current_id}'
)
elif verbose >= 2:
self.stdout.write(f' {seria.id:04d}: корень → {current_id}')
elif verbose >= 1:
self.stdout.write('+', ending='')
count_with_root += 1
else:
seria.kRoot_id = None
seria.save()
if verbose >= 3:
path_str = ''.join(str(i) for i in path_trace) if path_trace else '(нет)'
self.stdout.write(
f'{seria.id:>5} | {seria.sName:<35} | '
f'{str(seria.kParent_id or "-"):>10} | '
f'{path_str:<40} | ✗ Нет корня'
)
elif verbose >= 2:
self.stdout.write(f' {seria.id:04d}: Нет корня')
elif verbose >= 1:
self.stdout.write('-', ending='')
count_without_root += 1
except Exception as e:
if verbose >= 3:
self.stdout.write(
self.style.ERROR(f'{seria.id:>5} | ОШИБКА: {str(e):<80}')
)
elif verbose >= 1:
self.stdout.write('E', ending='')
count_errors += 1
if verbose >= 3:
self.stdout.write('-' * 110)
elif verbose < 2:
self.stdout.write('\n')
# ========== РЕЗУЛЬТАТЫ ==========
self.stdout.write(self.style.SUCCESS('\n\n=== РЕЗУЛЬТАТЫ ==='))
self.stdout.write(f'✓ Корневых серий (обработаны на этапе 1): {count_root}')
self.stdout.write(f'✓ Серий с найденным корнем: {count_with_root}')
self.stdout.write(f'⚠ Серий без корня: {count_without_root}')
if count_errors > 0:
self.stdout.write(self.style.ERROR(f'✗ Ошибок: {count_errors}'))
time_elapsed = self.get_time() - time_start
self.stdout.write(
self.style.SUCCESS(f'\n✅ Пересчет завершен! Время: {time_elapsed:.2f}с')
)
@staticmethod
def get_time():
import time
return time.perf_counter()