mod: refactor catalog_seria_info and update README/settings

This commit is contained in:
2026-04-21 21:31:17 +03:00
parent 1f0aaa7687
commit 0d97dafe3e
3 changed files with 257 additions and 231 deletions

View File

@@ -14,7 +14,7 @@
* Рефакторинг `catalog_profile_model` (`/catalog/profile/...`): raw SQL ⟶ ORM, упрощена логика, вынесены helper-функции, сокращено дублирование расчёта цветов рейтинга, нормализована подготовка `LIST_OTHER`/`MERCHANTS`/`PROFILES`/`PROFILE_DETAIL`, сохранена совместимость шаблонов. * Рефакторинг `catalog_profile_model` (`/catalog/profile/...`): raw SQL ⟶ ORM, упрощена логика, вынесены helper-функции, сокращено дублирование расчёта цветов рейтинга, нормализована подготовка `LIST_OTHER`/`MERCHANTS`/`PROFILES`/`PROFILE_DETAIL`, сохранена совместимость шаблонов.
* Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`. * Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`.
* Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`. * Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`.
* * Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии).
* *
* *
* *

View File

@@ -218,7 +218,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CAPTCHA_PUBLIC_KEY = env('CAPTCHA_PUBLIC_KEY', default='') CAPTCHA_PUBLIC_KEY = env('CAPTCHA_PUBLIC_KEY', default='')
CAPTCHA_PRIVATE_KEY = env('CAPTCHA_PRIVATE_KEY', default='') CAPTCHA_PRIVATE_KEY = env('CAPTCHA_PRIVATE_KEY', default='')
# количество коммерческих предложений во фреме отчета # если непонятно какая серия выбрана через каталог (finger fix) выбираем серию типового строения:
DEFAULT_SERIA_ID_FOR_CATALOG = 843 # СЕРИЯ 1-515/9 -- дом в котором я живу
# количество коммерческих предложений во фрейме отчета
OFFER_PER_FRAME = 5 OFFER_PER_FRAME = 5
OFFER_PER_FRAME_FOR_ONE_FLAP = 10 OFFER_PER_FRAME_FOR_ONE_FLAP = 10
# папка для хранения изображений # папка для хранения изображений

View File

