mod: рефакторинг "каталог типовых проёмов"

This commit is contained in:
2026-04-24 17:31:56 +03:00
parent c184d65c66
commit 4bb77a9892
3 changed files with 94 additions and 87 deletions

View File

@@ -16,7 +16,7 @@
* Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`. * Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`.
* Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии). * Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии).
* Добавлена management-команда `regenerate_seria_prerender` для оффлайн-пересборки pre-render шаблонов `catalog_seria_info` (все или выбранные root-серии), с режимами `--dry-run` и `--force`; серверный reload (Gunicon? uWSGI или что там еще будет) должен быть вынесен из кода приложения в оркестрацию (cron/systemd/deploy step). * Добавлена management-команда `regenerate_seria_prerender` для оффлайн-пересборки pre-render шаблонов `catalog_seria_info` (все или выбранные root-серии), с режимами `--dry-run` и `--force`; серверный reload (Gunicon? uWSGI или что там еще будет) должен быть вынесен из кода приложения в оркестрацию (cron/systemd/deploy step).
* * Рефакторинг `standard_opening`: raw SQL -> ORM, упрощена дедублекация, убраны лишние запросы и переменные контекста, добавлены комментарии, SEO-описание и keywords, стандартизирован хвост контекста с визитами и `ticks` через общий helper внутри `catalog_openings.py`.
* *
* *
* *
@@ -38,7 +38,6 @@
* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) единый runbook по management-командам и batch-операциям. * [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) единый runbook по management-командам и batch-операциям.
--- ---
Легаси-материалы старого README, которые могут быть полезны для понимания устройства проекта и его Легаси-материалы старого README, которые могут быть полезны для понимания устройства проекта и его
администрирования, а также для будущей реорганизации документации. администрирования, а также для будущей реорганизации документации.

View File

