add: management-команда regenerate_seria_prerender для оффлайн пересборки pre-render шаблонов серий; обновлены SETUP.md, README.md, MANAGEMENT_RUNBOOK.md

This commit is contained in:
2026-04-22 01:24:03 +03:00
parent 330737878e
commit 851babdda2
5 changed files with 312 additions and 7 deletions

175
MANAGEMENT_RUNBOOK.md Normal file
View File

@@ -0,0 +1,175 @@
# MANAGEMENT_RUNBOOK.md
Единый runbook по management-командам проекта.
Документ отвечает на 3 вопроса:
- что запускать;
- когда запускать;
- как безопасно откатываться/повторять запуск.
## Каталог команд
1. `generate_sitemaps` — оффлайн генерация sitemap-файлов.
`regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`.
## Общие правила запуска
- Запускать команды из корня репозитория.
- Для локального/CI запуска использовать `poetry`.
- Не запускать тяжелые операции через HTTP-эндпоинты `/service/*`.
- Перезапуск веб-сервера (`gunicorn`/`uWSGI`) делать отдельным шагом оркестрации, а не из кода Django.
Базовый шаблон запуска:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py <command> [args]
```
## 1) Команда `generate_sitemaps`
Назначение:
- пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`.
Базовый запуск:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py generate_sitemaps
```
Запуск с параметрами:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py generate_sitemaps \
--compare-min-depth 2 \
--compare-max-depth 4 \
--max-items 40000 \
--max-file-size 5242880 \
--max-files-qty 998
```
Когда запускать:
- после деплоя;
- по расписанию (cron/systemd timer);
- после крупных изменений данных каталога/блога.
### Важные замечания
Чтобы `sitemap.xml` отдавал прокси-nginx напрямую из файловой системы, нужно, чтобы он физически лежал
в `MEDIA_ROOT/_serv_sitemap/sitemap.xml`.
Допустимо, что файл доступен по двум URL (корневой и media), но в `robots.txt` должен быть указан один
канонический вариант `sitemap.xml`
#### NGINX snippet (alias для корневого sitemap)
```nginx
# Корневой sitemap.xml (для привычного для поисковиков URL)
location = /sitemap.xml {
alias /<путь-к-каталогку-с-докер-приложением>/media/_serv_sitemap/sitemap.xml;
default_type application/xml;
add_header Cache-Control "public, max-age=300";
}
```
## 2) Команда `regenerate_seria_prerender`
Назначение:
- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`.
Проверка без записи файлов:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --dry-run
```
Пересборка только отсутствующих файлов:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender
```
Принудительная пересборка всех root-серий:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --force
```
Выборочная пересборка:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force
```
Когда запускать:
- после обновления логики `catalog_seria_info`;
- после массового обновления данных серий/окон/квартир;
- после очистки `seria_info/prepared/`.
## Оркестрация и reload веб-сервера
Важно:
- reload веб-сервера не встроен в management-команды;
- это отдельная операция окружения.
Пример для systemd + gunicorn:
```bash
sudo systemctl reload gunicorn
```
Рекомендуемый batch-сценарий:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --force
poetry run python oknardia/manage.py generate_sitemaps
sudo systemctl reload gunicorn
```
## Cron/systemd timer (пример)
Пример cron (раз в сутки в 03:20):
```bash
20 3 * * * cd /Users/e-serg/PRJ/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_prerender --force && poetry run python oknardia/manage.py generate_sitemaps >> /var/log/oknardia-maintenance.log 2>&1
```
Если нужен reload после batch, добавляй отдельной строкой/шагом оркестратора.
## Диагностика
Быстрая проверка конфигурации:
```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py check
```
Типовые причины проблем:
- нет прав записи в директории `templates/seria_info/prepared` или `MEDIA_ROOT/_serv_sitemap`;
- устаревшее виртуальное окружение / неустановленные зависимости;
- запуск не из того каталога.
## План миграции `/service/*` -> management commands
Текущее направление:
- все тяжелые и административные операции переносить из HTTP в management-команды;
- `/service/*` оставлять только как thin UI/мониторинг или убрать полностью.
Кандидаты на перенос:
- действия из `service.py` (`/service/make_rating`, sitemap/служебные задачи и т.п.);
- любые операции, которые могут идти дольше обычного web-request.
---
См. также:
- `SETUP.md`
- `README.md`

