479 lines
28 KiB
Python
479 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
||
# from django.shortcuts import render, redirect
|
||
from django.shortcuts import render, redirect
|
||
from django.http import HttpRequest, HttpResponse
|
||
from django.utils.dateformat import format
|
||
from django.utils import timezone
|
||
from django.db.models import F, Q, ExpressionWrapper, BooleanField, Max, Count, Avg
|
||
from oknardia.models import LogVisitPriceReport, SetKit
|
||
from oknardia.settings import *
|
||
from web.add_func import normalize, get_rating_set_for_stars, sum_through
|
||
# from time import time
|
||
import django.utils.dateformat
|
||
import time
|
||
import json
|
||
import re
|
||
import pytils
|
||
|
||
# Сигнальные значения для поиска min/max: заведомо вне диапазона реальных данных
|
||
_INI_MAX = -100_000
|
||
_INI_MIN = 1_000_000
|
||
|
||
|
||
def _color_hi(value, val_min: float, val_max: float, threshold=None, epsilon: float = 0.001) -> str | None:
|
||
"""Цвет ячейки "чем больше, тем лучше": зеленее → значение ближе к max.
|
||
|
||
:param value: значение поля для текущей строки
|
||
:param val_min: минимум по всей выборке (из первого прогона)
|
||
:param val_max: максимум по всей выборке
|
||
:param threshold: нижний порог: значения <= threshold считаются "нет данных" и не окрашиваются
|
||
:param epsilon: минимальный разброс, при котором окраска имеет смысл
|
||
:return: hex-строка цвета или None
|
||
"""
|
||
try:
|
||
v = float(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
if val_max == _INI_MAX or val_min == _INI_MIN or val_max - val_min < epsilon:
|
||
return None
|
||
if threshold is not None and v <= threshold:
|
||
return None
|
||
if v <= val_min:
|
||
return None
|
||
ratio = (v - val_min) / (val_max - val_min)
|
||
c = 255 - int(ratio * 128)
|
||
return f"#{c:02x}ff{c:02x}"
|
||
|
||
|
||
def _color_lo(value, val_min: float, val_max: float, threshold=None, epsilon: float = 0.001) -> str | None:
|
||
"""Цвет ячейки "чем меньше, тем лучше": зеленее → значение ближе к min.
|
||
|
||
:param value: значение поля для текущей строки
|
||
:param val_min: минимум по всей выборке
|
||
:param val_max: максимум по всей выборке
|
||
:param threshold: нижний порог: значения <= threshold не окрашиваются
|
||
:param epsilon: минимальный разброс
|
||
:return: hex-строка цвета или None
|
||
"""
|
||
try:
|
||
v = float(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
if val_max == _INI_MAX or val_min == _INI_MIN or val_max - val_min < epsilon:
|
||
return None
|
||
if threshold is not None and v <= threshold:
|
||
return None
|
||
if v >= val_max:
|
||
return None
|
||
ratio = (v - val_min) / (val_max - val_min)
|
||
c = 127 + int(ratio * 128)
|
||
return f"#{c:02x}ff{c:02x}"
|
||
|
||
|
||
def _bounds(items: list, field: str, threshold=None) -> tuple[float, float]:
|
||
"""Вычисляет (min, max) значений поля field по списку items, игнорируя None и <= threshold.
|
||
|
||
:param items: список объектов (SetKit с аннотациями)
|
||
:param field: имя атрибута
|
||
:param threshold: значения <= threshold исключаются из выборки
|
||
:return: (min, max) или (_INI_MIN, _INI_MAX) если нет валидных значений
|
||
"""
|
||
vals = []
|
||
for item in items:
|
||
raw = getattr(item, field, None)
|
||
if raw is None:
|
||
continue
|
||
try:
|
||
v = float(raw)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if threshold is not None and v <= threshold:
|
||
continue
|
||
vals.append(v)
|
||
if not vals:
|
||
return _INI_MIN, _INI_MAX
|
||
return min(vals), max(vals)
|
||
|
||
|
||
def get_last_user_visit_cookies(request: HttpRequest) -> list:
|
||
""" Служебная функция: проверяет есть ли куки о последних посещениях пользователя, и если есть возвращает их
|
||
|
||
:param request: HttpRequest -- входящий http-запрос
|
||
:return LastVisit: json -- загруженный json-объект из куки LastVisit
|
||
"""
|
||
if "LastVisit" in request.COOKIES:
|
||
try:
|
||
return json.loads(request.COOKIES["LastVisit"])
|
||
except (json.decoder.JSONDecodeError, TypeError, ValueError, KeyError, AttributeError):
|
||
return []
|
||
else:
|
||
return []
|
||
|
||
|
||
def get_last_user_visit_list(list_visit: list) -> list:
|
||
""" Служебная функция: получает список с посещенных страниц с ценовой выдачей (ListVisit), меняет в нем даты
|
||
на описание типа "три недели назад" и возвращает обратно.
|
||
|
||
:param list_visit:
|
||
:return:
|
||
"""
|
||
result_list_visit = []
|
||
for i in list_visit:
|
||
result_list_visit.append({
|
||
"Time": pytils.dt.distance_of_time_in_words(int(i["Time"])),
|
||
"LastURL": i["LastURL"],
|
||
"LastAddress": i["LastAddress"],
|
||
"LastApart": i["LastApart"]
|
||
})
|
||
return result_list_visit
|
||
|
||
|
||
# def get_last_all_user_visit_list() -> tuple:
|
||
def get_last_all_user_visit_list() -> list:
|
||
""" Служебная функция: получает список с посещенных страниц с ценовой выдачей для всех пользователей из DB
|
||
|
||
:return: list -- список четырех последних посещений ценовых предложений всеми пользователями
|
||
"""
|
||
result_list_visit = []
|
||
# id_last_visit = 0 # id четвертого посещения??? Зачем??? Не помню хоть убей!!!
|
||
try:
|
||
q_log_visit = LogVisitPriceReport.objects.all().order_by('-dLogVisitTime')[:4]
|
||
for i in q_log_visit:
|
||
# if id_last_visit == 0:
|
||
# id_last_visit = i.id
|
||
result_list_visit.append({
|
||
"id": i.id,
|
||
"Time": pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.dLogVisitTime, 'U'))),
|
||
"LogURL": i.sLogURL,
|
||
"LogAddress": i.sLogAddress,
|
||
"LogApart": i.sLogNameApartment
|
||
})
|
||
except LogVisitPriceReport.DoesNotExist:
|
||
pass
|
||
# return id_last_visit+1, list_visit
|
||
return result_list_visit
|
||
|
||
|
||
def compare_offers(request: HttpRequest, to_compare: str = "1,2") -> HttpResponse:
|
||
""" Сравнение нескольких коммерческих предложений (оконных набора).
|
||
|
||
:param request: HttpRequest -- входящий http-запрос
|
||
:param to_compare: str -- список ,через запятую, id оконных наборов (Set) для сравнения
|
||
:return: HttpResponse --
|
||
"""
|
||
time_start = time.perf_counter()
|
||
to_template: dict[str, object] = {}
|
||
try:
|
||
# Этот блок нужен для 302-переадресации, когда разные URL отдают одинаковые страницы.
|
||
# Например, такое происходит для страницы: /compare_offers/1,2 и /compare_offers/2,1
|
||
# т.е. сравнивают одни и те же наборы, но они указаны в строке сравнения в разном порядке
|
||
# ----------------------------------------------------------------------------------------------
|
||
# получил строку: to_compare
|
||
# превращаем в список
|
||
list_input = to_compare.split(",")
|
||
list_fin = []
|
||
# убираем мусор и создаем список только из целых чисел (listResult)
|
||
for i in list_input:
|
||
try:
|
||
list_fin.append(int(i))
|
||
except ValueError:
|
||
continue
|
||
if len(list_fin) < 2:
|
||
return redirect("/compare_offers/1,2")
|
||
# Сортируем этот список (list_fin)
|
||
list_fin.sort()
|
||
# Превращаем список list_fin в строку list_fine (разделитель -- запятая)
|
||
list_fine = ','.join(map(str, list_fin))
|
||
# Сравниваем входной порядок параметров и отсоветованный. Если не совпадает, то переадресация-302
|
||
if to_compare != list_fine:
|
||
return redirect(f"/compare_offers/{list_fine}")
|
||
try:
|
||
q_set_kit = (
|
||
SetKit.objects
|
||
.filter(id__in=list_fin)
|
||
.annotate(
|
||
# Активность коммерческого предложения (аналог dSetCommercialUntil > NOW())
|
||
bCommercial=ExpressionWrapper(
|
||
Q(dSetCommercialUntil__gt=timezone.now()),
|
||
output_field=BooleanField()
|
||
),
|
||
# Алиасы из MerchantBrand (SetKit → OurUser → MerchantOffice → MerchantBrand)
|
||
MERCHANT_ID=F('kSet2User__kMerchantOffice__kMerchantName'),
|
||
sMerchantName=F('kSet2User__kMerchantOffice__kMerchantName__sMerchantName'),
|
||
sMerchantMainURL=F('kSet2User__kMerchantOffice__kMerchantName__sMerchantMainURL'),
|
||
pMerchantLogo=F('kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo'),
|
||
# Алиасы из PVCprofiles (SetKit → kSet2PVCprofiles)
|
||
PROFILE_ID=F('kSet2PVCprofiles'),
|
||
sProfileName=F('kSet2PVCprofiles__sProfileName'),
|
||
sProfileBriefDescription=F('kSet2PVCprofiles__sProfileBriefDescription'),
|
||
sProfileManufacturer=F('kSet2PVCprofiles__sProfileManufacturer'),
|
||
sProfileColor=F('kSet2PVCprofiles__sProfileColor'),
|
||
iProfileCameras=F('kSet2PVCprofiles__iProfileCameras'),
|
||
iProfileThickness=F('kSet2PVCprofiles__iProfileThickness'),
|
||
iProfileGlazingThickness=F('kSet2PVCprofiles__iProfileGlazingThickness'),
|
||
fProfileHeatTransf=F('kSet2PVCprofiles__fProfileHeatTransf'),
|
||
fProfileSeals=F('kSet2PVCprofiles__fProfileSeals'),
|
||
sProfileSealDescription=F('kSet2PVCprofiles__sProfileSealDescription'),
|
||
fProfileSoundproofing=F('kSet2PVCprofiles__fProfileSoundproofing'),
|
||
iProfileHeight=F('kSet2PVCprofiles__iProfileHeight'),
|
||
iProfileRabbet=F('kSet2PVCprofiles__iProfileRabbet'),
|
||
sProfileFillet=F('kSet2PVCprofiles__sProfileFillet'),
|
||
sProfileReinforcement=F('kSet2PVCprofiles__sProfileReinforcement'),
|
||
sProfileOther=F('kSet2PVCprofiles__sProfileOther'),
|
||
fProfileRating=F('kSet2PVCprofiles__fProfileRating'),
|
||
sProfileDescription=F('kSet2PVCprofiles__sProfileDescription'),
|
||
# Алиасы из Glazing (SetKit → kSet2Glazing)
|
||
iGlazingCamerasN=F('kSet2Glazing__iGlazingCamerasN'),
|
||
iGlazingThickness=F('kSet2Glazing__iGlazingThickness'),
|
||
sGlazingBriefDescription=F('kSet2Glazing__sGlazingBriefDescription'),
|
||
sGlazingDescription=F('kSet2Glazing__sGlazingDescription'),
|
||
sGlazingMark=F('kSet2Glazing__sGlazingMark'),
|
||
sGlazingManufacturer=F('kSet2Glazing__sGlazingManufacturer'),
|
||
fGlazingHeatTransfer=F('kSet2Glazing__fGlazingHeatTransfer'),
|
||
fGlazingSoundproofing=F('kSet2Glazing__fGlazingSoundproofing'),
|
||
fGlazingLightTransmission=F('kSet2Glazing__fGlazingLightTransmission'),
|
||
sGlazingLightReflectance=F('kSet2Glazing__sGlazingLightReflectance'),
|
||
fGlazingPassingSun=F('kSet2Glazing__fGlazingPassingSun'),
|
||
sGlazingReflectionAndAbsorptionOfHeat=F('kSet2Glazing__sGlazingReflectionAndAbsorptionOfHeat'),
|
||
sGlazingToning=F('kSet2Glazing__sGlazingToning'),
|
||
fGlazingRating=F('kSet2Glazing__fGlazingRating'),
|
||
)
|
||
)
|
||
except SetKit.DoesNotExist:
|
||
return redirect("/compare_offers/1,2")
|
||
list_set_kit = list(q_set_kit)
|
||
if len(list_set_kit) == 0:
|
||
return redirect("/compare_offers/1,2")
|
||
if len(list_set_kit) == 1:
|
||
return redirect(f"/specification_set/{list_set_kit[0].id}")
|
||
except (ValueError, TypeError):
|
||
return render("/compare_offers/1,2")
|
||
# ПРЕДВАРИТЕЛЬНЫЙ "ПРОГОН"
|
||
# Вычисляем min/max по каждому параметру для дальнейшей покраски ячеек.
|
||
# Камеры профиля требуют sum_through() — обрабатываем отдельно.
|
||
cameras_vals = [
|
||
c for i in list_set_kit
|
||
if (c := sum_through(i.iProfileCameras)) is not None and c > 0
|
||
]
|
||
min_cameras = min(cameras_vals) if cameras_vals else _INI_MIN
|
||
max_cameras = max(cameras_vals) if cameras_vals else _INI_MAX
|
||
|
||
# Остальные поля — через _bounds() с соответствующими порогами
|
||
# (threshold: значения <= порога считаются "нет данных" и исключаются из диапазона)
|
||
min_seals, max_seals = _bounds(list_set_kit, 'fProfileSeals', threshold=0)
|
||
min_thick, max_thick = _bounds(list_set_kit, 'iProfileThickness', threshold=10)
|
||
min_glaz_d, max_glaz_d = _bounds(list_set_kit, 'iProfileGlazingThickness', threshold=4)
|
||
min_heat_p, max_heat_p = _bounds(list_set_kit, 'fProfileHeatTransf', threshold=0)
|
||
min_sound_p, max_sound_p = _bounds(list_set_kit, 'fProfileSoundproofing', threshold=0)
|
||
min_rabbet, max_rabbet = _bounds(list_set_kit, 'iProfileRabbet', threshold=1)
|
||
min_height, max_height = _bounds(list_set_kit, 'iProfileHeight', threshold=12)
|
||
min_gl_cam, max_gl_cam = _bounds(list_set_kit, 'iGlazingCamerasN', threshold=0)
|
||
min_gl_thick, max_gl_thick = _bounds(list_set_kit, 'iGlazingThickness', threshold=3)
|
||
min_heat_g, max_heat_g = _bounds(list_set_kit, 'fGlazingHeatTransfer', threshold=0.05)
|
||
min_sound_g, max_sound_g = _bounds(list_set_kit, 'fGlazingSoundproofing', threshold=5)
|
||
min_light, max_light = _bounds(list_set_kit, 'fGlazingLightTransmission', threshold=5)
|
||
min_sun, max_sun = _bounds(list_set_kit, 'fGlazingPassingSun', threshold=5)
|
||
min_rating, max_rating = _bounds(list_set_kit, 'fSetRating', threshold=0.05)
|
||
|
||
list_of_merchant_name = list({i.sMerchantName for i in list_set_kit})
|
||
list_of_profile_name = list({i.sProfileName for i in list_set_kit})
|
||
list_of_glazing_brief = list({i.sGlazingMark for i in list_set_kit})
|
||
|
||
# ОКОНЧАТЕЛЬНЫЙ ПРОГОН
|
||
# Формируем список словарей для шаблона; цвета вычисляются через хелперы _color_hi / _color_lo.
|
||
dim = []
|
||
for i in list_set_kit:
|
||
profile_num_cameras = sum_through(i.iProfileCameras)
|
||
|
||
# Рейтинг НАБОРА — особая логика со "звёздочками"
|
||
if i.fSetRating > RARING_SET_MAX:
|
||
rating_set_n = RARING_SET_MAX
|
||
rating_set_color = "#80ff80"
|
||
elif i.fSetRating < RARING_SET_MIN + 0.05 or max_rating - min_rating < 0.001:
|
||
rating_set_n = RARING_SET_MIN
|
||
rating_set_color = ""
|
||
else:
|
||
try:
|
||
rating_set_n = i.fSetRating * (RARING_SET_MAX - RARING_SET_MIN) / RARING_STAR
|
||
rating_set_color = _color_hi(i.fSetRating, min_rating, max_rating)
|
||
except (ZeroDivisionError, TypeError):
|
||
rating_set_color = None
|
||
rating_set_n = RARING_SET_MIN
|
||
|
||
list2_del = f",{to_compare},"
|
||
dim.append({
|
||
"MERCHANT": i.sMerchantName,
|
||
"MERCHANT_ID": i.MERCHANT_ID,
|
||
"IS_COMMERCIAL": i.bCommercial,
|
||
"MERCHANT_T": pytils.translit.slugify(i.sMerchantName),
|
||
"MERCHANT_URL": i.sMerchantMainURL,
|
||
"MERCHANT_URL_SHOT": re.sub(r"(?:^https?://|/$|www\.)", "", i.sMerchantMainURL),
|
||
"SET_NAME": i.sSetName,
|
||
"MERCHANT_LOGO": i.pMerchantLogo,
|
||
"RATING_SET": get_rating_set_for_stars(i.fSetRating),
|
||
"RATING_SET_N": rating_set_n,
|
||
"RATING_SET_COLOR": rating_set_color,
|
||
"PROFILE_ID": i.PROFILE_ID,
|
||
"PROFILE_NAME": i.sProfileName,
|
||
"PROFILE_NAME_T": pytils.translit.slugify(i.sProfileName),
|
||
"PROFILE_MANUFACTURER": i.sProfileManufacturer,
|
||
"PROFILE_MANUFACTURER_T": pytils.translit.slugify(i.sProfileManufacturer),
|
||
"PROFILE_NUM_COLOR": i.sProfileColor,
|
||
"PROFILE_NUM_CAMERAS": i.iProfileCameras, # Число камер рамы/створки
|
||
"PROFILE_NUM_CAMERAS_COLOR": _color_hi(profile_num_cameras, min_cameras, max_cameras, threshold=1),
|
||
"PROFILE_THICKNESS": i.iProfileThickness, # Монтажная ширина профиля
|
||
"PROFILE_THICKNESS_COLOR": _color_hi(i.iProfileThickness, min_thick, max_thick, threshold=10),
|
||
"PROFILE_GLAZING_THICKNESS": i.iProfileGlazingThickness, # Макс. толщина стеклопакета
|
||
"PROFILE_GLAZING_THICKNESS_COLOR": _color_hi(i.iProfileGlazingThickness, min_glaz_d, max_glaz_d, threshold=4),
|
||
"PROFILE_HEAT_TRANSFER": i.fProfileHeatTransf, # Сопротивление теплопередаче
|
||
"PROFILE_HEAT_TRANSFER_COLOR": _color_hi(i.fProfileHeatTransf, min_heat_p, max_heat_p),
|
||
"PROFILE_NUM_SEALS": i.fProfileSeals, # Контуров уплотнения
|
||
"PROFILE_NUM_SEALS_COLOR": _color_hi(i.fProfileSeals, min_seals, max_seals, threshold=0),
|
||
"PROFILE_SEAL_DESCRIPTION": i.sProfileSealDescription,
|
||
"PROFILE_SOUND_PROOFING": i.fProfileSoundproofing, # Коэффициент звукоизоляции профиля
|
||
"PROFILE_SOUND_PROOFING_COLOR": _color_hi(i.fProfileSoundproofing, min_sound_p, max_sound_p),
|
||
"PROFILE_HEIGHT": i.iProfileHeight, # Высота в световом проеме (меньше = лучше)
|
||
"PROFILE_HEIGHT_COLOR": _color_lo(i.iProfileHeight, min_height, max_height, threshold=12),
|
||
"PROFILE_RABBET": i.iProfileRabbet, # Фальц
|
||
"PROFILE_RABBET_COLOR": _color_hi(i.iProfileRabbet, min_rabbet, max_rabbet, threshold=1),
|
||
"PROFILE_FILLET": i.sProfileFillet, # Штапик
|
||
"PROFILE_REINFORCEMENT": i.sProfileReinforcement, # Армирование профиля
|
||
"PROFILE_OTHER": i.sProfileOther,
|
||
"SET_ID": i.id,
|
||
"SET_CLIMATE_CONTROL": i.sSetClimateControl,
|
||
"SET_STILL": i.sSetSill,
|
||
"SET_IMPLEMENTS_ALL": i.sSetImplementAll,
|
||
"SET_IMPLEMENTS_HANDLES": i.sSetImplementHandles,
|
||
"SET_IMPLEMENTS_HINGES": i.sSetImplementHinges,
|
||
"SET_IMPLEMENTS_LATCH": i.sSetImplementLatch,
|
||
"SET_IMPLEMENTS_LIMITER": i.sSetImplementLimiter,
|
||
"SET_IMPLEMENTS_CATCH": i.sSetImplementCatch,
|
||
"SET_PANES": i.sSetPanes,
|
||
"SET_SLOPE": i.sSetSlope,
|
||
"SET_DELIVERY": i.sSetDelivery,
|
||
"SET_DELIVERY_B": i.bSetDelivery,
|
||
"SET_UNINSTALL_INSTALL": i.sSetUninstallInstall,
|
||
"SET_UNINSTALL_INSTALL_B": i.bSetUninstallInstall,
|
||
"SET_OTHER_CONDITIONS": i.sSetOtherConditions,
|
||
"GLAZING_CAMERAS_NUM": i.iGlazingCamerasN, # Камер стеклопакета
|
||
"GLAZING_CAMERAS_COLOR": _color_hi(i.iGlazingCamerasN, min_gl_cam, max_gl_cam),
|
||
"GLAZING_THICKNESS": i.iGlazingThickness, # Толщина стеклопакета
|
||
"GLAZING_THICKNESS_COLOR": _color_hi(i.iGlazingThickness, min_gl_thick, max_gl_thick, threshold=3),
|
||
"GLAZING_BRIEF_DESCRIPTION": re.sub(r",[\s\d]+мм", "", i.sGlazingBriefDescription),
|
||
"GLAZING_MARK": i.sGlazingMark,
|
||
"GLAZING_MANUFACTURER": i.sGlazingManufacturer,
|
||
"GLAZING_HEAT_TRANSFER": i.fGlazingHeatTransfer, # Ro стеклопакета (м²×°C/Вт)
|
||
"GLAZING_HEAT_TRANSFER_COLOR": _color_hi(i.fGlazingHeatTransfer, min_heat_g, max_heat_g, threshold=0.05),
|
||
"GLAZING_SOUNDPROOFING": i.fGlazingSoundproofing, # Звукоизоляция стеклопакета
|
||
"GLAZING_SOUNDPROOFING_COLOR": _color_hi(i.fGlazingSoundproofing, min_sound_g, max_sound_g, threshold=5),
|
||
"GLAZING_LIGHT_TRANSMISSION": i.fGlazingLightTransmission,
|
||
"GLAZING_LIGHT_TRANSMISSION_COLOR": _color_hi(i.fGlazingLightTransmission, min_light, max_light, threshold=5, epsilon=0.002),
|
||
"GLAZING_LIGHT_REFLECTION": i.sGlazingLightReflectance,
|
||
"GLAZING_PASSING_SUN": i.fGlazingPassingSun, # Солнцепропускание (меньше = лучше)
|
||
"GLAZING_PASSING_SUN_COLOR": _color_lo(i.fGlazingPassingSun, min_sun, max_sun, threshold=5, epsilon=0.0001),
|
||
"GLAZING_REFLECTION_AND_ABSORPTION": i.sGlazingReflectionAndAbsorptionOfHeat,
|
||
"GLAZING_TONING": i.sGlazingToning,
|
||
"URL_W_DEL": list2_del.replace(f",{i.id},", ",")[1:-1],
|
||
})
|
||
to_template.update({'SET_LIST': dim,
|
||
'LIST_MERCHANT': list_of_merchant_name,
|
||
'LIST_PROFILE': list_of_profile_name,
|
||
'LIST_GLAZING': list_of_glazing_brief})
|
||
# Предложения для добавления в сравнения:
|
||
if len(list_set_kit) < 7:
|
||
try:
|
||
q_set_kit = (
|
||
SetKit.objects
|
||
.exclude(id__in=list_fin) # исключаем уже сравниваемые наборы
|
||
.filter(priceoffer__isnull=False) # только наборы с ценовыми предложениями
|
||
.annotate(
|
||
dLastData=Max('priceoffer__dOfferModify'),
|
||
sMerchantName=F('kSet2User__kMerchantOffice__kMerchantName__sMerchantName'),
|
||
)
|
||
.order_by('-dLastData')[:25]
|
||
)
|
||
dim = []
|
||
for i in q_set_kit:
|
||
# Вычисляем deltaData в Python (аналог TO_DAYS(NOW()) - TO_DAYS(MAX(dOfferModify)))
|
||
i.deltaData = (
|
||
(timezone.now().date() - i.dLastData.date()).days
|
||
if i.dLastData else 999
|
||
)
|
||
if i.deltaData < 100:
|
||
early_data = pytils.dt.distance_of_time_in_words(
|
||
int(django.utils.dateformat.format(i.dLastData, 'U')), accuracy=2
|
||
)
|
||
else:
|
||
early_data = ""
|
||
dim.append({
|
||
"ID_LIST": "%s,%d" % (to_compare, i.id),
|
||
"SET_NAME": i.sSetName,
|
||
"MERCHANT": i.sMerchantName,
|
||
"DATA_MODIFY": early_data,
|
||
"R": i.fSetRating,
|
||
"R_STAR": get_rating_set_for_stars(i.fSetRating),
|
||
})
|
||
to_template.update({'LIST_TO_ADD': dim})
|
||
except SetKit.DoesNotExist:
|
||
pass
|
||
to_template.update({
|
||
# получаем последние визиты клиента через куки
|
||
'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(),
|
||
'ticks': float(time.perf_counter() - time_start)
|
||
})
|
||
return render(request, "report/report_compare_set.html", to_template)
|
||
|
||
|
||
def show_rating_components(request: HttpRequest, win_set: str = "1") -> HttpResponse:
|
||
""" Показать состав рейтинга оконного предложения (компоненты рейтинга)
|
||
|
||
:param request: HttpRequest -- входящий http-запрос
|
||
:param win_set: str -- id оконного набора, для которого показать состав рейтинга
|
||
:return: HttpResponse --
|
||
"""
|
||
time_start = time.perf_counter()
|
||
to_template: dict[str, object] = {}
|
||
try:
|
||
win_set = int(win_set)
|
||
except ValueError:
|
||
win_set = 1
|
||
q = (
|
||
SetKit.objects
|
||
.filter(id=win_set)
|
||
.annotate(
|
||
dPriceModify=Max('priceoffer__dOfferModify'),
|
||
NumOffer=Count('priceoffer__id'),
|
||
fOfferRatingAvg=Avg('priceoffer__fOfferRating'),
|
||
fProfileRating=F('kSet2PVCprofiles__fProfileRating'),
|
||
fGlazingRating=F('kSet2Glazing__fGlazingRating'),
|
||
)
|
||
)
|
||
raring_list = list(q)
|
||
f_rating_service = raring_list[0].fSetRating - RARING_WEIGHT_PVC_PROFILE_IN_SET * normalize(
|
||
raring_list[0].fProfileRating, val_max=RARING_PVC_PROFILE_MAX
|
||
)
|
||
f_rating_service -= RARING_WEIGHT_GLAZING_IN_SET * normalize(raring_list[0].fGlazingRating,
|
||
val_max=RARING_GLAZING_MAX)
|
||
f_rating_service = normalize(f_rating_service,
|
||
val_max=RARING_SET_MAX-RARING_WEIGHT_PVC_PROFILE_IN_SET-RARING_WEIGHT_GLAZING_IN_SET)
|
||
f_rating_service *= RARING_SET_MAX
|
||
to_template.update({'RATING_SERVIZ': f_rating_service,
|
||
'RATING_SERVIZ_STARS': get_rating_set_for_stars(f_rating_service),
|
||
'RATING_GLAZ': raring_list[0].fGlazingRating,
|
||
'RATING_GLAZ_STARS': get_rating_set_for_stars(raring_list[0].fGlazingRating),
|
||
'RATING_PVC': raring_list[0].fProfileRating,
|
||
'RATING_PVC_STARS': get_rating_set_for_stars(raring_list[0].fProfileRating),
|
||
'RATING_OFFER': raring_list[0].fOfferRatingAvg,
|
||
'RATING_OFFER_STARS': get_rating_set_for_stars(raring_list[0].fOfferRatingAvg),
|
||
'DATA_OFFER_UPDATE': pytils.dt.distance_of_time_in_words(
|
||
int(django.utils.dateformat.format(raring_list[0].dPriceModify, 'U')), accuracy=2),
|
||
'NUM_OFFERS': pytils.numeral.get_plural(raring_list[0].NumOffer,
|
||
"коммерческое предложение, коммерческих предложения,"
|
||
" коммерческих предложений"),
|
||
'TEST': win_set,
|
||
'ticks': float(time.perf_counter() - time_start)})
|
||
return render(request, "report/show_rating_components.html", to_template)
|