@@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import F from django.db.models import Count, F, IntegerField, Value
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string from django.template.loader import render_to_string
from oknardia.settings import * from oknardia.settings import *
from oknardia.models import ( from oknardia.models import (
Apartment_Type,
MountDim2Apartment,
PriceOffer,
Seria_Info, Seria_Info,
Win_MountDim, Win_MountDim,
Building_Info, Building_Info,
@@ -31,10 +34,10 @@ def _append_visit_context(to_template: dict, request: HttpRequest, time_start: f
'ticks': float(time.perf_counter() - time_start), 'ticks': float(time.perf_counter() - time_start),
}) })
# Каталог типовых серий зданий (пока переадресация) # Каталог типовых серий зданий.
def catalog_seria(request: HttpRequest) -> HttpResponse: def catalog_seria(request: HttpRequest) -> HttpResponse:
""" """
КАТАЛОГ ТИПОВЫЙ СЕРИЙ: страница со всеми сериями зданий в базе окнардии КАТАЛОГ ТИПОВЫХ СЕРИЙ: выводит список корневых серий из каталога.
:param request: HttpRequest -- входящий http-запрос :param request: HttpRequest -- входящий http-запрос
:return response: HttpResponse -- исходящий http-ответ :return response: HttpResponse -- исходящий http-ответ
@@ -61,180 +64,175 @@ def catalog_seria(request: HttpRequest) -> HttpResponse:
return render(request, "catalog/catalog_seria.html", to_template) return render(request, "catalog/catalog_seria.html", to_template)
def catalog_seria_info(request: HttpRequest, seria_name_translit: None, seria_id: int = 843) -> HttpResponse: def catalog_seria_info(
request: HttpRequest,
seria_name_translit: str | None,
seria_id: int = DEFAULT_SERIA_ID_FOR_CATALOG,
) -> HttpResponse:
""" """
КАТАЛОГ ТИПОВЫЙ СЕРИЙ: страница детальной информацией по серии зданий КАТАЛОГ ТИПОВОЙ СЕРИИ: детальная страница по серии домов.
Что делает вьюха:
- канонизирует URL (root-id серии + корректный slug),
- собирает таблицу окон по типам квартир,
- для "тяжелого" режима дополнительно готовит навигацию/график/гео-данные
и сохраняет pre-render include-шаблон для последующих быстрых ответов.
:param request: HttpRequest -- входящий http-запрос :param request: HttpRequest -- входящий http-запрос
:param seria_name_translit: str -- имя серии здания (транслитерированное pytils.translit.slugify()) :param seria_name_translit: str -- имя серии здания (транслитерированное через pytils)
:param seria_id: int -- id серии :param seria_id: int -- id серии
:return response: HttpResponse -- исходящий http-ответ :return response: HttpResponse -- исходящий http-ответ
""" """
time_start = time.perf_counter() time_start = time.perf_counter()
msg = "" # Канонизируем URL: страница серии должна открываться только по корневой серии и правильному slug.
try: try:
seria_id = int(seria_id) seria_id = int(seria_id)
q_seria = Seria_Info.objects.get(id=seria_id) q_seria = Seria_Info.objects.only("id", "kRoot_id", "sName").get(id=seria_id)
if q_seria.id != q_seria.kRoot_id or seria_name_translit != pytils.translit.slugify(q_seria.sName): if q_seria.id != q_seria.kRoot_id or seria_name_translit != pytils.translit.slugify(q_seria.sName):
return redirect(f"/catalog/seria/{pytils.translit.slugify(q_seria.sName)}/all{seria_id}") return redirect(f"/catalog/seria/{pytils.translit.slugify(q_seria.sName)}/all{seria_id}")
except(ObjectDoesNotExist, ValueError,): except (ObjectDoesNotExist, ValueError):
return redirect("/catalog/") return redirect("/catalog/")
# если есть "облегченный" шаблон с частичным пре-рендером, то используем его.
light_template = f"{PATH_FOR_SERIA_INFO_HTML_INCLUDE}{str(seria_id)}_id.html" # Если есть "облегченный" шаблон с частичным pre-render, используем его.
light_template = f"{PATH_FOR_SERIA_INFO_HTML_INCLUDE}{seria_id}_id.html"
light_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{light_template}" light_template_w_path = f"{TEMPLATES[0]['DIRS'][0]}/{light_template}"
# print(f"{TEMPLATES[0]['DIRS'][0]}/{light_template}") is_hard_template = not os.path.isfile(light_template_w_path)
# print(light_template_w_path)
# print(light_template_w_path) to_template: dict[str, object] = {}
if os.path.isfile(light_template_w_path): # Получаем все уникальные проемы серии и сразу добавляем iQuantity=1
is_hard_template = False # для совместимости с get_flaps_for_big_pictures().
else: list_win_in_seria = list(
is_hard_template = True Win_MountDim.objects.filter(kApartment__kSeria_id=seria_id)
to_template: dict[str, object] ={} .annotate(iQuantity=Value(1, output_field=IntegerField()))
# получаем проемы использующиеся в данной серии домов .only(
q_windows_in_seria = Win_MountDim.objects.raw( "id",
f"SELECT DISTINCT" "iWinWidth",
f" oknardia_win_mountdim.iWinWidth, oknardia_win_mountdim.iWinHight," "iWinHight",
f" oknardia_win_mountdim.sDescripion, oknardia_win_mountdim.bIsDoor," "sDescripion",
f" oknardia_win_mountdim.bIsNearDoor, oknardia_win_mountdim.sFlapConfig," "bIsDoor",
f" oknardia_win_mountdim.iWinDepth, oknardia_win_mountdim.id," "bIsNearDoor",
f" 1 AS iQuantity " "sFlapConfig",
f"FROM oknardia_mountdim2apartment" "iWinDepth",
f" INNER JOIN oknardia_win_mountdim" )
f" ON oknardia_mountdim2apartment.kMountDim_id = oknardia_win_mountdim.id" .order_by("-bIsNearDoor", "-bIsDoor", "iWinWidth", "-iWinHight", "id")
f" INNER JOIN oknardia_apartment_type" .distinct()
f" ON oknardia_mountdim2apartment.kApartment_id = oknardia_apartment_type.id " )
f"WHERE oknardia_apartment_type.kSeria_id = {seria_id}"
f" ORDER BY oknardia_win_mountdim.bIsNearDoor DESC,"
f" oknardia_win_mountdim.bIsDoor DESC,"
f" oknardia_win_mountdim.iWinWidth,"
f" oknardia_win_mountdim.iWinHight DESC;")
if is_hard_template: if is_hard_template:
# Получаем данные для отрисовки больших картинок с проёмами и передаём в "тяжёлый" шаблон # Для "тяжелого" шаблона нужны большие картинки схем окон.
to_template.update(get_flaps_for_big_pictures(q_windows_in_seria)) to_template.update(get_flaps_for_big_pictures(list_win_in_seria))
# формируем строку для включения в SQL-запрос вида "(2,8,16,46,1)"
str_for_sql_in = "(" window_ids = [win.id for win in list_win_in_seria]
for count in q_windows_in_seria: apartments_in_seria = list(
str_for_sql_in += str(count.id) + "," Apartment_Type.objects.filter(kSeria_id=seria_id)
str_for_sql_in = str_for_sql_in[:-1] + ")" .values("id", "sNameApartment")
# print StringForSqlIN .order_by("iSort", "id")
# Получаем данные для таблички Окон по типам квартирах в серии дома )
# " IFNULL(oknardia_mountdim2apartment.iQuantity, 0) AS iQuantity," \ apartment_ids = [apartment["id"] for apartment in apartments_in_seria]
# tStart2 = time.perf_counter() # замер времени
q_win_in_apartment_in_seria = Win_MountDim.objects.raw( # Кэшируем количество проемов по паре (квартира, проем), чтобы не делать N*M обращений к БД.
f"SELECT" quantities_by_pair = {
f" oknardia_win_mountdim.id," (row["kApartment_id"], row["kMountDim_id"]): row["iQuantity"]
f" oknardia_apartment_type.sNameApartment," for row in MountDim2Apartment.objects.filter(
f" oknardia_win_mountdim.iWinWidth," kApartment_id__in=apartment_ids,
f" oknardia_win_mountdim.iWinHight," kMountDim_id__in=window_ids,
f" oknardia_apartment_type.id AS id_apart," ).values("kApartment_id", "kMountDim_id", "iQuantity")
f" IFNULL(oknardia_mountdim2apartment.iQuantity, 0) AS iQuantity," }
f" COUNT(oknardia_priceoffer.id) AS NumOffers " # Число офферов считаем один раз по каждому проему и переиспользуем при сборке таблицы.
f"FROM oknardia_apartment_type" offers_by_window = {
f" INNER JOIN oknardia_win_mountdim" row["kOffer2MountDim_id"]: row["num_offers"]
f" LEFT OUTER JOIN oknardia_mountdim2apartment" for row in PriceOffer.objects.filter(kOffer2MountDim_id__in=window_ids)
f" ON oknardia_mountdim2apartment.kMountDim_id = oknardia_win_mountdim.id" .values("kOffer2MountDim_id")
f" AND oknardia_mountdim2apartment.kApartment_id = oknardia_apartment_type.id" .annotate(num_offers=Count("id"))
f" LEFT OUTER JOIN oknardia_priceoffer" }
f" ON oknardia_priceoffer.kOffer2MountDim_id = oknardia_win_mountdim.id"
f" LEFT OUTER JOIN oknardia_ouruser"
f" ON oknardia_ouruser.id = oknardia_priceoffer.kOfferFromUser_id "
f"WHERE oknardia_apartment_type.kSeria_id = {seria_id} "
f"AND oknardia_win_mountdim.id IN {str_for_sql_in} "
f"GROUP BY oknardia_apartment_type.id,"
f" oknardia_apartment_type.sNameApartment,"
f" oknardia_win_mountdim.id,"
f" oknardia_mountdim2apartment.iQuantity "
f"ORDER BY oknardia_apartment_type.iSort,"
f" oknardia_win_mountdim.bIsNearDoor DESC,"
f" oknardia_win_mountdim.bIsDoor DESC,"
f" oknardia_win_mountdim.iWinWidth,"
f" oknardia_win_mountdim.iWinHight DESC;")
list_win_in_seria = list(q_windows_in_seria)
total_column = len(list_win_in_seria) - 1 total_column = len(list_win_in_seria) - 1
count_column = 0
min_offer_in_row = 1000000000
table_of_win_in_seria_by_apartmment = [] table_of_win_in_seria_by_apartmment = []
row_for_table = []
offer_and_merchant_per_win = [ offer_and_merchant_per_win = [
{ {
"WIN_OFFER": 0, "WIN_OFFER": offers_by_window.get(list_win_in_seria[i].id, 0),
"WIN_MERCHANT": 0, "WIN_MERCHANT": 0,
"WIN_W": list_win_in_seria[i].iWinWidth, "WIN_W": list_win_in_seria[i].iWinWidth,
"WIN_H": list_win_in_seria[i].iWinHight, "WIN_H": list_win_in_seria[i].iWinHight,
"WIN_ID": list_win_in_seria[i].id "WIN_ID": list_win_in_seria[i].id,
} for i in range(total_column + 1)] }
for count in q_win_in_apartment_in_seria: for i in range(total_column + 1)
if count.iQuantity != 0: ]
row_for_table.append({
"WIN_NUM": [chr(65 + count_column)], for apartment in apartments_in_seria:
"WIN_Q": count.iQuantity, row_for_table = []
"WIN_ID": count.id, # None = в строке квартиры еще не встретилось ни одного окна.
"WIN_WIDTH": list_win_in_seria[count_column].iWinWidth, min_offer_in_row = None
"WIN_HEIGHT": list_win_in_seria[count_column].iWinHight, for count_column, window in enumerate(list_win_in_seria):
"WIN_DESCRIPTION": list_win_in_seria[count_column].sDescripion, quantity = quantities_by_pair.get((apartment["id"], window.id), 0)
"WIN_FLAPCFG": list_win_in_seria[count_column].sFlapConfig if quantity != 0:
}) num_offers = offers_by_window.get(window.id, 0)
if min_offer_in_row > count.NumOffers: row_for_table.append(
min_offer_in_row = count.NumOffers {
if offer_and_merchant_per_win[count_column]["WIN_OFFER"] < count.NumOffers: "WIN_NUM": [chr(65 + count_column)],
offer_and_merchant_per_win[count_column]["WIN_OFFER"] = count.NumOffers "WIN_Q": quantity,
else: "WIN_ID": window.id,
row_for_table.append({"WIN_NUM": ""}) "WIN_WIDTH": window.iWinWidth,
if count_column < total_column: "WIN_HEIGHT": window.iWinHight,
count_column += 1 "WIN_DESCRIPTION": window.sDescripion,
else: "WIN_FLAPCFG": window.sFlapConfig,
# print row_for_table }
table_of_win_in_seria_by_apartmment.append({"WIN_IN_APART": row_for_table, )
"APART_NAME": count.sNameApartment, if min_offer_in_row is None or min_offer_in_row > num_offers:
"APART_ID": count.id_apart, min_offer_in_row = num_offers
"NUM_OFFERS": min_offer_in_row}) else:
count_column = 0 row_for_table.append({"WIN_NUM": ""})
min_offer_in_row = 10000
row_for_table = [] table_of_win_in_seria_by_apartmment.append(
# print(table_of_win_in_seria_by_apartmment) {
# print(f"==============>{float(time.perf_counter()-tStart2)}<==============") "WIN_IN_APART": row_for_table,
# print NumOffersPerColumn, NumMerchantPerColumn "APART_NAME": apartment["sNameApartment"],
to_template.update({"WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win, "APART_ID": apartment["id"],
"TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment}) # Если у серии нет ни одного окна, показываем 0 вместо служебного sentinel.
# для "тяжелого шаблона" получаем навигацию страницы, данные для карты и графика ввода в эксплуатацию "NUM_OFFERS": 0 if min_offer_in_row is None else min_offer_in_row,
}
)
to_template.update(
{
"WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win,
"TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment,
}
)
# Для "тяжелого" шаблона получаем навигацию, карту и график, затем кэшируем pre-render.
if is_hard_template: if is_hard_template:
# если вызывается "тяжелый" шаблон, то нужно подготовить тяжелые данные для построения навигации
seria_id, for_seria_nav = seria_nav(seria_id) seria_id, for_seria_nav = seria_nav(seria_id)
to_template.update(for_seria_nav) # данные для навигации по сериям to_template.update(for_seria_nav)
to_template.update(seria_info_year(seria_id)) # данные для графика ввода зданий серии в эксплуатацию to_template.update(seria_info_year(seria_id))
to_template.update(seria_info_geo_code(seria_id)) # данные для карты to_template.update(seria_info_geo_code(seria_id))
# т.к. обрабатывается "тяжелый шаблон" надо создать "легкий шаблон"
# для его использования в будущем.
string_prerender = render_to_string("seria_info/all_seria_info_pre_light.html", to_template) string_prerender = render_to_string("seria_info/all_seria_info_pre_light.html", to_template)
file = open(light_template_w_path, 'w') with open(light_template_w_path, "w", encoding="utf-8") as file:
# file.write(AA.encode('utf-8')) file.write(string_prerender)
file.write(string_prerender)
file.close()
touch_reload_wsgi(light_template_w_path) touch_reload_wsgi(light_template_w_path)
else: else:
seria_name = Seria_Info.objects.get(id=seria_id).sName to_template.update({"THIS_SERIA_NAME": q_seria.sName})
to_template.update({'THIS_SERIA_NAME': seria_name})
_append_visit_context(to_template, request, time_start) _append_visit_context(to_template, request, time_start)
return render(request, light_template, to_template) return render(request, light_template, to_template)
def seria_nav(seria_id: int = 12) -> (int, dict): def seria_nav(seria_id: int = DEFAULT_SERIA_ID_FOR_CATALOG) -> tuple[int, dict]:
""" """
Возвращает корректный seria_id и кортеж для построения навигации по сериям дома Возвращает корректный seria_id и данные навигации по корневым сериям.
Если переданный seria_id невалиден, подбирает ближайший допустимый root-id.
:param seria_id: id серии :param seria_id: id серии
:return: :return: tuple[int, dict] -- (seria_id, {"SERIA_NAV_DIM": ..., "THIS_SERIA_*": ...})
""" """
q_seria = Seria_Info.objects.raw( q_seria = list(
'SELECT oknardia_seria_info.id,' Seria_Info.objects.filter(id=F("kRoot_id"))
' oknardia_seria_info.sName,' .only("id", "sName", "sSeriaDescription", "kRoot_id", "kParent_id")
' oknardia_seria_info.sSeriaDescription,' .order_by("sName")
' oknardia_seria_info.kRoot_id,' )
' oknardia_seria_info.kParent_id ' if not q_seria:
'FROM oknardia_seria_info ' return seria_id, {"SERIA_NAV_DIM": []}
'WHERE oknardia_seria_info.id = oknardia_seria_info.kRoot_id '
'ORDER BY oknardia_seria_info.sName;')
error_seria = True error_seria = True
for count_seria in q_seria: for count_seria in q_seria:
if count_seria.id == int(seria_id): if count_seria.id == int(seria_id):
@@ -248,9 +246,9 @@ def seria_nav(seria_id: int = 12) -> (int, dict):
# базовая серия прописана в kRoot_id # базовая серия прописана в kRoot_id
seria_id = query.kRoot_id seria_id = query.kRoot_id
else: else:
# == корневой нет # Корневой серии нет.
# == ищем методом наименьших расстояний" # Ищем методом наименьших расстояний
min_min = 100000000 min_min = 100_000_000
min_id = seria_id min_id = seria_id
for count_seria in q_seria: for count_seria in q_seria:
if math.fabs(int(seria_id) - count_seria.id) < min_min: if math.fabs(int(seria_id) - count_seria.id) < min_min:
@@ -259,33 +257,46 @@ def seria_nav(seria_id: int = 12) -> (int, dict):
seria_id = min_id seria_id = min_id
except ObjectDoesNotExist: except ObjectDoesNotExist:
seria_id = q_seria[0].id seria_id = q_seria[0].id
# print(f"-->{seria_id}<--")
return all_seria_nav(seria_id, q_seria) return all_seria_nav(seria_id, q_seria)
def all_seria_nav(seria_id: int, q_seria) -> (int, dict): def all_seria_nav(seria_id: int, q_seria) -> tuple[int, dict]:
"""
Формирует структуру навигации по сериям для шаблонов.
:param seria_id: активный id серии
:param q_seria: коллекция серий (ORM-объекты или dict из values())
:return: tuple[int, dict] -- (seria_id, словарь с SERIA_NAV_DIM и данными активной серии)
"""
seria_nav_dim = [] seria_nav_dim = []
this_return = {} this_return = {}
# Поддерживаем оба формата входных элементов: ORM-объекты и dict из values().
for count_seria in q_seria: for count_seria in q_seria:
one_seria = {} seria_name = count_seria["sName"] if isinstance(count_seria, dict) else count_seria.sName
one_seria.update({"SERIA_R": count_seria.sName, "ID2URL": count_seria.id}) seria_id_value = count_seria["id"] if isinstance(count_seria, dict) else count_seria.id
if count_seria.id == seria_id: seria_description = (
this_return.update({"THIS_SERIA_NAME": count_seria.sName, count_seria.get("sSeriaDescription")
"THIS_SERIA_DESCRIPTION": count_seria.sSeriaDescription}) if isinstance(count_seria, dict)
# one_seria.update({"SERIA_L": ""}) else count_seria.sSeriaDescription
one_seria.update({"SERIA_L": pytils.translit.slugify(count_seria.sName)}) )
else: one_seria = {
one_seria.update({"SERIA_L": pytils.translit.slugify(count_seria.sName)}) "SERIA_R": seria_name,
"ID2URL": seria_id_value,
"SERIA_L": pytils.translit.slugify(seria_name),
}
if seria_id_value == seria_id:
this_return.update({"THIS_SERIA_NAME": seria_name,
"THIS_SERIA_DESCRIPTION": seria_description})
seria_nav_dim.append(one_seria) seria_nav_dim.append(one_seria)
this_return.update({"SERIA_NAV_DIM": seria_nav_dim}) this_return.update({"SERIA_NAV_DIM": seria_nav_dim})
return seria_id, this_return return seria_id, this_return
def seria_info_year(seria_id: int = 12) -> dict: def seria_info_year(seria_id: int = DEFAULT_SERIA_ID_FOR_CATALOG) -> dict:
""" Возвращает данные для графика распределения сдачи серии в эксплуатацию """Возвращает данные для графика ввода домов серии в эксплуатацию.
:param seria_id: int -- id серии для которой нужно получить данные :param seria_id: int -- id корневой серии
:return: dict -- данные для графика распределения сдачи серии в эксплуатацию типа: :return: dict -- данные для графика по годам вида:
{"DATA4GRAPH": [{'YEAR': 1997, 'NUMS': 1, 'CLRS': '99'}, {"DATA4GRAPH": [{'YEAR': 1997, 'NUMS': 1, 'CLRS': '99'},
{'YEAR': 1998, 'NUMS': 15, 'CLRS': 'сс'}, {'YEAR': 1998, 'NUMS': 15, 'CLRS': 'сс'},
{'YEAR': 1998, 'NUMS': 10, 'CLRS': 'a9'} {'YEAR': 1998, 'NUMS': 10, 'CLRS': 'a9'}
@@ -293,43 +304,52 @@ def seria_info_year(seria_id: int = 12) -> dict:
} }
""" """
seria_in_years = [] seria_in_years = []
query = Seria_Info.objects.raw( query = list(
f"SELECT oknardia_building_info.iCommissioning_year as id," Building_Info.objects.filter(kSeria_Link__kRoot_id=seria_id)
f" COUNT(oknardia_building_info.iCommissioning_year) AS NumInYear " .values("iCommissioning_year")
f"FROM oknardia_building_info" .annotate(NumInYear=Count("iCommissioning_year"))
f" INNER JOIN oknardia_seria_info" .order_by("iCommissioning_year")
f" ON oknardia_building_info.kSeria_Link_id = oknardia_seria_info.id "
f"WHERE oknardia_seria_info.kRoot_id = {seria_id} "
f"GROUP BY oknardia_building_info.iCommissioning_year;"
) )
max_per_year = 0 max_per_year = 0
graph_color_light = 0xCC # самый светлый цвет на графике (максимальное значение) graph_color_light = 0xCC # самый светлый цвет на графике (максимальное значение)
graph_color_dark = 0x99 # самый темный цвет на графике (минимальное значение) graph_color_dark = 0x99 # самый темный цвет на графике (минимальное значение)
for YearCount in query: for year_count in query:
if int(YearCount.NumInYear) > max_per_year: if int(year_count["NumInYear"]) > max_per_year:
max_per_year = int(YearCount.NumInYear) max_per_year = int(year_count["NumInYear"])
# print("max", MaxPerYear) for year_count in query:
for YearCount in query:
data_of_year = {} data_of_year = {}
try: try:
data_of_year.update({ data_of_year.update({
"YEAR": int(YearCount.id), "YEAR": int(year_count["iCommissioning_year"]),
"NUMS": YearCount.NumInYear, "NUMS": year_count["NumInYear"],
"CLRS": str(hex(int(graph_color_dark + YearCount.NumInYear * ( "CLRS": str(hex(int(graph_color_dark + year_count["NumInYear"] * (
graph_color_light - graph_color_dark) / max_per_year)))[2:] graph_color_light - graph_color_dark) / max_per_year)))[2:]
}) })
except ValueError: except ValueError:
continue continue
seria_in_years.append(data_of_year) seria_in_years.append(data_of_year)
# print(seria_in_years)
return {"DATA4GRAPH": seria_in_years} return {"DATA4GRAPH": seria_in_years}
def seria_info_geo_code(seria_id: str = '12') -> dict: def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> dict:
""" Возвращает массив геокоординат зданий одной серии """Возвращает гео-точки и агрегированную статистику по серии.
:param seria_id: str -- id серии для которой нужно получить данные Кроме массива координат, функция считает суммарные показатели серии:
:return: dict -- массив геокоординат зданий серии жилые/муниципальные/государственные площади, число жителей, квартир,
лицевых счетов и диапазон показателя состояния домов.
:param seria_id: int | str -- id серии, для которой нужно получить данные
:return: dict -- {
"DATA4GEO": [...],
"MUNICIPAL_M2": ...,
"RESIDENTIAL_M2": ...,
"GOVERNMENT_M2": ...,
"RESIDENTS": ...,
"APARTMENTS": ...,
"ACCOUNTS": ...,
"CONDITION_MAX": ...,
"CONDITION_MIN": ...,
}
""" """
data_return = {} data_return = {}
seria_to_geo = [] seria_to_geo = []
@@ -337,57 +357,61 @@ def seria_info_geo_code(seria_id: str = '12') -> dict:
residential_m2 = 0 # жилой фонд (кв.м) residential_m2 = 0 # жилой фонд (кв.м)
government_m2 = 0 # государственные учреждения занимают (кв.м.) government_m2 = 0 # государственные учреждения занимают (кв.м.)
residents = 0 # количество жильцов residents = 0 # количество жильцов
apartments = 0 # число квартиры apartments = 0 # число квартир
accounts = 0 # количество лицевых счетов accounts = 0 # количество лицевых счетов
condition_max = 0 # максимальное значение показателя состояния здания condition_max = 0 # максимальное значение показателя состояния здания
condition_min = 1000000 # минимальное значение показателя состояния здания condition_min = 1_000_000 # минимальное значение показателя состояния здания
query = Building_Info.objects.raw( query = Building_Info.objects.filter(kSeria_Link__kRoot_id=int(seria_id)).values(
f"SELECT" "id",
f" oknardia_building_info.id," "kSeria_Link__kRoot_id",
f" oknardia_seria_info.kRoot_id as SerId," "sAddress",
f" oknardia_building_info.sAddress," "fResidential_Area",
f" oknardia_building_info.fResidential_Area," "fMunicipal_Area",
f" oknardia_building_info.fMunicipal_Area," "fGovernment_Area",
f" oknardia_building_info.fGovernment_Area," "iNum_Residents",
f" oknardia_building_info.iNum_Residents," "iNum_Apartments",
f" oknardia_building_info.iNum_Apartments," "iNum_Accounts",
f" oknardia_building_info.iNum_Accounts," "fCondition_House",
f" oknardia_building_info.fCondition_House," "fGeoCode_Latitude",
f" oknardia_building_info.fGeoCode_Latitude," "fGeoCode_Longitude",
f" oknardia_building_info.fGeoCode_Longitude "
f"FROM oknardia_building_info"
f" INNER JOIN oknardia_seria_info"
f" ON oknardia_building_info.kSeria_Link_id = oknardia_seria_info.id "
f"WHERE oknardia_seria_info.kRoot_id IN ({seria_id});"
) )
for count in query: # iterator() уменьшает пиковое потребление памяти на больших сериях домов.
if int(count.fGeoCode_Latitude) != 0 and int(count.fGeoCode_Longitude) != 0: for count in query.iterator(chunk_size=500):
seria_to_geo.append({"LATITUDE": count.fGeoCode_Latitude, latitude = count["fGeoCode_Latitude"] or 0
"LONGITUDE": count.fGeoCode_Longitude, longitude = count["fGeoCode_Longitude"] or 0
"ADDR_ID": count.id, municipal_area = count["fMunicipal_Area"] or 0
"ADDR_LAT": pytils.translit.slugify(count.sAddress), residential_area = count["fResidential_Area"] or 0
"ADDR_RUS": count.sAddress, government_area = count["fGovernment_Area"] or 0
"SER_ID": count.SerId num_residents = count["iNum_Residents"] or 0
num_apartments = count["iNum_Apartments"] or 0
num_accounts = count["iNum_Accounts"] or 0
house_condition = count["fCondition_House"] or 0
if int(latitude) != 0 and int(longitude) != 0:
seria_to_geo.append({"LATITUDE": latitude,
"LONGITUDE": longitude,
"ADDR_ID": count["id"],
"ADDR_LAT": pytils.translit.slugify(count["sAddress"]),
"ADDR_RUS": count["sAddress"],
"SER_ID": count["kSeria_Link__kRoot_id"]
}) })
if count.fMunicipal_Area > 0: if municipal_area > 0:
municipal_m2 += count.fMunicipal_Area municipal_m2 += municipal_area
if count.fResidential_Area > 0: if residential_area > 0:
residential_m2 += count.fResidential_Area residential_m2 += residential_area
if count.fGovernment_Area > 0: if government_area > 0:
government_m2 += count.fGovernment_Area government_m2 += government_area
if count.iNum_Residents > 0: if num_residents > 0:
residents += count.iNum_Residents residents += num_residents
if count.iNum_Residents > 0: if num_apartments > 0:
residents += count.iNum_Residents apartments += num_apartments
if count.iNum_Apartments > 0: if num_accounts > 0:
apartments += count.iNum_Apartments accounts += num_accounts
if count.iNum_Accounts > 0: if house_condition > 0:
accounts += count.iNum_Accounts if house_condition > condition_max:
if count.fCondition_House > 0: condition_max = house_condition
if count.fCondition_House > condition_max: if house_condition < condition_min:
condition_max = count.fCondition_House condition_min = house_condition
if count.fCondition_House < condition_min:
condition_min = count.fCondition_House
data_return.update({"DATA4GEO": seria_to_geo, data_return.update({"DATA4GEO": seria_to_geo,
"MUNICIPAL_M2": municipal_m2, "MUNICIPAL_M2": municipal_m2,
"RESIDENTIAL_M2": residential_m2, "RESIDENTIAL_M2": residential_m2,
@@ -397,5 +421,4 @@ def seria_info_geo_code(seria_id: str = '12') -> dict:
"ACCOUNTS": accounts, "ACCOUNTS": accounts,
"CONDITION_MAX": condition_max, "CONDITION_MAX": condition_max,
"CONDITION_MIN": condition_min}) "CONDITION_MIN": condition_min})
# print(seria_to_geo)
return data_return return data_return