@@ -1,12 +1,12 @@
{% extends "base.html" %}{% load static %} {% extends "base.html" %}{% load static %}
{% block Title %} Стандартные оконные проёмы типовых серий домов :: каталог{% endblock %} {% block Title %}Стандартные оконные проёмы и балконные блоки для типовых серий домов: размеры, схемы, каталог{% endblock %}
{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %} {% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %}
{% block Description %}Каталог «Окнардия»: стандартные оконные проёмы типовых серий домов...{% endblock %} {% block Description %}Найдите точные размеры (ширину и высоту) и схемы стандартных оконных проёмов и балконных блоков для самых распространённых типовых серий домов в России. Удобный каталог для подбора окон.{% endblock %}
{% block Keywords %}оконные проёмы, стандартные окна, стандартные оконные проемы, каталог, каталог оконных проёмов, oknardia, окнардия {{ META_KEYWORDS|default:"" }} {% endblock %} {% block Keywords %}типовые окна, размеры окон, оконные проемы, балконный блок, стандартные окна, размеры окон в панельном доме, серия дома, каталог окон, схемы открывания окон, П-44, II-49, 1-515, oknardia, окнардия{% endblock %}
{% block Date4Meta %}{{ PUB_DAT|date:"c" }}{% endblock %} {% block Date4Meta %}{{ PUB_DAT|date:"c" }}{% endblock %}
@@ -58,18 +58,18 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="/">Главная</a></li> <li><a href="/">Главная</a></li>
<li><a href="/catalog">Каталог</a></li> <li><a href="/catalog">Каталог</a></li>
<li>Оконные проёмы и балконные блоки</li> <li>Оконные проёмы и&nbsp;балконные блоки</li>
</ol> </ol>
</div> </div>
</div>{# <!--- Хлебные крошки: КОНЕЦ ---> #} </div>{# <!--- Хлебные крошки: КОНЕЦ ---> #}
<dIv class="row"> <dIv class="row">
{# ПЕРВЫЙ РАЗДЕЛ #}<div class="col-md-9 col-xs-8"> {# ПЕРВЫЙ РАЗДЕЛ #}<div class="col-md-9 col-xs-8">
<h1>Стандартные оконные проёмы и&nbsp;балконные блоки</h1> <h1>Стандартные оконные проёмы и&nbsp;балконные блоки</h1>
<p>Ценовая выдача «Окнардии» основана на базе стандартных оконных проёмов в типовых сериях домов. Для каждого проёма существуют рекомендованные организациями-проектировщиками схемы открывание, но партнёры «Окнардии» могут предложить свои, более расширенные или наоборот сокращенные. В таблице приведены параметры стандартных проёмов базы.</p> <p>Ценовая выдача «Окнардии» основана на&nbsp;базе стандартных оконных проёмов в&nbsp;типовых сериях домов. Для&nbsp;каждого проёма существуют рекоме&shy;ндованные органи&shy;зациями-проекти&shy;ровщиками схемы открывание, но&nbsp;партнёры «Окнардии» могут предложить свои, более расширенные или наоборот сокращенные. В&nbsp;таблице приведены параметры стандартных проёмов базы.</p>
</div> </div>
{# реклама Oknardia 250x250 СБОКУ #}<div class="col-md-3 col-xs-4 float-right">{% include "ad/bannet-250x250.html" %}</div> {# реклама Oknardia 250x250 СБОКУ #}<div class="col-md-3 col-xs-4 float-right">{% include "ad/bannet-250x250.html" %}</div>
<div class="col-md-11 col-xs-12 catalog scrolled"> <div class="col-md-11 col-xs-12 catalog scrolled">
<table class="table-striped table-hover table-responsive flap-catalog"> <table class="table-striped table-hover table-responsive flap-catalog" style="margin-top: 1em">
<thead> <thead>
<tr> <tr>
<th colspan="3" class="c">Размеры (мм)</th> <th colspan="3" class="c">Размеры (мм)</th>
@@ -98,24 +98,15 @@
<td>{{ i.DESCRIPTION }}</td> <td>{{ i.DESCRIPTION }}</td>
<td>{% for j in i.INCLUDING_IN_SERIA %}<a href="/catalog/seria/{{ j.NAME_T }}/all{{ j.ID }}">{{ j.NAME }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td> <td>{% for j in i.INCLUDING_IN_SERIA %}<a href="/catalog/seria/{{ j.NAME_T }}/all{{ j.ID }}">{{ j.NAME }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
<td><a class="btn btn-default btn-xs" href="/tsena-odnogo-okna/{{ i.W|stringformat:".0f" }}x{{ i.H|stringformat:".0f" }}mm/tip{{ i.ID }}">цены</a></td> <td><a class="btn btn-default btn-xs" href="/tsena-odnogo-okna/{{ i.W|stringformat:".0f" }}x{{ i.H|stringformat:".0f" }}mm/tip{{ i.ID }}">цены</a></td>
{# {% for j in SERIAS %}#}
{# <td>{% if j.id in i.INCLUDING_IN_SERIA %}#{% endif %}</td>{% endfor %}#}
</tr>{% endfor %} </tr>{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<DIV class="col-xs-12" style="height:6em;"></DIV> <DIV class="col-xs-12" style="height:6em;"></DIV>
</dIv> </dIv>
{# --- Баннер: НАЧАЛО --- #}<div class="row"><div class="col-md-12 col-xs-12"><hr class="dotted-black" />{% include "ad/bannet-wide.html" %}</div></div>{# --- Баннер: конец --- #}
{# --- Баннер: НАЧАЛО --- #}
<div class="row"><div class="col-md-12 col-xs-12"><hr class="dotted-black" />{% include "ad/bannet-wide.html" %}</div></div>
{# --- Баннер: конец --- #}
<div class="row"> <div class="row">
{% include "report/report_last_user_visit.html" %} {% include "report/report_last_user_visit.html" %}
{% include "report/report_log_user_visit.html" %} {% include "report/report_log_user_visit.html" %}
</div> </div>
</div>{% endblock %} </div>{% endblock %}

View File

@@ -1,82 +1,99 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db.models import F
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from oknardia.models import ( from oknardia.models import MountDim2Apartment
Seria_Info,
Win_MountDim,
)
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
from web.add_func import get_flaps_for_mini_pictures from web.add_func import get_flaps_for_mini_pictures
import time import time
import pytils import pytils
from typing import Any
from itertools import groupby
from operator import itemgetter
def standard_opening(request: HttpRequest) -> HttpResponse:
time_start = time.perf_counter() def _make_slug(value: str) -> str:
to_template: dict[str, object] = {} # словарь, для передачи шаблону """Транслитерирует строку в slug (pytils)."""
q_seria = Seria_Info.objects.raw('SELECT oknardia_seria_info.id, oknardia_seria_info.sName ' return pytils.translit.slugify(value)
'FROM oknardia_seria_info '
'WHERE oknardia_seria_info.id = oknardia_seria_info.kRoot_id '
'ORDER BY oknardia_seria_info.sName;') def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
to_template.update({'SERIAS': list(q_seria)}) """Дописывает в контекст стандартный хвост: визиты и время выполнения."""
q_win_opening = Win_MountDim.objects.raw(
'SELECT oknardia_win_mountdim.*,'
' oknardia_seria_info.sName,'
' oknardia_seria_info.id AS ID_Seria '
'FROM oknardia_win_mountdim'
' INNER JOIN oknardia_mountdim2apartment'
' ON oknardia_win_mountdim.id = oknardia_mountdim2apartment.kMountDim_id'
' RIGHT OUTER JOIN oknardia_apartment_type'
' ON oknardia_apartment_type.id = oknardia_mountdim2apartment.kApartment_id'
' RIGHT OUTER JOIN oknardia_seria_info'
' ON oknardia_apartment_type.kSeria_id = oknardia_seria_info.id '
'WHERE oknardia_seria_info.id = oknardia_seria_info.kRoot_id '
'GROUP BY oknardia_win_mountdim.iWinWidth, oknardia_win_mountdim.iWinHight,'
' oknardia_win_mountdim.bIsDoor, oknardia_win_mountdim.bIsNearDoor,'
' oknardia_win_mountdim.sFlapConfig, oknardia_win_mountdim.id,'
' oknardia_seria_info.sName, oknardia_seria_info.id '
'ORDER BY oknardia_win_mountdim.iWinWidth DESC,'
' oknardia_win_mountdim.iWinHight DESC,'
' oknardia_win_mountdim.bIsNearDoor,'
' oknardia_win_mountdim.bIsDoor,'
' oknardia_win_mountdim.id,'
' oknardia_seria_info.sName;')
list_windows_opening = []
tmp_id = 0
for i in q_win_opening:
if tmp_id != i.id:
tmp_id = i.id
image_file_name = get_flaps_for_mini_pictures(i.sFlapConfig)
list_windows_opening.append({
"ID": i.id,
"INCLUDING_IN_SERIA": [{
"ID": i.ID_Seria,
"NAME_T": pytils.translit.slugify(i.sName),
"NAME": i.sName
}],
"INCLUDING_IN_SERIA_ID": [],
"URL2IMG": image_file_name,
"FLAP_CONFIG": i.sFlapConfig,
"DESCRIPTION": i.sDescripion.split(" для")[0].split(" (")[0],
"DESCRIPTION_L": i.sDescripion,
"IS_DOOR": i.bIsDoor,
"IS_NEAR_DOOR": i.bIsNearDoor,
"H": i.iWinHight * 10,
"W": i.iWinWidth * 10
})
else:
list_windows_opening[-1]["INCLUDING_IN_SERIA"].append({
"ID": i.ID_Seria,
"NAME_T": pytils.translit.slugify(i.sName),
"NAME": i.sName
})
to_template.update({ to_template.update({
'LIST_WIN_OPENING': list_windows_opening,
# получаем последние визиты клиента через куки # получаем последние визиты клиента через куки
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]), 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
# получаем последние визиты всех посетителей из базы # получаем последние визиты всех посетителей из базы
# id2log, log_visit = get_last_all_user_visit_list()
'LOG_VISIT': get_last_all_user_visit_list(), 'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start) 'ticks': float(time.perf_counter() - time_start),
}) })
return render(request, "catalog/catalog_standard_opening.html", to_template)
def standard_opening(request: HttpRequest) -> HttpResponse:
"""
Каталог стандартных оконных проёмов и балконных блоков.
Что делает вьюха:
- Собирает уникальные пары «проём ↔ серия» через ORM.
- Агрегирует данные для шаблона в структуру LIST_WIN_OPENING с помощью groupby.
- Добавляет в контекст последние визиты и время выполнения.
"""
time_start = time.perf_counter()
q_win_opening = (
MountDim2Apartment.objects.filter(kApartment__kSeria_id=F('kApartment__kSeria__kRoot_id'))
.values(
'kMountDim_id',
'kMountDim__sFlapConfig',
'kMountDim__sDescripion',
'kMountDim__bIsDoor',
'kMountDim__bIsNearDoor',
'kMountDim__iWinHight',
'kMountDim__iWinWidth',
'kApartment__kSeria_id',
'kApartment__kSeria__sName',
)
.distinct()
.order_by(
'-kMountDim__iWinWidth',
'-kMountDim__iWinHight',
'kMountDim__bIsNearDoor',
'kMountDim__bIsDoor',
'kMountDim_id',
'kApartment__kSeria__sName',
)
)
list_windows_opening: list[dict[str, Any]] = []
# Группируем результаты по ID проёма, чтобы собрать все серии, в которые он входит.
# `order_by` в запросе гарантирует, что все записи для одного проёма идут подряд.
for mount_dim_id, group in groupby(q_win_opening, key=itemgetter('kMountDim_id')):
rows_for_opening = list(group)
first_row = rows_for_opening[0]
description_full = first_row['kMountDim__sDescripion'] or ''
# Собираем список серий для текущего проёма.
serias_for_opening = [
{
'ID': row['kApartment__kSeria_id'],
'NAME_T': _make_slug(row['kApartment__kSeria__sName']),
'NAME': row['kApartment__kSeria__sName'],
}
for row in rows_for_opening
]
# Формируем данные для строки таблиц (типовой проем)
list_windows_opening.append({
'ID': mount_dim_id,
'INCLUDING_IN_SERIA': serias_for_opening,
'URL2IMG': get_flaps_for_mini_pictures(first_row['kMountDim__sFlapConfig']),
'FLAP_CONFIG': first_row['kMountDim__sFlapConfig'],
'DESCRIPTION': description_full.split(' для')[0].split(' (')[0],
'DESCRIPTION_L': description_full,
'IS_DOOR': first_row['kMountDim__bIsDoor'],
'IS_NEAR_DOOR': first_row['kMountDim__bIsNearDoor'],
'H': first_row['kMountDim__iWinHight'] * 10, # см -> мм
'W': first_row['kMountDim__iWinWidth'] * 10, # см -> мм
})
to_template = {'LIST_WIN_OPENING': list_windows_opening}
_append_visit_context(to_template, request, time_start)
return render(request, 'catalog/catalog_standard_opening.html', to_template)