fix: пререндер-шаблоны работали некорректно в prod

This commit is contained in:
2026-05-19 22:45:56 +03:00
parent 2dfa3595d4
commit 5627a23d17
10 changed files with 785 additions and 95 deletions
+91 -19
View File
@@ -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
@@ -14,9 +14,20 @@ from web.add_func import sanitize_slug
class Command(BaseCommand):
"""Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all<ID>)."""
"""Пересоздает pre-render шаблоны ля статических данных страниц серий (/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:
# Проверяем существование всх 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}."
)
)