mod: refactor catalog_seria_info and update README/settings
This commit is contained in:
@@ -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 шаблонов, гео+статистика серии).
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
# папка для хранения изображений
|
# папка для хранения изображений
|
||||||
|
|||||||
@@ -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)
|
|
||||||
if os.path.isfile(light_template_w_path):
|
|
||||||
is_hard_template = False
|
|
||||||
else:
|
|
||||||
is_hard_template = True
|
|
||||||
to_template: dict[str, object] = {}
|
to_template: dict[str, object] = {}
|
||||||
# получаем проемы использующиеся в данной серии домов
|
# Получаем все уникальные проемы серии и сразу добавляем iQuantity=1
|
||||||
q_windows_in_seria = Win_MountDim.objects.raw(
|
# для совместимости с get_flaps_for_big_pictures().
|
||||||
f"SELECT DISTINCT"
|
list_win_in_seria = list(
|
||||||
f" oknardia_win_mountdim.iWinWidth, oknardia_win_mountdim.iWinHight,"
|
Win_MountDim.objects.filter(kApartment__kSeria_id=seria_id)
|
||||||
f" oknardia_win_mountdim.sDescripion, oknardia_win_mountdim.bIsDoor,"
|
.annotate(iQuantity=Value(1, output_field=IntegerField()))
|
||||||
f" oknardia_win_mountdim.bIsNearDoor, oknardia_win_mountdim.sFlapConfig,"
|
.only(
|
||||||
f" oknardia_win_mountdim.iWinDepth, oknardia_win_mountdim.id,"
|
"id",
|
||||||
f" 1 AS iQuantity "
|
"iWinWidth",
|
||||||
f"FROM oknardia_mountdim2apartment"
|
"iWinHight",
|
||||||
f" INNER JOIN oknardia_win_mountdim"
|
"sDescripion",
|
||||||
f" ON oknardia_mountdim2apartment.kMountDim_id = oknardia_win_mountdim.id"
|
"bIsDoor",
|
||||||
f" INNER JOIN oknardia_apartment_type"
|
"bIsNearDoor",
|
||||||
f" ON oknardia_mountdim2apartment.kApartment_id = oknardia_apartment_type.id "
|
"sFlapConfig",
|
||||||
f"WHERE oknardia_apartment_type.kSeria_id = {seria_id}"
|
"iWinDepth",
|
||||||
f" ORDER BY oknardia_win_mountdim.bIsNearDoor DESC,"
|
)
|
||||||
f" oknardia_win_mountdim.bIsDoor DESC,"
|
.order_by("-bIsNearDoor", "-bIsDoor", "iWinWidth", "-iWinHight", "id")
|
||||||
f" oknardia_win_mountdim.iWinWidth,"
|
.distinct()
|
||||||
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({
|
|
||||||
|
for apartment in apartments_in_seria:
|
||||||
|
row_for_table = []
|
||||||
|
# None = в строке квартиры еще не встретилось ни одного окна.
|
||||||
|
min_offer_in_row = None
|
||||||
|
for count_column, window in enumerate(list_win_in_seria):
|
||||||
|
quantity = quantities_by_pair.get((apartment["id"], window.id), 0)
|
||||||
|
if quantity != 0:
|
||||||
|
num_offers = offers_by_window.get(window.id, 0)
|
||||||
|
row_for_table.append(
|
||||||
|
{
|
||||||
"WIN_NUM": [chr(65 + count_column)],
|
"WIN_NUM": [chr(65 + count_column)],
|
||||||
"WIN_Q": count.iQuantity,
|
"WIN_Q": quantity,
|
||||||
"WIN_ID": count.id,
|
"WIN_ID": window.id,
|
||||||
"WIN_WIDTH": list_win_in_seria[count_column].iWinWidth,
|
"WIN_WIDTH": window.iWinWidth,
|
||||||
"WIN_HEIGHT": list_win_in_seria[count_column].iWinHight,
|
"WIN_HEIGHT": window.iWinHight,
|
||||||
"WIN_DESCRIPTION": list_win_in_seria[count_column].sDescripion,
|
"WIN_DESCRIPTION": window.sDescripion,
|
||||||
"WIN_FLAPCFG": list_win_in_seria[count_column].sFlapConfig
|
"WIN_FLAPCFG": window.sFlapConfig,
|
||||||
})
|
}
|
||||||
if min_offer_in_row > count.NumOffers:
|
)
|
||||||
min_offer_in_row = count.NumOffers
|
if min_offer_in_row is None or min_offer_in_row > num_offers:
|
||||||
if offer_and_merchant_per_win[count_column]["WIN_OFFER"] < count.NumOffers:
|
min_offer_in_row = num_offers
|
||||||
offer_and_merchant_per_win[count_column]["WIN_OFFER"] = count.NumOffers
|
|
||||||
else:
|
else:
|
||||||
row_for_table.append({"WIN_NUM": "—"})
|
row_for_table.append({"WIN_NUM": "—"})
|
||||||
if count_column < total_column:
|
|
||||||
count_column += 1
|
table_of_win_in_seria_by_apartmment.append(
|
||||||
else:
|
{
|
||||||
# print row_for_table
|
"WIN_IN_APART": row_for_table,
|
||||||
table_of_win_in_seria_by_apartmment.append({"WIN_IN_APART": row_for_table,
|
"APART_NAME": apartment["sNameApartment"],
|
||||||
"APART_NAME": count.sNameApartment,
|
"APART_ID": apartment["id"],
|
||||||
"APART_ID": count.id_apart,
|
# Если у серии нет ни одного окна, показываем 0 вместо служебного sentinel.
|
||||||
"NUM_OFFERS": min_offer_in_row})
|
"NUM_OFFERS": 0 if min_offer_in_row is None else min_offer_in_row,
|
||||||
count_column = 0
|
}
|
||||||
min_offer_in_row = 10000
|
)
|
||||||
row_for_table = []
|
|
||||||
# print(table_of_win_in_seria_by_apartmment)
|
to_template.update(
|
||||||
# print(f"==============>{float(time.perf_counter()-tStart2)}<==============")
|
{
|
||||||
# print NumOffersPerColumn, NumMerchantPerColumn
|
"WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win,
|
||||||
to_template.update({"WIN_OFFER_AND_MERCHANT": offer_and_merchant_per_win,
|
"TABLE_OF_WINDOWS": table_of_win_in_seria_by_apartmment,
|
||||||
"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
|
||||||
|
|||||||
Reference in New Issue
Block a user