View File

@@ -15,7 +15,7 @@
* Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`. * Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`.
* Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`. * Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`.
* Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии). * Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии).
* * Добавлена management-команда `regenerate_seria_prerender` для оффлайн-пересборки pre-render шаблонов `catalog_seria_info` (все или выбранные root-серии), с режимами `--dry-run` и `--force`; серверный reload (Gunicon? uWSGI или что там еще будет) должен быть вынесен из кода приложения в оркестрацию (cron/systemd/deploy step).
* *
* *
* *
@@ -35,8 +35,7 @@
* [`AGENTS.md`](AGENTS.md) контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии). * [`AGENTS.md`](AGENTS.md) контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии).
* [`SETUP.md`](SETUP.md) пошаговая настройка окружения, запуск проекта и базовые команды разработки. * [`SETUP.md`](SETUP.md) пошаговая настройка окружения, запуск проекта и базовые команды разработки.
* Сервисные утилиты: * [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) единый runbook по management-командам и batch-операциям.
- [`SITEMAP_RUNBOOK.md`](SITEMAP_RUNBOOK.md) sitemap (генерация, веса, cron, nginx)

View File

@@ -233,6 +233,26 @@ python manage.py remove_stale_contenttypes # Удалить устаревши
# Служебные # Служебные
python manage.py check # Проверить конфигурацию python manage.py check # Проверить конфигурацию
python manage.py check --deploy # Проверка для продакшена python manage.py check --deploy # Проверка для продакшена
python manage.py generate_sitemaps # Оффлайн генерация sitemap XML
python manage.py regenerate_seria_prerender --dry-run # Проверка пересборки pre-render шаблонов серий
python manage.py regenerate_seria_prerender --force # Принудительная пересборка pre-render шаблонов серий
```
### Пересборка pre-render шаблонов серий (рекомендуемый сценарий)
Шаблоны для `catalog_seria_info` пересобираются оффлайн management-командой, без reload из кода Django.
```bash
cd /path/to/project
poetry run python oknardia/manage.py regenerate_seria_prerender --force
# затем (опционально) один внешний reload процесса приложения, если это требуется вашей конфигурацией
# sudo systemctl reload gunicorn
```
Для выборочной пересборки используйте `--seria-id` несколько раз:
```bash
poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force
``` ```
## 📚 Дополнительные ресурсы ## 📚 Дополнительные ресурсы
@@ -240,7 +260,6 @@ python manage.py check --deploy # Проверка для продак
- [Django документация](https://docs.djangoproject.com/en/stable/) - [Django документация](https://docs.djangoproject.com/en/stable/)
- [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта - [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта
- [README.md](./README.md) — основная информация о проекте - [README.md](./README.md) — основная информация о проекте
- [SECURITY_AUDIT_REPORT.md](./SECURITY_AUDIT_REPORT.md) — отчёт безопасности
## ❓ Решение проблем ## ❓ Решение проблем

View File

@@ -14,7 +14,7 @@ from oknardia.models import (
Building_Info, Building_Info,
) )
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
from web.add_func import get_flaps_for_big_pictures, touch_reload_wsgi from web.add_func import get_flaps_for_big_pictures
import time import time
import os import os
import math import math
@@ -217,7 +217,6 @@ def catalog_seria_info(
string_prerender = render_to_string("seria_info/all_seria_info_pre_light.html", to_template) string_prerender = render_to_string("seria_info/all_seria_info_pre_light.html", to_template)
with open(light_template_w_path, "w", encoding="utf-8") as file: with open(light_template_w_path, "w", encoding="utf-8") as file:
file.write(string_prerender) file.write(string_prerender)
touch_reload_wsgi(light_template_w_path)
else: else:
to_template.update({"THIS_SERIA_NAME": q_seria.sName}) to_template.update({"THIS_SERIA_NAME": q_seria.sName})
@@ -346,7 +345,7 @@ def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> d
жилые/муниципальные/государственные площади, число жителей, квартир, жилые/муниципальные/государственные площади, число жителей, квартир,
лицевых счетов и диапазон показателя состояния домов. лицевых счетов и диапазон показателя состояния домов.
:param seria_id: int | str -- id серии, для которой нужно получить данные :param seria_id: int | str -- id серии, для которой нужно получить данные.
:return: dict -- { :return: dict -- {
"DATA4GEO": [...], "DATA4GEO": [...],
"MUNICIPAL_M2": ..., "MUNICIPAL_M2": ...,

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from pathlib import Path
import pytils
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db.models import F
from django.test import RequestFactory
from oknardia.models import Seria_Info
from web import catalog_series
class Command(BaseCommand):
"""Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all<ID>)."""
help = "Пересоздает pre-render шаблоны catalog_seria_info для выбранных или всех корневых серий."
def add_arguments(self, parser):
parser.add_argument(
"--seria-id",
type=int,
action="append",
default=[],
help="ID серии (можно передавать несколько раз). По умолчанию пересоздаются все корневые серии.",
)
parser.add_argument(
"--force",
action="store_true",
help="Пересоздать даже если pre-render файл уже существует.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Только показать, что будет сделано, без генерации файлов.",
)
def handle(self, *args, **options):
seria_ids: list[int] = options["seria_id"]
force: bool = options["force"]
dry_run: bool = options["dry_run"]
# Берем только корневые серии, потому что для них строятся канонические URL /all<ID>.
query = Seria_Info.objects.filter(id=F("kRoot_id")).only("id", "sName").order_by("id")
if seria_ids:
query = query.filter(id__in=seria_ids)
targets = list(query)
if not targets:
raise CommandError("Не найдено подходящих корневых серий для пересоздания pre-render.")
templates_root = Path(settings.TEMPLATES[0]["DIRS"][0])
prepared_dir = templates_root / settings.PATH_FOR_SERIA_INFO_HTML_INCLUDE
prepared_dir.mkdir(parents=True, exist_ok=True)
request_factory = RequestFactory()
created = 0
planned = 0
skipped = 0
for seria in targets:
target_file = prepared_dir / f"{seria.id}_id.html"
if target_file.exists() and not force:
skipped += 1
self.stdout.write(f"SKIP {seria.id}: {target_file}")
continue
if dry_run:
action = "REGEN" if target_file.exists() else "CREATE"
self.stdout.write(f"{action} {seria.id}: {target_file}")
planned += 1
continue
if target_file.exists():
target_file.unlink()
slug = pytils.translit.slugify(seria.sName)
request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}")
# В команде принудительно включаем «production-mode» для вьюхи,
# чтобы она прошла тяжелую ветку и пересоздала pre-render файл.
old_debug = catalog_series.DEBUG
try:
catalog_series.DEBUG = False
response = catalog_series.catalog_seria_info(request, slug, seria.id)
finally:
catalog_series.DEBUG = old_debug
if response.status_code != 200:
raise CommandError(
f"Серия {seria.id}: ожидался status=200, получен {response.status_code}."
)
if not target_file.exists():
raise CommandError(f"Серия {seria.id}: pre-render файл не создан: {target_file}")
created += 1
self.stdout.write(self.style.SUCCESS(f"OK {seria.id}: {target_file}"))
if dry_run:
self.stdout.write(
self.style.SUCCESS(
f"DRY-RUN. Обработано: {len(targets)}. Будет создано/пересоздано: {planned}. Пропущено: {skipped}."
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created}. Пропущено: {skipped}."
)
)