fix: пререндер-шаблоны работали некорректно в prod
This commit is contained in:
361
CACHE_PRERENDER_SYSTEM.md
Normal file
361
CACHE_PRERENDER_SYSTEM.md
Normal file
@@ -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
|
||||
# Генерируем кеш пр<D0BF><D180> запуске
|
||||
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
|
||||
|
||||
@@ -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-данные обновлены.
|
||||
- **После добавления новых зданий в серию** — карта и список зда<D0B4><D0B0>ий изменились.
|
||||
- **По расписанию** (опционально, если данные по геокоординатам обновляются):
|
||||
```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`
|
||||
|
||||
|
||||
30
README.md
30
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 # все сер<D0B5><D180>и
|
||||
python manage.py regenerate_seria_prerender --seria-id 210 # конкретная серия
|
||||
```
|
||||
|
||||
⏱️ **Когда регенерировать**: Изменены координаты зданий, добавлены новые здания, обновлены годы ввода в эксплуатацию.
|
||||
|
||||
❌ **Когда НЕ нужна регенерация**: Добавлены новые предложения/цены (таблица обновляется автоматически), изменены статьи через админку (рендерятся динамически).
|
||||
|
||||
**Подробности**: см. [`CACHE_PRERENDER_SYSTEM.md`](CACHE_PRERENDER_SYSTEM.md)
|
||||
|
||||
|
||||
|
||||
@@ -132,40 +132,47 @@ TechArticle: описывает страницу как технический
|
||||
</div>
|
||||
</div>{# <!--- Хлебные крошки: КОНЕЦ ---> #}
|
||||
<div class="row">
|
||||
<div class="col-md-12">{{ THIS_SERIA_DESCRIPTION|safe }}</div>
|
||||
<div class="col-md-12">
|
||||
{# ВЕРХНЯЯ СТАТЬЯ: рендерится динамически (из БД), можно редактировать через админку #}
|
||||
<div>{{ THIS_SERIA_DESCRIPTION|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-10">
|
||||
<h2 class="header">Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и схемы открывания</h2>
|
||||
<div class="col-lg-10">
|
||||
<h2 class="header">Дома серии {{ THIS_SERIA_NAME }}: типовые размеры и схемы открывания</h2>
|
||||
</div>
|
||||
<div class="col-lg-12" style="padding:1em 0 0 0;margin-left:-1em">
|
||||
{# СХЕМЫ ОТКРЫВАНИЯ: статическая часть, если используется кеш #}
|
||||
{% if PRE_RENDERED_STATIC_FLAPS_PATH %}
|
||||
{% include PRE_RENDERED_STATIC_FLAPS_PATH %}
|
||||
{% else %}
|
||||
{% include 'report/show_big_flap_pictures.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12" style="padding:1em 0 0 0;margin-left:-1em">
|
||||
{% include 'report/show_big_flap_pictures.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-xs-12 col-md-offset-1">
|
||||
<h3 class="header">Оконные проёмы в типовых квартирах <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
|
||||
</div>
|
||||
<div class="col-lg-8 col-xs-12 col-md-offset-1">
|
||||
{# --- ОСНОВНОЙ БЛОК С ТАБЛИЦЕЙ --- #}
|
||||
{# Если есть кешированный файл, включаем его. Иначе - рендерим блок на лету. #}
|
||||
{% 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" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9"><a name="s_graph"></a>
|
||||
<h2 class="header">Здания серия {{ THIS_SERIA_NAME }}: ввод в эксплуатацию по годам</h2>
|
||||
</div>
|
||||
<div class="col-md-9 col-md-offset-1" style="height:300px;font-size:large;">
|
||||
{% include 'seria_info/yaer_graph.html' %}
|
||||
<div class="row">
|
||||
<div class="col-md-9"><a name="s_graph"></a>
|
||||
<h2 class="header">Здания серия {{ THIS_SERIA_NAME }}: ввод в эксплуатацию по годам</h2>
|
||||
</div>
|
||||
<div class="col-md-9 col-md-offset-1" style="height:300px;font-size:large;" id="graph">
|
||||
{# ГРАФИК: статическая часть, если используется кеш #}
|
||||
{% 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 %}
|
||||
</div>
|
||||
<div class="col-md-9 col-md-offset-1">
|
||||
<div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div>
|
||||
@@ -176,26 +183,31 @@ TechArticle: описывает страницу как технический
|
||||
<div class="col-md-7"><a name="s_map"></a>
|
||||
<h2 class="header">Строения серии {{ THIS_SERIA_NAME }} на карте</h2>
|
||||
</div>
|
||||
<div class="col-md-7 col-lg-offset-1">
|
||||
<div class="col-md-7 col-lg-offset-1">
|
||||
<p><small>Чтобы посмотреть цены на установку и замену окон от партнёров «Окнардия» в своей квартире: найдите дом на карте; кликните на него; перейдите по ссылке «Смотреть коммерческие предложения». При необходимости смените типовую планировку квартиры (на странице ценовой выдачи, справа от изображения типовых проёмов и схем открывания).</small></p>
|
||||
<div style="height:350px;">
|
||||
{% include 'seria_info/geo_map.html' with first_apart_id=TABLE_OF_WINDOWS.0.APART_ID %}
|
||||
<div style="height:350px;">
|
||||
<div id="SeriaMap" style="height: 100%;"></div>
|
||||
</div>
|
||||
<div style="font-size: xx-small;float: right">© 2015-{% now "Y" %}, данные: oknardia.ru</div>
|
||||
</div>
|
||||
|
||||
<diV class="col-md-4">
|
||||
<h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
|
||||
<p>Совокупно во всех зданиях типового проекта:</p>
|
||||
<ul>
|
||||
<li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li>
|
||||
<li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li>
|
||||
<li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li>
|
||||
<li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong> — муниципальное жильё.</li>
|
||||
<li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.</li>
|
||||
<li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — <strong>{{ CONDITION_MAX|stringformat:".2f" }} %</strong>. Минимальный — <strong>{{ CONDITION_MIN|stringformat:".2f" }} %</strong>. </li>
|
||||
</ul>
|
||||
</diV>
|
||||
{# КАРТА И СТАТИСТИКА: статическая часть, если используется кеш #}
|
||||
{% if PRE_RENDERED_STATIC_MAP_STATS_PATH %}
|
||||
{% include PRE_RENDERED_STATIC_MAP_STATS_PATH %}
|
||||
{% else %}
|
||||
<diV class="col-md-4">
|
||||
<h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
|
||||
<p>Совокупно во всех зданиях типового проекта:</p>
|
||||
<ul>
|
||||
<li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li>
|
||||
<li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li>
|
||||
<li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li>
|
||||
<li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong> — муниципальное жильё.</li>
|
||||
<li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.</li>
|
||||
<li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — <strong>{{ CONDITION_MAX|stringformat:".2f" }} %</strong>. Минимальный — <strong>{{ CONDITION_MIN|stringformat:".2f" }} %</strong>. </li>
|
||||
</ul>
|
||||
</diV>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{# ============================================================================ #}
|
||||
{# ДИНАМИЧЕСКИЕ ДАННЫЕ ДЛЯ СЕРИИ (НЕ кешируемая часть) #}
|
||||
{# Содержит: Таблица раскладки окон по квартирам + статистика предложений #}
|
||||
{# ЗАМЕЧАНИЕ: этот блок ЧАСТО меняется (при добавлении новых предложений) #}
|
||||
{# ============================================================================ #}
|
||||
|
||||
<div class="col-md-9 col-xs-12" style="padding:0;">
|
||||
<!--- прешаблон начало --->
|
||||
<table style="padding:2px;">
|
||||
{% for row in TABLE_OF_WINDOWS %}
|
||||
<tr class="tr2">
|
||||
@@ -34,5 +39,5 @@
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--- прешаблон конец --->
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{# ============================================================================ #}
|
||||
{# СХЕМЫ ОТКРЫВАНИЯ И РАЗМЕРЫ (кешируемая статическая часть) #}
|
||||
{# ============================================================================ #}
|
||||
{% load static %}
|
||||
{% load filters %}
|
||||
|
||||
{% if WIN_DIM %}
|
||||
{% for I_WIN_DIM in FLAP_DIM %}
|
||||
<div class="win_discr pull-left" id="flap{{ forloop.counter0 }}">
|
||||
<div><img src="{% static I_WIN_DIM.url2img %}" alt="{{ I_WIN_DIM.sDescription }}. Размер {{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 (Ш х В, мм.). Типовая схема открывания." title="{{ I_WIN_DIM.sDescription }}. Размер {{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 (Ш х В, мм.). Типовая схема открывания." itemprop="image" /></div>
|
||||
<div class="caption" style="width:{{ I_WIN_DIM.W }}px;min-width:13ex;">
|
||||
<nobr>{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0×{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0 мм.</nobr><br />{% if not I_WIN_DIM.iQuantity == 0 %}
|
||||
<nobr><b>{{ I_WIN_DIM.iQuantity }} шт.</b>{% for I_II in I_WIN_DIM.qStr %}<span class="color-bullet" style="background-image:url('{% static 'img/svg/mark' %}{{ I_II }}.svg');"></span>{% endfor %}</nobr><br />{% endif %}
|
||||
{{ I_WIN_DIM.sDescription }}{% if not I_WIN_DIM.iQuantity == 0 %}<br />
|
||||
<a href="/catalog/standard_opening/price-{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0mm-tip{{ I_WIN_DIM.id }}">цены только этого типового окна</a>{% endif %}
|
||||
</div>
|
||||
</div>{% endfor %}
|
||||
{% else %}
|
||||
<h1>Нет данных о проемах и рекомендованных схемах открывания окон</h1>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{# ============================================================================ #}
|
||||
{# ГРАФИК ВВОДА В ЭКСПЛУАТАЦИЮ (кешируемая статическая часть) #}
|
||||
{# ============================================================================ #}
|
||||
|
||||
<script type="text/javascript" src="https://www.google.com/jsapi" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
google.load("visualization", "1", {packages:["corechart"]});
|
||||
google.setOnLoadCallback(drawChart);
|
||||
function drawChart() {
|
||||
let data = google.visualization.arrayToDataTable([
|
||||
["Год", "Введено в эксплуатацию", {role:'style'}],{% for row in DATA4GRAPH %}
|
||||
["{{ row.YEAR }}",{{ row.NUMS }},"color: #99{{ row.CLRS }}99"]{% if not forloop.last %},{% endif %}{% endfor %}
|
||||
]);
|
||||
let view = new google.visualization.DataView(data);
|
||||
view.setColumns([0, 1,
|
||||
{ calc: "stringify",
|
||||
sourceColumn: 1,
|
||||
type: "string",
|
||||
role: "annotation" },
|
||||
2
|
||||
]);
|
||||
let options = {
|
||||
animation:{
|
||||
duration: 1500,
|
||||
easing: 'in',
|
||||
startup: true
|
||||
},
|
||||
backgroundColor: "#EEEEEE",
|
||||
bar: {groupWidth: "76.4%"},
|
||||
chartArea: {left: "2%", top: "5%", width: '96%', height: '85%'},
|
||||
dataOpacity: 0.76,
|
||||
explorer:{
|
||||
maxZoomIn: 0.20,
|
||||
maxZoomOut: 32 },
|
||||
vAxis: {
|
||||
baselineColor:'grey',
|
||||
gridlines:{color: 'silver', count: 7},
|
||||
minorGridlines:{color: '#dddddd', count: 3},
|
||||
textPosition: 'in',
|
||||
textStyle: {fontSize: 10}
|
||||
},
|
||||
hAxis: { textStyle: {fontSize: 10} },
|
||||
isStacked: true,
|
||||
tooltip: {
|
||||
textStyle:{color: 'grey', fontSize: 10 },
|
||||
trigger: 'selection'
|
||||
},
|
||||
annotations: {textStyle: { fontSize: 8, bold: true, color: 'black', opacity: 0.8 }},
|
||||
legend: { position: "none" }
|
||||
};
|
||||
let chart = new google.visualization.ColumnChart(document.getElementById("graph"));
|
||||
chart.draw(data, options);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{# ============================================================================ #}
|
||||
{# КАРТА И СТАТИСТИКА СЕРИИ (кешируемая статическая часть) #}
|
||||
{# ============================================================================ #}
|
||||
{% load humanize %}
|
||||
{% load filters %}
|
||||
|
||||
{# БЛОК КАРТА: левая часть (col-md-7) #}
|
||||
<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// Функция для декодирования Base64-обфускованных геоданных (защита координат)
|
||||
function decodeGeoData(b64str) {
|
||||
try {
|
||||
var json = atob(b64str);
|
||||
return JSON.parse(json);
|
||||
} catch(e) {
|
||||
console.error('Ошибка декодирования геоданных:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ymaps.ready(function () {
|
||||
let myMap = new ymaps.Map('SeriaMap', {
|
||||
center: [55.75, 37.57],
|
||||
zoom: 10,
|
||||
behaviors: ['default', 'scrollZoom'],
|
||||
controls: [ 'rulerControl', 'zoomControl', 'geolocationControl', 'fullscreenControl' ]
|
||||
});
|
||||
// Создадим кластеризатор, вызвав функцию-конструктор.
|
||||
clusterer = new ymaps.Clusterer({
|
||||
preset: 'islands#invertedGrayClusterIcons',
|
||||
groupByCoordinates: false,
|
||||
hasHint: false,
|
||||
viewportMargin: 0,
|
||||
zoomMargin: 16,
|
||||
clusterDisableClickZoom: false,
|
||||
gridSize: 80
|
||||
});
|
||||
geoObjects = [];
|
||||
|
||||
const linkText = 'Смотреть коммерческие предложения</a>';
|
||||
const hintText = '<b>Здание серии {{ THIS_SERIA_NAME }}</b>';
|
||||
const apartmentId = {{ first_apart_id|default:0 }};
|
||||
const seriaId = {{ THIS_SERIA_ID }};
|
||||
const seriaSlug = '{{ THIS_SERIA_NAME_T }}';
|
||||
|
||||
// Декодируем обфускованные геоданные: [lat, lon, addr_id, seria_id]
|
||||
var geoData = decodeGeoData('{{ DATA4GEO_B64 }}');
|
||||
|
||||
// Создаем метки для каждого здания серии
|
||||
for(var i = 0, len = geoData.length; i < len; i++) {
|
||||
const latitude = geoData[i][0];
|
||||
const longitude = geoData[i][1];
|
||||
const buildingId = geoData[i][2];
|
||||
// Формируем SEO-URL для каждой метки
|
||||
const balloonLink = `<a href="/price/seriaID${seriaId}--${seriaSlug}/appartID${apartmentId}/addressID${buildingId}--null">`;
|
||||
|
||||
geoObjects[i] = new ymaps.Placemark( [longitude, latitude],
|
||||
{ // Содержимое иконки, балуна и хинта.
|
||||
balloonContent: balloonLink + linkText,
|
||||
hintContent: hintText
|
||||
},
|
||||
{ preset:'islands#circleIcon',iconColor: 'silver'} );
|
||||
geoObjects[i].events
|
||||
.add('mouseenter', function (e) {
|
||||
e.get('target').options.set('preset', 'islands#yellowCircleIcon');
|
||||
})
|
||||
.add('mouseleave', function (e) {
|
||||
e.get('target').options.set('preset', 'islands#grayCircleIcon');
|
||||
});
|
||||
}
|
||||
// Добавляем метки в кластеризатор.
|
||||
clusterer.add(geoObjects);
|
||||
myMap.geoObjects.add(clusterer);
|
||||
// позиционирование карты так, чтобы на ней были видны все объекты кластера.
|
||||
myMap.setBounds(clusterer.getBounds(), { checkZoomRange: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
{# БЛОК СТАТИСТИКА: правая часть (col-md-4) #}
|
||||
<diV class="col-md-4">
|
||||
<h3 class="header">Статистика <nobr>серии {{ THIS_SERIA_NAME }}</nobr></h3>
|
||||
<p>Совокупно во всех зданиях типового проекта:</p>
|
||||
<ul>
|
||||
<li><strong>{{ ACCOUNTS|price_format }}</strong> квартир.</li>
|
||||
<li>Проживает <strong>{{ APARTMENTS|price_format }}</strong> семей <small>(<strong>{{ RESIDENTS|price_format }}</strong> человек)</small>.</li>
|
||||
<li><strong>{{ RESIDENTIAL_M2|stringformat:".1f"|price_format }} м²</strong> жилых помещений.</li>
|
||||
<li><strong>{{ MUNICIPAL_M2|stringformat:".1f"|price_format }} м²</strong> — муниципальное жильё.</li>
|
||||
<li><strong>{{ GOVERNMENT_M2|stringformat:".1f"|price_format }} м²</strong> занимают государственные и городские службы, учреждения бытового обслуживания, магазины, офисы и тому подобное.</li>
|
||||
<li>Максимальный износ жилого фонда серии {{ THIS_SERIA_NAME }} — <strong>{{ CONDITION_MAX|stringformat:".2f" }} %</strong>. Минимальный — <strong>{{ CONDITION_MIN|stringformat:".2f" }} %</strong>. </li>
|
||||
</ul>
|
||||
</diV>
|
||||
|
||||
@@ -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 используем кеширующие файлы, если они существую<D183><D18E>
|
||||
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
|
||||
|
||||
@@ -14,9 +14,20 @@ from web.add_func import sanitize_slug
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all<ID>)."""
|
||||
"""Пересоздает pre-render шаблоны <EFBFBD><EFBFBD>ля статических данных страниц серий (/catalog/seria/.../all<ID>).
|
||||
|
||||
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:
|
||||
# Проверяем существование вс<D0B2><D181>х 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)}. Б<EFBFBD><EFBFBD>дет создано/пересоздано: {planned}. Пропущено: {skipped}."
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created}. Пропущено: {skipped}."
|
||||
f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created} × 3 файла. Пропущено: {skipped}."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user