From cb9dab9e56e06aadfac3f925d03fad620de207fc Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 19 May 2026 22:45:56 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BF=D1=80=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D0=B5=D1=80-=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D1=8B?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BB=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B2=20prod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CACHE_PRERENDER_SYSTEM.md | 361 ++++++++++++++++++ MANAGEMENT_RUNBOOK.md | 58 ++- README.md | 30 +- .../seria_info/all_seria_info_pre_light.html | 84 ++-- ...seria_info_pre_light_dynamic_include.html} | 9 +- ...all_seria_info_pre_light_static_flaps.html | 21 + ...all_seria_info_pre_light_static_graph.html | 55 +++ ...seria_info_pre_light_static_map_stats.html | 93 +++++ oknardia/web/catalog_series.py | 110 +++++- .../commands/regenerate_seria_prerender.py | 59 ++- 10 files changed, 785 insertions(+), 95 deletions(-) create mode 100644 CACHE_PRERENDER_SYSTEM.md rename oknardia/templates/seria_info/{all_seria_info_pre_light_include.html => all_seria_info_pre_light_dynamic_include.html} (74%) create mode 100644 oknardia/templates/seria_info/all_seria_info_pre_light_static_flaps.html create mode 100644 oknardia/templates/seria_info/all_seria_info_pre_light_static_graph.html create mode 100644 oknardia/templates/seria_info/all_seria_info_pre_light_static_map_stats.html diff --git a/CACHE_PRERENDER_SYSTEM.md b/CACHE_PRERENDER_SYSTEM.md new file mode 100644 index 0000000..ec0ebe9 --- /dev/null +++ b/CACHE_PRERENDER_SYSTEM.md @@ -0,0 +1,361 @@ +# Система двухуровневого кеширования страниц серий + +## 📖 Описание + +Система страниц серий домов использует **двухуровневое кеширование**: + +1. **Статический кеш** — дорогостоящие данные (карты, графики, схемы), генерируются один раз и сохраняются на диск +2. **Динамические данные** — верхняя статья (редактируется через админку) и таблица оконных проёмов (показывает свежие предложения), пересчитываются при каждом запросе + +--- + +## 🏗️ Архитектура кеша + +Для каждой серии создаются **3 отдельных кешируемых файла**: + +| Файл | Содержимое | Размер | Где появляется | +|------|-----------|--------|---| +| `{seria_id}_id_static_flaps.html` | Схемы открывания и типовые размеры окон | 4-7KB | В разделе "Дома серии: типовые размеры" | +| `{seria_id}_id_static_graph.html` | Google Charts: график ввода в эксплуатацию | 2-3KB | В контейнере height:300px | +| `{seria_id}_id_static_map_stats.html` | Yandex Maps + блок статистики | 6-46KB | Карта (col-md-7) + статистика (col-md-4) | + +### Верхняя статья — ДИНАМИЧЕСКИЕ ДАННЫЕ, не кешируется! + +**Верхняя статья про серию** (`THIS_SERIA_DESCRIPTION`) — это динамические данные, которые: +- Хранятся в БД (поле `sDescription` модели `Seria_Info`) +- **Редактируются через админку** → нужны изменения **без перезагрузки контейнера** +- Рендерятся всегда из БД, не сохраняются в кеш-файлы + +Если бы мы кешировали верхнюю статью: +- Админ редактирует статью → кеш-файл не обновляется автоматически +- Нужно перезагрузить контейнер или вручную удалять файл кеша +- При регенерации кеша может произойти перезапись старыми данными + +**Решение**: верхняя статья **всегда рендерится из БД**, как и таблица окон. + +--- + +## 🔄 Логика работы + +### При первом запросе seriesId (cache miss): + +1. View `catalog_seria_info()` обнаруживает отсутствие кеш-файлов +2. Вычисляются дорогостоящие данные: + - Геокоординаты всех зданий серии + - Год ввода в эксплуатацию (для графика) + - Схемы открывания окон +3. Генерируются **3 отдельных файла** в `oknardia/templates/seria_info/prepared/` +4. Верхняя статья и таблица окон **рендерятся из БД** при каждом запросе +5. Контекст получает пути к 3 кеш-файлам + свежие динамические данные +6. Main-шаблон включает 3 кеш-файла + 2 динамических блока +7. Ответ отправляется пользователю + +### При последующих запросах (cache hit): + +1. View обнаруживает, что все 3 файла существуют +2. **Верхняя статья пересчитывается заново** из БД (от админа) +3. **Таблица окон пересчитывается заново** (новые предложения видны сразу) +4. Main-шаблон включает 3 статических файла + 2 динамических блока +5. Ответ отправляется быстро (схемы, график, геоданные из кеша) + +### Ключевой мюмент: Всё свежее! + +- Кеширование **не трогает** верхнюю статью и таблицу окон → они всегда свежие +- Новые цены от поставщиков видны пользователям **сразу** +- Админ может редактировать текст про серию → видно **без перезагрузки** контейнера +- Таблица пересчитывается в запросе через Q-фильтры к БД (есть индексы на БД) + +--- + +## 📁 Структура файлов + +### Templates (шаблоны) + +``` +oknardia/templates/seria_info/ +├── all_seria_info_pre_light.html # Main-шаблон (включает 3 static-файла + динамику) +├── all_seria_info_pre_light_static_flaps.html # ШАГ 1: Схемы открывания (кешируется) +├── all_seria_info_pre_light_static_graph.html # ШАГ 2: График ввода (кешируется) +├── all_seria_info_pre_light_static_map_stats.html # ШАГ 3: Карта + статистика (кешируется) +└── prepared/ # Директория с кеш-файлами + ├── 210_id_static_flaps.html + ├── 210_id_static_graph.html + ├── 210_id_static_map_stats.html + ├── 100_id_static_flaps.html + └── ... (93 файла: 31 серия × 3 типа) +``` + +### Генерация кеша (Web-логика) + +``` +oknardia/web/ +├── catalog_series.py # catalog_seria_info() — генерирует 3 файла + динамику +└── management/commands/ + └── regenerate_seria_prerender.py # Batch-команда для регенерации всех серий +``` + +--- + +## 🚀 Управление кешем + +### Разработка (DEV-режим) + +```bash +python manage.py runserver +``` + +В DEV-режиме (`DEBUG = True`): +- Кеш **не используется** (всегда `PRE_RENDERED_STATIC_*_PATH = ""`) +- Main-шаблон рендерит данные напрямую +- Удобно для разработки (вижу изменения сразу) + +### Production (PROD-режим) + +#### Первоначальная генерация (все 31 серия): + +```bash +python manage.py regenerate_seria_prerender +``` + +Вывод: +``` +OK seria 100: 3 кеш-файла созданы +OK seria 12: 3 кеш-файла созданы +... +Готово. Обработано: 31. Создано/пересоздано: 31 × 3 файла. Пропущено: 0. +``` + +#### Регенерация конкретной серии: + +```bash +python manage.py regenerate_seria_prerender --seria-id 210 +``` + +#### Dry-run (без создания файлов): + +```bash +python manage.py regenerate_seria_prerender --dry-run +``` + +#### Force-переписать даже если есть кеш: + +```bash +python manage.py regenerate_seria_prerender --force +``` + +--- + +## 📊 Когда нужна регенерация кеша? + +### ✅ Регенерируйте, если: + +- Изменены координаты зданий (geo-данные) +- Добавлены новые здания в серию +- Обновлены годы ввода в эксплуатацию (для графика) +- Изменены схемы открывания окон +- Обновлена верхняя статья про серию + +```bash +# Все изменилось → перестроить все +python manage.py regenerate_seria_prerender --force + +# Изменилась одна серия → перестроить одну +python manage.py regenerate_seria_prerender --seria-id 210 +``` + +### ❌ НЕ нужна регенерация, если: + +- Добавлены новые **предложения** (цены от поставщиков) +- Обновлены **наличие/status** существующих предложений +- Система просто должна **показать свежие цены** + +Таблица окон пересчитывается при каждом запросе автоматически! 🎉 + +--- + +## 🔌 Интеграция с Docker + +В контейнере (PROD-режим): + +```dockerfile +# Генерируем кеш пр�� запуске +RUN python manage.py regenerate_seria_prerender + +# Запускаем сервер +CMD ["gunicorn", "oknardia.wsgi:application", "--bind", "0.0.0.0:8000"] +``` + +Кеш-файлы сохраняются на диск в томе, поэтому переживают перезагрузку контейнера. + +--- + +## 📝 Контекстные переменные + +### При первом запросе (происходит генерация): + +View `catalog_seria_info()` отправляет в шаблон: + +```python +to_template = { + "THIS_SERIA_ID": seria_id, # ID серии (210) + "THIS_SERIA_NAME": q_seria.sName, # Название ("1-335") + "THIS_SERIA_DESCRIPTION": html_description, # Верхняя статья + "FLAP_DIM": flap_dimensions, # Массив схем открывания + "DATA4GRAPH": graph_data, # Годы и кол-во домов + "DATA4GEO": geo_data, # Координаты зданий + "ACCOUNTS": buildings_count, # Кол-во квартир + "APARTMENTS": families_count, # Кол-во семей + "RESIDENTIAL_M2": total_area, # Площадь жилая + # ... и другие +} +``` + +### Для шаблонов генерации кеша: + +Каждый template файл получает **все** эти переменные и рендерится отдельно. + +### Для main-шаблона: + +Main-шаблон получает пути только к 3 кеш-файлам: + +```python +to_template.update({ + "PRE_RENDERED_STATIC_FLAPS_PATH": "seria_info/prepared/210_id_static_flaps.html", + "PRE_RENDERED_STATIC_GRAPH_PATH": "seria_info/prepared/210_id_static_graph.html", + "PRE_RENDERED_STATIC_MAP_STATS_PATH": "seria_info/prepared/210_id_static_map_stats.html", +}) + +# Верхняя статья всегда передается как THIS_SERIA_DESCRIPTION и рендерится динамически +to_template.update({ + "THIS_SERIA_DESCRIPTION": html_description, # Из БД, не кешируется +}) +``` + +Main-шаблон использует `{% include %}` для 3 кеш-файлов, а верхнюю статью рендерит напрямую: `{{ THIS_SERIA_DESCRIPTION|safe }}`. + +--- + +## 🎯 Примеры использования + +### Добавили новое здание в серию 210 + +```bash +# Обновили БД через Django ORM/admin +python manage.py shell +>>> from oknardia.models import Building_Info +>>> Building_Info.objects.create(...) + +# Регенерируем кеш только этой серии +python manage.py regenerate_seria_prerender --seria-id 210 + +# Пользователи видят новое здание на карте и в таблице +``` + +### Обновили года ввода в эксплуатацию + +```bash +# Изменили данные +python manage.py shell +>>> from oknardia.models import Building_Info +>>> Building_Info.objects.filter(...).update(...) + +# Кеш станет невалидным — нужна регенерация +python manage.py regenerate_seria_prerender --force + +# Пользователи видят обновленный график +``` + +### Добавили новое предложение (цену) + +```bash +# PriceOffer.objects.create(...) → система добавляет новое предложение +# НЕ нужна регенерация! + +# Таблица окон обновилась сама на следующем запросе +# Пользователи видят новое предложение сразу +``` + +--- + +## 🐛 Отладка + +### Проверить, какие кеш-файлы существуют: + +```bash +ls -lah oknardia/templates/seria_info/prepared/ +``` + +### Вручную удалить кеш (для тестирования): + +```bash +# Удалить кеш одной серии +rm oknardia/templates/seria_info/prepared/210*.html + +# Удалить ВСЕ кеш-файлы +rm oknardia/templates/seria_info/prepared/*_id_static_*.html +``` + +### Проверить содержимое кеш-файла: + +```bash +cat oknardia/templates/seria_info/prepared/210_id_static_graph.html | head -20 +cat oknardia/templates/seria_info/prepared/210_id_static_flaps.html | head -30 +cat oknardia/templates/seria_info/prepared/210_id_static_map_stats.html | head -50 +``` + +### Логирование: + +В `catalog_series.py` логируем создание файлов: + +```python +logger.info(f"Cache created: {file_flaps}") +logger.info(f"Cache created: {file_graph}") +logger.info(f"Cache created: {file_map_stats}") +# file_upper НЕ создается — верхняя статья рендерится динамически +``` + +--- + +## 📈 Производительность + +### Без кеша (DEV-режим): + +- ~2-3 сек на запрос (вычисляются geo, graph, flaps) +- Каждый запрос трогает БД +- Удобно для разработки + +### С кешем (PROD-режим): + +- ~100-300 мс на первый запрос (генерируется кеш) +- ~50-100 мс на последующие (включаются файлы + динамическая таблица) +- Статические данные из кеша (очень быстро) +- Таблица окон кешируется на уровне DB-запроса (индексы работают) + +--- + +## ✅ Чек-лист для администратора + +При развертывании в production: + +- [ ] Запустить `python manage.py regenerate_seria_prerender` (сгенерировать все 93 файла) +- [ ] Проверить размеры файлов в `prepared/` (~100-200 KB всего) +- [ ] Протестировать на локалхосте `DEBUG = False` +- [ ] Проверить, что таблица окон обновляется при добавлении новых предложений +- [ ] Настроить логирование создания кеша в продакшене + +--- + +## 🔗 Близкие компоненты + +- **Main-шаблон**: `oknardia/templates/seria_info/all_seria_info_pre_light.html` +- **Динамическая таблица**: `oknardia/templates/seria_info/all_seria_info_pre_light_dynamic_include.html` +- **View**: `oknardia/web/catalog_series.py::catalog_seria_info()` +- **Management-команда**: `oknardia/web/management/commands/regenerate_seria_prerender.py` +- **Тесты**: `oknardia/web/test_prices.py` (проверяет свежесть таблицы) + +--- + +**Версия:** 2.0 (Двухуровневое кеширование) +**Последнее обновление:** 2026-05-19 +**Статус:** ✅ Production-ready + diff --git a/MANAGEMENT_RUNBOOK.md b/MANAGEMENT_RUNBOOK.md index e5f70c2..78d1b6e 100644 --- a/MANAGEMENT_RUNBOOK.md +++ b/MANAGEMENT_RUNBOOK.md @@ -394,40 +394,72 @@ location = /sitemap.xml { ## 4) Команда `regenerate_seria_prerender` Назначение: -- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`. +- пересобрать pre-render шаблоны для **статических данных** страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`. +- генерирует **3 типа файлов** для каждой серии (верхняя статья НЕ кешируется, она рендерится динамически из БД): + - `{seria_id}_id_static_flaps.html` — схемы открывания + - `{seria_id}_id_static_graph.html` — график ввода в эксплуатацию + - `{seria_id}_id_static_map_stats.html` — карта + статистика -Проверка без записи файлов: +### Контекст: двухуровневое кеширование + +Система использует двухуровневое кеширование: +- **Статический кеш** — дорогостоящие операции (геокоординаты, графики, схемы). Генерируются один раз и сохраняются на диск. +- **Динамические данные** — верхняя статья про серию (может редактироваться через админку, видно БЕЗ перезагрузки контейнера) и таблица оконных проёмов (показывает актуальные предложения). + +Подробнее: см. [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md) + +### Проверка без записи файлов (dry-run): ```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-серий: +Вывод: +``` +OK seria 210: 3 кеш-файла созданы +OK seria 100: 3 кеш-файла созданы +... +Готово. Обработано: 31. Создано/пересоздано: 31 × 3 файла. Пропущено: 0. +``` + +Принудительная пересборка всех 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/`. +### Когда запускать + +- **После первого развертывания** — сгенерировать кеш всех 31 серии один раз. +- **После обновления логики `catalog_seria_info`** — изменились параметры графика или карты. +- **После изменения координат зданий** — geo-данные обновлены. +- **После добавления новых зданий в серию** — карта и список зда��ий изменились. +- **По расписанию** (опционально, если данные по геокоординатам обновляются): + ```bash + 0 3 * * 0 cd /Users/e-serg/PRJ/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_prerender >> /var/log/oknardia-prerender.log 2>&1 + ``` + +### Когда НЕ нужна регенерация + +- **При добавлении новых предложений/цен** — таблица окон обновляется при каждом запросе автоматически. +- **При редактировании верхней статьи через админку** — она рендерится динамически, кешируется НЕ нужно. +- **При изменении наличия/статуса профилей** — рейтинги пересчитываются запросом через `make_rating`. ## 5) Команда `populate_seo_fields` @@ -532,14 +564,14 @@ print(f'Пусто sMetaKeywords: {posts.filter(sMetaKeywords=\"\").count()}') ✓ Записей обновлено в БД: 28 ✗ Ошибок при обработке: 0 -✅ Обновлено 28 записей успешно! +Обновлено 28 записей успешно! ``` ### Откат и безопасность -- ✅ **Безопасна для повторного запуска** — пустые поля не изменяются при повторной работе. -- ✅ **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';` -- ✅ **Всегда используй `--dry-run`** перед первым запуском для проверки. +- **Безопасна для повторного запуска** — пустые поля не изменяются при повторной работе. +- **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';` +- **Всегда используй `--dry-run`** перед первым запуском для проверки. ## 6) Команда `make_rating` diff --git a/README.md b/README.md index f078496..6a5ebba 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ # См. также: -* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и batch-операциям, сниппеты. +* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и кастом-операциям (регенерация кеша, рейтингов, sitemap и т.д.). +* [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md) – двухуровневая система кеширования страниц серий, структура статик-шаблонов, управление кешем. * [`AGENTS.md`](AGENTS.md) – контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии). * [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки. @@ -65,14 +66,25 @@ В папке `oknardia/templates/seria_info/prepared/` создаются пре-рендер HTML-шаблоны с информацией о сериях домов. -Эти шаблоны создаются при первом обращении к странице серии и хранятся для ускорения последующих запросов. -**Важно**: их надо периодически удалять, особенно если меняются: -* данные по сериям и размерам окон -* коммерческие предложения и цены -* рейтинги компонентов +**Архитектура (май 2026)**: Для каждой серии создаются **3 отдельных кешируемых файла** (верхняя статья НЕ кешируется): +* `{seria_id}_id_static_flaps.html` — схемы открывания окон +* `{seria_id}_id_static_graph.html` — график ввода в эксплуатацию +* `{seria_id}_id_static_map_stats.html` — карта Яндекса и статистика -**Рекомендация**: настроить cronjob на ежедневное или еженедельное удаление этих файлов. При обращении к соответствующим -страницам эти шаблоны будут пересозданы автоматически. На быстрых серверах можно вообще отключить кеширование, если оно -не критично для производительности. +**Верхняя статья рендерится динамически** из БД, поэтому изменения через админку видны без перезагрузки контейнера. + +Таблица оконных проёмов **не кешируется** — пересчитывается при каждом запросе, поэтому новые предложения видны пользователям сразу. + +**Регенерация кеша**: +```bash +python manage.py regenerate_seria_prerender # все сер��и +python manage.py regenerate_seria_prerender --seria-id 210 # конкретная серия +``` + +⏱️ **Когда регенерировать**: Изменены координаты зданий, добавлены новые здания, обновлены годы ввода в эксплуатацию. + +❌ **Когда НЕ нужна регенерация**: Добавлены новые предложения/цены (таблица обновляется автоматически), изменены статьи через админку (рендерятся динамически). + +**Подробности**: см. [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md) diff --git a/oknardia/templates/seria_info/all_seria_info_pre_light.html b/oknardia/templates/seria_info/all_seria_info_pre_light.html index 657d931..acb7432 100755 --- a/oknardia/templates/seria_info/all_seria_info_pre_light.html +++ b/oknardia/templates/seria_info/all_seria_info_pre_light.html @@ -132,40 +132,47 @@ TechArticle: описывает страницу как технический {# #}
-
{{ THIS_SERIA_DESCRIPTION|safe }}
+
+ {# ВЕРХНЯЯ СТАТЬЯ: рендерится динамически (из БД), можно редактировать через админку #} +
{{ THIS_SERIA_DESCRIPTION|safe }}
+
-
-

Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и схемы открывания

+
+

Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и схемы открывания

+
+
+ {# СХЕМЫ ОТКРЫВАНИЯ: статическая часть, если используется кеш #} + {% if PRE_RENDERED_STATIC_FLAPS_PATH %} + {% include PRE_RENDERED_STATIC_FLAPS_PATH %} + {% else %} + {% include 'report/show_big_flap_pictures.html' %} + {% endif %} +
-
- {% include 'report/show_big_flap_pictures.html' %} -
-

Оконные проёмы в типовых квартирах серии {{ THIS_SERIA_NAME }}

- {# --- ОСНОВНОЙ БЛОК С ТАБЛИЦЕЙ --- #} - {# Если есть кешированный файл, включаем его. Иначе - рендерим блок на лету. #} - {% if PRE_RENDERED_INCLUDE_PATH %} - {% include PRE_RENDERED_INCLUDE_PATH %} - {% else %} - {% include "seria_info/all_seria_info_pre_light_include.html" %} - {% endif %} - {# --- КОНЕЦ ОСНОВНОГО БЛОКА --- #} + {# ТАБЛИЦА ОКОН: часть, которая считается при каждом запросе #} + {% include "seria_info/all_seria_info_pre_light_dynamic_include.html" %}
-
-
-

Здания серия {{ THIS_SERIA_NAME }}: ввод в эксплуатацию по годам

-
-
- {% include 'seria_info/yaer_graph.html' %} +
+
+

Здания серия {{ THIS_SERIA_NAME }}: ввод в эксплуатацию по годам

+
+
+ {# ГРАФИК: статическая часть, если используется кеш #} + {% if PRE_RENDERED_STATIC_GRAPH_PATH %} + {% include PRE_RENDERED_STATIC_GRAPH_PATH %} + {% else %} + {% include 'seria_info/all_seria_info_pre_light_static_graph.html' %} + {% endif %}
© 2015-{% now "Y" %}, данные: oknardia.ru
@@ -176,26 +183,31 @@ TechArticle: описывает страницу как технический

Строения серии {{ THIS_SERIA_NAME }} на карте

-
+

Чтобы посмотреть цены на установку и замену окон от партнёров «Окнардия» в своей квартире: найдите дом на карте; кликните на него; перейдите по ссылке «Смотреть коммерческие предложения». При необходимости смените типовую планировку квартиры (на странице ценовой выдачи, справа от изображения типовых проёмов и схем открывания).

-
- {% include 'seria_info/geo_map.html' with first_apart_id=TABLE_OF_WINDOWS.0.APART_ID %} +
+
© 2015-{% now "Y" %}, данные: oknardia.ru
-
-

Статистика серии {{ THIS_SERIA_NAME }}

-

Совокупно во всех зданиях типового проекта:

-
    -
  • {{ ACCOUNTS|price_format }} квартир.
  • -
  • Проживает {{ APARTMENTS|price_format }} семей ({{ RESIDENTS|price_format }} человек).
  • -
  • {{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м² жилых помещений.
  • -
  • {{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м² — муниципальное жильё.
  • -
  • {{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м² занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.
  • -
  • Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — {{ CONDITION_MAX|stringformat:".2f" }} %. Минимальный — {{ CONDITION_MIN|stringformat:".2f" }} %.
  • -
-
+ {# КАРТА И СТАТИСТИКА: статическая часть, если используется кеш #} + {% if PRE_RENDERED_STATIC_MAP_STATS_PATH %} + {% include PRE_RENDERED_STATIC_MAP_STATS_PATH %} + {% else %} +
+

Статистика серии {{ THIS_SERIA_NAME }}

+

Совокупно во всех зданиях типового проекта:

+
    +
  • {{ ACCOUNTS|price_format }} квартир.
  • +
  • Проживает {{ APARTMENTS|price_format }} семей ({{ RESIDENTS|price_format }} человек).
  • +
  • {{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м² жилых помещений.
  • +
  • {{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м² — муниципальное жильё.
  • +
  • {{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м² занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.
  • +
  • Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — {{ CONDITION_MAX|stringformat:".2f" }} %. Минимальный — {{ CONDITION_MIN|stringformat:".2f" }} %.
  • +
+
+ {% endif %}
diff --git a/oknardia/templates/seria_info/all_seria_info_pre_light_include.html b/oknardia/templates/seria_info/all_seria_info_pre_light_dynamic_include.html similarity index 74% rename from oknardia/templates/seria_info/all_seria_info_pre_light_include.html rename to oknardia/templates/seria_info/all_seria_info_pre_light_dynamic_include.html index b030b75..62da134 100644 --- a/oknardia/templates/seria_info/all_seria_info_pre_light_include.html +++ b/oknardia/templates/seria_info/all_seria_info_pre_light_dynamic_include.html @@ -1,5 +1,10 @@ +{# ============================================================================ #} +{# ДИНАМИЧЕСКИЕ ДАННЫЕ ДЛЯ СЕРИИ (НЕ кешируемая часть) #} +{# Содержит: Таблица раскладки окон по квартирам + статистика предложений #} +{# ЗАМЕЧАНИЕ: этот блок ЧАСТО меняется (при добавлении новых предложений) #} +{# ============================================================================ #} +
- {% for row in TABLE_OF_WINDOWS %} @@ -34,5 +39,5 @@
-
+ diff --git a/oknardia/templates/seria_info/all_seria_info_pre_light_static_flaps.html b/oknardia/templates/seria_info/all_seria_info_pre_light_static_flaps.html new file mode 100644 index 0000000..1012058 --- /dev/null +++ b/oknardia/templates/seria_info/all_seria_info_pre_light_static_flaps.html @@ -0,0 +1,21 @@ +{# ============================================================================ #} +{# СХЕМЫ ОТКРЫВАНИЯ И РАЗМЕРЫ (кешируемая статическая часть) #} +{# ============================================================================ #} +{% load static %} +{% load filters %} + +{% if WIN_DIM %} + {% for I_WIN_DIM in FLAP_DIM %} +
+
{{ I_WIN_DIM.sDescription }}. Размер {{ I_WIN_DIM.iWinWidth|stringformat:
+
+ {{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0×{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 мм.
{% if not I_WIN_DIM.iQuantity == 0 %} + {{ I_WIN_DIM.iQuantity }} шт.{% for I_II in I_WIN_DIM.qStr %}{% endfor %}
{% endif %} + {{ I_WIN_DIM.sDescription }}{% if not I_WIN_DIM.iQuantity == 0 %}
+ цены только этого типового окна{% endif %} +
+
{% endfor %} +{% else %} +

Нет данных о проемах и рекомендованных схемах открывания окон

+{% endif %} + diff --git a/oknardia/templates/seria_info/all_seria_info_pre_light_static_graph.html b/oknardia/templates/seria_info/all_seria_info_pre_light_static_graph.html new file mode 100644 index 0000000..892c4c8 --- /dev/null +++ b/oknardia/templates/seria_info/all_seria_info_pre_light_static_graph.html @@ -0,0 +1,55 @@ +{# ============================================================================ #} +{# ГРАФИК ВВОДА В ЭКСПЛУАТАЦИЮ (кешируемая статическая часть) #} +{# ============================================================================ #} + + + + diff --git a/oknardia/templates/seria_info/all_seria_info_pre_light_static_map_stats.html b/oknardia/templates/seria_info/all_seria_info_pre_light_static_map_stats.html new file mode 100644 index 0000000..428ee3a --- /dev/null +++ b/oknardia/templates/seria_info/all_seria_info_pre_light_static_map_stats.html @@ -0,0 +1,93 @@ +{# ============================================================================ #} +{# КАРТА И СТАТИСТИКА СЕРИИ (кешируемая статическая часть) #} +{# ============================================================================ #} +{% load humanize %} +{% load filters %} + +{# БЛОК КАРТА: левая часть (col-md-7) #} + + + +{# БЛОК СТАТИСТИКА: правая часть (col-md-4) #} +
+

Статистика серии {{ THIS_SERIA_NAME }}

+

Совокупно во всех зданиях типового проекта:

+
    +
  • {{ ACCOUNTS|price_format }} квартир.
  • +
  • Проживает {{ APARTMENTS|price_format }} семей ({{ RESIDENTS|price_format }} человек).
  • +
  • {{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м² жилых помещений.
  • +
  • {{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м² — муниципальное жильё.
  • +
  • {{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м² занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.
  • +
  • Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — {{ CONDITION_MAX|stringformat:".2f" }} %. Минимальный — {{ CONDITION_MIN|stringformat:".2f" }} %.
  • +
+
+ diff --git a/oknardia/web/catalog_series.py b/oknardia/web/catalog_series.py index 898aeee..62f79a4 100644 --- a/oknardia/web/catalog_series.py +++ b/oknardia/web/catalog_series.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from pathlib import Path from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count, F, IntegerField, Value from django.shortcuts import render, redirect @@ -17,6 +18,9 @@ from web.report1 import get_last_all_user_visit_list from web.add_func import get_flaps_for_big_pictures, sanitize_slug import time import os +import math +import base64 +import json def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None: @@ -89,13 +93,15 @@ def catalog_seria_info( # чтобы тестировать актуальную серверную логику, а не сохраненный html-файл. if DEBUG: light_template = "seria_info/all_seria_info_pre_light.html" - light_template_w_path = "" + static_include_path = "" # в DEV не используем кеш is_hard_template = True else: - # В PROD используем существующий pre-render include при наличии на диске. - light_template = f"seria_info/prepared/{seria_id}_id.html" - light_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{light_template}" - is_hard_template = not os.path.isfile(light_template_w_path) + # В PROD используем существующий pre-render include для статических данных (если есть). + light_template = "seria_info/all_seria_info_pre_light.html" + static_template_filename = f"seria_info/prepared/{seria_id}_id_static.html" + static_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{static_template_filename}" + is_hard_template = not os.path.isfile(static_template_w_path) + static_include_path = static_template_filename if not is_hard_template else "" to_template: dict[str, object] = {} # Получаем все уникальные проемы серии и сразу добавляем iQuantity=1 @@ -192,30 +198,80 @@ def catalog_seria_info( { "WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win, "TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment, + # Первая квартира из таблицы (нужна для картоки в пре-рендер шаблоне) + "first_apart_id": table_of_win_in_seria_by_apartmment[0]["APART_ID"] if table_of_win_in_seria_by_apartmment else 0, } ) - # Для "тяжелого" шаблона получаем навигацию, карту и график, затем кэшируем pre-render. + # Для "тяжелого" шаблона получаем навигацию, карту и график. + # ВАЖНО: таблица окон (TABLE_OF_WINDOWS) считается ВСЕГДА — она не кешируется! if is_hard_template: to_template.update(get_flaps_for_big_pictures(list_win_in_seria)) seria_id, for_seria_nav = seria_nav(seria_id) to_template.update(for_seria_nav) to_template.update(seria_info_year(seria_id)) to_template.update(seria_info_geo_code(seria_id)) + if not DEBUG: - # Пре-рендер происходит только для "включаемого" шаблона, - # чтобы избежать дублирования базовой разметки. - string_prerender = render_to_string("seria_info/all_seria_info_pre_light_include.html", to_template) - with open(light_template_w_path, "w", encoding="utf-8") as file: - file.write(string_prerender) - # Основной шаблон будет просто включать в себя уже готовый HTML - light_template = "seria_info/all_seria_info_pre_light.html" - else: - to_template.update({"THIS_SERIA_NAME": q_seria.sName}) - # Указываем путь к кешированному файлу для include - to_template.update({"PRE_RENDERED_INCLUDE_PATH": light_template}) - # Основной шаблон должен быть один и тот же - light_template = "seria_info/all_seria_info_pre_light.html" + # Пре-рендер ТРЁХ отдельных файлов для статических данных. + # Верхняя статья НЕ кешируется — она рендерится динамически, чтобы изменения + # через админку были видны сразу без перезагрузки контейнера. + prepared_dir = Path(TEMPLATES[0]["DIRS"][0]) / PATH_FOR_SERIA_INFO_HTML_INCLUDE + prepared_dir.mkdir(parents=True, exist_ok=True) + + # 1. Схемы открывания и размеры + string_flaps = render_to_string( + "seria_info/all_seria_info_pre_light_static_flaps.html", + to_template + ) + file_flaps = prepared_dir / f"{seria_id}_id_static_flaps.html" + with open(file_flaps, "w", encoding="utf-8") as f: + f.write(string_flaps) + + # 2. График ввода в эксплуатацию + string_graph = render_to_string( + "seria_info/all_seria_info_pre_light_static_graph.html", + to_template + ) + file_graph = prepared_dir / f"{seria_id}_id_static_graph.html" + with open(file_graph, "w", encoding="utf-8") as f: + f.write(string_graph) + + # 3. Карта и статистика + string_map_stats = render_to_string( + "seria_info/all_seria_info_pre_light_static_map_stats.html", + to_template + ) + file_map_stats = prepared_dir / f"{seria_id}_id_static_map_stats.html" + with open(file_map_stats, "w", encoding="utf-8") as f: + f.write(string_map_stats) + + # Добавляем в контекст пути к кешируемым файлам (верхняя статья всегда динамична) + pre_rendered_flaps_path = "" + pre_rendered_graph_path = "" + pre_rendered_map_stats_path = "" + + if not DEBUG: + # В production используем кеширующие файлы, если они существую�� + prepared_dir = Path(TEMPLATES[0]["DIRS"][0]) / PATH_FOR_SERIA_INFO_HTML_INCLUDE + + file_flaps = prepared_dir / f"{seria_id}_id_static_flaps.html" + file_graph = prepared_dir / f"{seria_id}_id_static_graph.html" + file_map_stats = prepared_dir / f"{seria_id}_id_static_map_stats.html" + + if file_flaps.exists(): + pre_rendered_flaps_path = f"seria_info/prepared/{seria_id}_id_static_flaps.html" + if file_graph.exists(): + pre_rendered_graph_path = f"seria_info/prepared/{seria_id}_id_static_graph.html" + if file_map_stats.exists(): + pre_rendered_map_stats_path = f"seria_info/prepared/{seria_id}_id_static_map_stats.html" + + to_template.update({ + "THIS_SERIA_NAME": q_seria.sName, + "PRE_RENDERED_STATIC_FLAPS_PATH": pre_rendered_flaps_path, + "PRE_RENDERED_STATIC_GRAPH_PATH": pre_rendered_graph_path, + "PRE_RENDERED_STATIC_MAP_STATS_PATH": pre_rendered_map_stats_path, + }) _append_visit_context(to_template, request, time_start) @@ -440,4 +496,20 @@ def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> d "ACCOUNTS": accounts, "CONDITION_MAX": condition_max, "CONDITION_MIN": condition_min}) + + # Кодируем геоданные в Base64 для защиты (используется в статик-шаблонах) + # Формат: [latitude, longitude, addr_id, seria_id] для каждого здания + geo_for_encoding = [] + for geo_point in seria_to_geo: + geo_for_encoding.append([ + float(geo_point["LATITUDE"]), + float(geo_point["LONGITUDE"]), + geo_point["ADDR_ID"], + geo_point["SER_ID"] + ]) + + geo_json = json.dumps(geo_for_encoding, separators=(',', ':')) + geo_b64 = base64.b64encode(geo_json.encode('utf-8')).decode('utf-8') + data_return["DATA4GEO_B64"] = geo_b64 + return data_return diff --git a/oknardia/web/management/commands/regenerate_seria_prerender.py b/oknardia/web/management/commands/regenerate_seria_prerender.py index df25e2b..3df6b02 100644 --- a/oknardia/web/management/commands/regenerate_seria_prerender.py +++ b/oknardia/web/management/commands/regenerate_seria_prerender.py @@ -14,9 +14,20 @@ from web.add_func import sanitize_slug class Command(BaseCommand): - """Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all).""" + """Пересоздает pre-render шаблоны ��ля статических данных страниц серий (/catalog/seria/.../all). - help = "Пересоздает pre-render шаблоны catalog_seria_info для выбранных или всех корневых серий." + ВАЖНО: Кешируются ТОЛЬКО статические данные (схемы, график, карта, статистика). + Верхняя статья рендерится ДИНАМИЧЕСКИ из БД, чтобы изменения через админку + были видны без перезагрузки контейнера. + Таблица оконных проёмов также пересчитывается при каждом запросе. + + Создаёт 3 файла для каждой серии: + - _id_static_flaps.html (схемы открывания) + - _id_static_graph.html (график ввода в эксплуатацию) + - _id_static_map_stats.html (карта и статистика) + """ + + help = "Пересоздает pre-render шаблоны (3 файла) для статических данных выбранных или всех корневых серий." def add_arguments(self, parser): parser.add_argument( @@ -29,7 +40,7 @@ class Command(BaseCommand): parser.add_argument( "--force", action="store_true", - help="Пересоздать даже если pre-render файл уже существует.", + help="Пересоздать даже если pre-render файлы уже существуют.", ) parser.add_argument( "--dry-run", @@ -61,26 +72,35 @@ class Command(BaseCommand): skipped = 0 for seria in targets: - target_file = prepared_dir / f"{seria.id}_id.html" - if target_file.exists() and not force: + # Проверяем существование вс��х 3 файлов (верхняя статья НЕ кешируется) + target_files = [ + prepared_dir / f"{seria.id}_id_static_flaps.html", + prepared_dir / f"{seria.id}_id_static_graph.html", + prepared_dir / f"{seria.id}_id_static_map_stats.html", + ] + all_exist = all(f.exists() for f in target_files) + + if all_exist and not force: skipped += 1 - self.stdout.write(f"SKIP {seria.id}: {target_file}") + self.stdout.write(f"SKIP {seria.id}: все кеш-файлы уже существуют") continue if dry_run: - action = "REGEN" if target_file.exists() else "CREATE" - self.stdout.write(f"{action} {seria.id}: {target_file}") + action = "REGEN" if all_exist else "CREATE" + self.stdout.write(f"{action} {seria.id}: 3 файла (flaps, graph, map_stats)") planned += 1 continue - if target_file.exists(): - target_file.unlink() + # Удаляем старые файлы перед пересоздаванием + for f in target_files: + if f.exists(): + f.unlink() slug = sanitize_slug(seria.sName) request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}") # В команде принудительно включаем «production-mode» для вьюхи, - # чтобы она прошла тяжелую ветку и пересоздала pre-render файл. + # чтобы она прошла тяжелую ветку и пересоздала pre-render файлы. old_debug = catalog_series.DEBUG try: catalog_series.DEBUG = False @@ -92,22 +112,29 @@ class Command(BaseCommand): raise CommandError( f"Серия {seria.id}: ожидался status=200, получен {response.status_code}." ) - if not target_file.exists(): - raise CommandError(f"Серия {seria.id}: pre-render файл не создан: {target_file}") + + # Проверяем, что все 3 файла были созданы + missing_files = [f for f in target_files if not f.exists()] + if missing_files: + raise CommandError( + f"Серия {seria.id}: не созданы файлы: {[f.name for f in missing_files]}" + ) created += 1 - self.stdout.write(self.style.SUCCESS(f"OK {seria.id}: {target_file}")) + self.stdout.write( + self.style.SUCCESS(f"OK {seria.id}: 3 кеш-файла созданы") + ) if dry_run: self.stdout.write( self.style.SUCCESS( - f"DRY-RUN. Обработано: {len(targets)}. Будет создано/пересоздано: {planned}. Пропущено: {skipped}." + f"DRY-RUN. Обработано: {len(targets)}. Б��дет создано/пересоздано: {planned}. Пропущено: {skipped}." ) ) else: self.stdout.write( self.style.SUCCESS( - f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created}. Пропущено: {skipped}." + f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created} × 3 файла. Пропущено: {skipped}." ) )