mod: Рефакторинг страницы цен одного окна (вьюшки, шаблоны, тесты, новый canonical-роутинг)

This commit is contained in:
2026-04-26 14:53:49 +03:00
parent 21501799ca
commit 3479b31f0e
10 changed files with 777 additions and 164 deletions

View File

@@ -72,7 +72,7 @@ def _as_sitemap_date(value: date | datetime | None) -> str:
class SingleWindowSitemap(Sitemap):
"""Источник URL для страниц цен одного проёма (/tsena-odnogo-okna/...)."""
"""Источник URL для страниц цен одного проёма (/catalog/standard_opening/price-...)."""
changefreq = "weekly"
priority = 0.5
@@ -98,7 +98,7 @@ class SingleWindowSitemap(Sitemap):
# поэтому умножаем на 10 и приводим к int.
width_mm = int(float(item.iWinWidth) * 10)
height_mm = int(float(item.iWinHight) * 10)
return f"/tsena-odnogo-okna/{width_mm}x{height_mm}mm/tip{item.id}"
return f"/catalog/standard_opening/price-{width_mm}x{height_mm}mm-tip{item.id}"
def lastmod(self, item: Win_MountDim) -> datetime:
return self.lastmod_value

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.shortcuts import render, redirect
from django.http import HttpRequest, HttpResponse
from oknardia.models import Win_MountDim, PriceOffer, Apartment_Type, Seria_Info, LogVisitPriceReport
from django.utils import timezone
from oknardia.models import Win_MountDim, PriceOffer, Apartment_Type, Seria_Info, LogVisitPriceReport, MountDim2Apartment
from oknardia.settings import *
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 normalize, get_rating_set_for_stars, get_flaps_for_big_pictures, get_flaps_for_mini_pictures, \
@@ -12,9 +14,26 @@ import time
import os
import re
import json
from types import SimpleNamespace
import pytils
def _one_win_price_canonical_path(win_width_mm: int | str, win_height_mm: int | str, win_id: int | str) -> str:
"""Возвращает канонический путь страницы цен для одного типового окна."""
return f"/catalog/standard_opening/price-{int(win_width_mm)}x{int(win_height_mm)}mm-tip{int(win_id)}/"
def redirect_one_win_price_legacy(request: HttpRequest,
win_width_mm: str | int = DEFAULT_WIN_WIDTH_MM,
win_height_mm: str | int = DEFAULT_WIN_HEIGHT_MM,
win_id: str | int = DEFAULT_WIN_ID) -> HttpResponse:
"""301-редирект со старого URL /tsena-odnogo-okna/... на канонический URL."""
return redirect(
_one_win_price_canonical_path(win_width_mm=win_width_mm, win_height_mm=win_height_mm, win_id=win_id),
permanent=True,
)
def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_longitude: float, address_latitude: float,
frame_begin_n: int = 0, brand_id: int = 0, win_id: int = 0) -> dict:
""" Формируем выдачу цен для фрейма
@@ -49,65 +68,219 @@ def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_long
add_to_sql_for_widget = f" AND oknardia_merchantbrand.id = {brand_id} "
offer_per_frame = 1000 # Фреймовый вывод не нужен... фигачим сразу целую 1000 предложений.
if int(apartment_id) == 0 and int(win_id) != 0:
# если выводим цены только для одного проема
# ORM-ветка для одиночного типового окна.
# Здесь можно полностью уйти от raw SQL, потому что все связи линейные и хорошо покрываются select_related.
# Контракт ответа сохраняем прежним: META_DATA_PUBLISH, PRICE_FRAME, N и все вложенные ключи оферов.
offer_per_frame = OFFER_PER_FRAME_FOR_ONE_FLAP
q_price_offer = PriceOffer.objects.raw(
f"SELECT"
f" oknardia_priceoffer.id, oknardia_priceoffer.iOfferImpressions,"
f" oknardia_priceoffer.fOfferPrice, oknardia_priceoffer.dOfferModify,"
f" oknardia_priceoffer.fOfferRating, oknardia_priceoffer.sOfferFlapConfig,"
f" oknardia_priceoffer.iOfferViews, oknardia_priceoffer.sOfferActive,"
f" oknardia_win_mountdim.sDescripion, oknardia_win_mountdim.id AS mID, "
f" oknardia_win_mountdim.bIsNearDoor, oknardia_win_mountdim.bIsDoor,"
f" oknardia_win_mountdim.iWinWidth, oknardia_win_mountdim.iWinHight,"
f" oknardia_setkit.id AS setID,"
f" oknardia_setkit.sSetName, oknardia_setkit.dSetModify,"
f" oknardia_setkit.sSetClimateControl, oknardia_setkit.sSetSill,"
f" oknardia_setkit.sSetImplementAll, oknardia_setkit.sSetImplementHandles,"
f" oknardia_setkit.sSetImplementHinges, oknardia_setkit.sSetImplementLatch,"
f" oknardia_setkit.sSetImplementLimiter, oknardia_setkit.sSetImplementCatch,"
f" oknardia_setkit.sSetPanes, oknardia_setkit.sSetSlope,"
f" oknardia_setkit.sSetOtherConditions, oknardia_setkit.sSetActive,"
f" oknardia_setkit.bSetDelivery, oknardia_setkit.sSetDelivery,"
f" oknardia_setkit.sSetUninstallInstall, oknardia_setkit.bSetUninstallInstall,"
f" oknardia_setkit.fSetRating, oknardia_setkit.iSetNumEval,"
f" oknardia_setkit.iSetImpressions, oknardia_setkit.iSetViews,"
f" (oknardia_setkit.dSetCommercialUntil > NOW()) AS bCommercial,"
f" oknardia_merchantoffice.sOfficePhones, "
f" oknardia_merchantoffice.sOfficeDiscountMetaFormula,"
f" oknardia_merchantoffice.sOfficeName, oknardia_merchantoffice.sOfficeAddress,"
f" oknardia_glazing.fGlazingRating,"
f" oknardia_glazing.sGlazingName, oknardia_glazing.sGlazingBriefDescription,"
f" oknardia_glazing.sGlazingMark, oknardia_glazing.sGlazingToning,"
f" oknardia_pvcprofiles.sProfileBriefDescription, oknardia_pvcprofiles.id AS pwc_id,"
f" oknardia_pvcprofiles.sProfileReinforcement, oknardia_pvcprofiles.sProfileSealDescription,"
f" oknardia_pvcprofiles.sProfileName, oknardia_pvcprofiles.sProfileColor,"
f" oknardia_pvcprofiles.fProfileRating, oknardia_pvcprofiles.sProfileManufacturer,"
f" oknardia_merchantbrand.sMerchantName, oknardia_merchantbrand.pMerchantLogo,"
f" oknardia_merchantbrand.sMerchantMainURL, oknardia_merchantbrand.id AS brand_id,"
f" 1 AS iQuantity, 0 AS fOfficeGeoCode_Longitude, 0 AS fOfficeGeoCode_Latitude "
f"FROM oknardia_priceoffer"
f" INNER JOIN oknardia_win_mountdim"
f" ON oknardia_priceoffer.kOffer2MountDim_id = oknardia_win_mountdim.id"
f" INNER JOIN oknardia_setkit"
f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id"
f" INNER JOIN oknardia_ouruser"
f" ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id"
f" INNER JOIN oknardia_merchantoffice"
f" ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id"
f" INNER JOIN oknardia_glazing"
f" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id"
f" INNER JOIN oknardia_pvcprofiles"
f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id"
f" INNER JOIN oknardia_merchantbrand"
f" ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id "
f"WHERE oknardia_priceoffer.sOfferActive IS TRUE"
f" AND oknardia_setkit.sSetActive IS TRUE "
f" AND oknardia_win_mountdim.id = {int(win_id)}"
f" {add_to_sql_for_widget} "
f"ORDER BY"
f" oknardia_priceoffer.dOfferModify DESC "
f"LIMIT {int(frame_begin_n)}, 10000;")
if brand_id != 0:
offer_per_frame = 1000
q_price_offer = (
PriceOffer.objects.filter(
sOfferActive=True,
kOffer2MountDim_id=win_id,
kOffer2SetKit__sSetActive=True,
kOffer2SetKit__kSet2User__kMerchantOffice__isnull=False,
kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__isnull=False,
kOffer2SetKit__kSet2Glazing__isnull=False,
kOffer2SetKit__kSet2PVCprofiles__isnull=False,
)
.values(
'id',
'fOfferPrice',
'dOfferModify',
'sOfferFlapConfig',
'kOffer2MountDim__sDescripion',
'kOffer2MountDim__iWinWidth',
'kOffer2MountDim__iWinHight',
'kOffer2SetKit__id',
'kOffer2SetKit__sSetName',
'kOffer2SetKit__dSetModify',
'kOffer2SetKit__dSetCommercialUntil',
'kOffer2SetKit__sSetClimateControl',
'kOffer2SetKit__sSetSill',
'kOffer2SetKit__sSetImplementAll',
'kOffer2SetKit__sSetImplementHandles',
'kOffer2SetKit__sSetImplementHinges',
'kOffer2SetKit__sSetImplementLatch',
'kOffer2SetKit__sSetImplementLimiter',
'kOffer2SetKit__sSetImplementCatch',
'kOffer2SetKit__sSetPanes',
'kOffer2SetKit__sSetSlope',
'kOffer2SetKit__sSetOtherConditions',
'kOffer2SetKit__sSetDelivery',
'kOffer2SetKit__bSetDelivery',
'kOffer2SetKit__sSetUninstallInstall',
'kOffer2SetKit__bSetUninstallInstall',
'kOffer2SetKit__fSetRating',
'kOffer2SetKit__kSet2User__kMerchantOffice__sOfficePhones',
'kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeDiscountMetaFormula',
'kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeName',
'kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeAddress',
'kOffer2SetKit__kSet2Glazing__sGlazingBriefDescription',
'kOffer2SetKit__kSet2Glazing__sGlazingMark',
'kOffer2SetKit__kSet2Glazing__sGlazingToning',
'kOffer2SetKit__kSet2PVCprofiles__id',
'kOffer2SetKit__kSet2PVCprofiles__sProfileName',
'kOffer2SetKit__kSet2PVCprofiles__sProfileManufacturer',
'kOffer2SetKit__kSet2PVCprofiles__sProfileSealDescription',
'kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName',
'kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo',
'kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantMainURL',
)
.order_by("-dOfferModify")
)
if brand_id != 0:
q_price_offer = q_price_offer.filter(
kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName_id=brand_id,
)
q_price_offer = [
SimpleNamespace(
id=offer['id'],
fOfferPrice=offer['fOfferPrice'],
dOfferModify=offer['dOfferModify'],
sOfferFlapConfig=offer['sOfferFlapConfig'],
sDescripion=offer['kOffer2MountDim__sDescripion'],
iWinWidth=offer['kOffer2MountDim__iWinWidth'],
iWinHight=offer['kOffer2MountDim__iWinHight'],
setID=offer['kOffer2SetKit__id'],
sSetName=offer['kOffer2SetKit__sSetName'],
dSetModify=offer['kOffer2SetKit__dSetModify'],
dSetCommercialUntil=offer['kOffer2SetKit__dSetCommercialUntil'],
sSetClimateControl=offer['kOffer2SetKit__sSetClimateControl'],
sSetSill=offer['kOffer2SetKit__sSetSill'],
sSetImplementAll=offer['kOffer2SetKit__sSetImplementAll'],
sSetImplementHandles=offer['kOffer2SetKit__sSetImplementHandles'],
sSetImplementHinges=offer['kOffer2SetKit__sSetImplementHinges'],
sSetImplementLatch=offer['kOffer2SetKit__sSetImplementLatch'],
sSetImplementLimiter=offer['kOffer2SetKit__sSetImplementLimiter'],
sSetImplementCatch=offer['kOffer2SetKit__sSetImplementCatch'],
sSetPanes=offer['kOffer2SetKit__sSetPanes'],
sSetSlope=offer['kOffer2SetKit__sSetSlope'],
sSetOtherConditions=offer['kOffer2SetKit__sSetOtherConditions'],
sSetDelivery=offer['kOffer2SetKit__sSetDelivery'],
bSetDelivery=offer['kOffer2SetKit__bSetDelivery'],
sSetUninstallInstall=offer['kOffer2SetKit__sSetUninstallInstall'],
bSetUninstallInstall=offer['kOffer2SetKit__bSetUninstallInstall'],
fSetRating=offer['kOffer2SetKit__fSetRating'],
sOfficePhones=offer['kOffer2SetKit__kSet2User__kMerchantOffice__sOfficePhones'],
sOfficeDiscountMetaFormula=offer['kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeDiscountMetaFormula'],
sOfficeName=offer['kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeName'],
sOfficeAddress=offer['kOffer2SetKit__kSet2User__kMerchantOffice__sOfficeAddress'],
sGlazingBriefDescription=offer['kOffer2SetKit__kSet2Glazing__sGlazingBriefDescription'],
sGlazingMark=offer['kOffer2SetKit__kSet2Glazing__sGlazingMark'],
sGlazingToning=offer['kOffer2SetKit__kSet2Glazing__sGlazingToning'],
pwc_id=offer['kOffer2SetKit__kSet2PVCprofiles__id'],
sProfileName=offer['kOffer2SetKit__kSet2PVCprofiles__sProfileName'],
sProfileManufacturer=offer['kOffer2SetKit__kSet2PVCprofiles__sProfileManufacturer'],
sProfileSealDescription=offer['kOffer2SetKit__kSet2PVCprofiles__sProfileSealDescription'],
sMerchantName=offer['kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName'],
pMerchantLogo=offer['kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo'],
sMerchantMainURL=offer['kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantMainURL'],
iQuantity=1,
)
for offer in q_price_offer[frame_begin_n:frame_begin_n + 10000]
]
price_frame = []
n_begin = int(frame_begin_n)
for offer in q_price_offer:
n_begin += 1
total = offer.fOfferPrice
image_file = get_flaps_for_mini_pictures(offer.sOfferFlapConfig)
dim_in_offer = [{
'PRICE': offer.fOfferPrice,
'FLAP': offer.sOfferFlapConfig,
'DESCRIPTION': offer.sDescripion,
'WIDTH': offer.iWinWidth,
'HIGHT': offer.iWinHight,
'ID': offer.id,
'IMG_MINI': image_file,
'QUANTITY': 1,
'BULLET': ['A'],
'SUBTOTAL': offer.fOfferPrice,
}]
discount = 0
try:
meta_keys = eval(offer.sOfficeDiscountMetaFormula)
if KEY_DICSOUNT in meta_keys:
for CountVal in sorted(meta_keys[KEY_DICSOUNT]):
if float(total) > float(CountVal):
discount = meta_keys[KEY_DICSOUNT][CountVal]
except (ValueError, TypeError):
pass
fin_price = total * (100 - discount) / 100
if discount > 99 or discount < 0.1:
discount_color1 = ""
discount_color2 = ""
else:
color_ratio = (discount + 0.) / 100
discount_color1 = f"#{255 - int(color_ratio * 128):02x}ff{255 - int(color_ratio * 128):02x}"
discount_color2 = f"#{255 - int(color_ratio * 255):02x}ff{255 - int(color_ratio * 255):02x}"
price_frame.append({
'DISTANCE': -1,
'DIM': dim_in_offer,
'TOTAL': total,
'DISCOUNT': discount,
'DISCOUNT_COLOR1': discount_color1,
'DISCOUNT_COLOR2': discount_color2,
'FIN_PRICE': fin_price,
'OFFICE_NAME': offer.sOfficeName,
'OFFICE_ADDRESS': offer.sOfficeAddress,
'OFFICE_PHONES': offer.sOfficePhones,
'MERCHANT': offer.sMerchantName,
'MERCHANT_LOGO': offer.pMerchantLogo,
'MERCHANT_URL': offer.sMerchantMainURL,
'MERCHANT_URL_SHOT': re.sub(r"(^http://|^https://|/$|www\.)", "", offer.sMerchantMainURL),
'SETS_NAME': offer.sSetName,
'GLAZING_NAME_B': offer.sGlazingBriefDescription,
'GLAZING_MARK': offer.sGlazingMark,
'GLAZING_TONING': offer.sGlazingToning,
'PVC_ID': offer.pwc_id,
'PVC_NAME': offer.sProfileName,
'PVC_NAME_T': pytils.translit.slugify(offer.sProfileName).lower(),
'PVC_MANUFACTURER': offer.sProfileManufacturer,
'PVC_MANUFACTURER_T': pytils.translit.slugify(offer.sProfileManufacturer).lower(),
'PVC_SEAL': offer.sProfileSealDescription,
'SETS_CLIMATE_CONTROL': offer.sSetClimateControl,
'SETS_SILL': offer.sSetSill,
'SETS_IMPLEMENT': offer.sSetImplementAll,
'SETS_IMPLEMENT_R': offer.sSetImplementHandles,
'SETS_IMPLEMENT_P': offer.sSetImplementHinges,
'SETS_IMPLEMENT_Z': offer.sSetImplementLatch,
'SETS_IMPLEMENT_O': offer.sSetImplementLimiter,
'SETS_IMPLEMENT_F': offer.sSetImplementCatch,
'SETS_PANES': offer.sSetPanes,
'SETS_SLOPE': offer.sSetSlope,
'SETS_DELIVERY': offer.sSetDelivery,
'SETS_DELIVERY_B': offer.bSetDelivery,
'SETS_OTHER': offer.sSetOtherConditions,
'SETS_ID': offer.setID,
'SETS_UNINSTALL_INSTALL': offer.sSetUninstallInstall,
'SETS_UNINSTALL_INSTALL_B': offer.bSetUninstallInstall,
'SETS_RATING': offer.fSetRating,
'SETS_RATING_STARTS': get_rating_set_for_stars(offer.fSetRating),
'SETS_DATA_MODIFY': offer.dOfferModify,
'IS_COMMERCIAL': offer.dSetCommercialUntil > timezone.now(),
})
if time_for_meta == 0 or django.utils.dateformat.format(time_for_meta, 'U') < \
django.utils.dateformat.format(offer.dOfferModify, 'U'):
time_for_meta = offer.dOfferModify
if time_for_meta == 0 or django.utils.dateformat.format(time_for_meta, 'U') < \
django.utils.dateformat.format(offer.dSetModify, 'U'):
time_for_meta = offer.dSetModify
if len(price_frame) == offer_per_frame:
break
price_frame = sorted(price_frame, key=lambda item: item['DISTANCE'])
if len(price_frame) < offer_per_frame:
n_begin = '-1'
return {'META_DATA_PUBLISH': time_for_meta, 'PRICE_FRAME': price_frame, 'N': n_begin}
else:
# если выводим цены для типовой квартиры
# print("Нужно несколько окон для квартиры")
@@ -123,7 +296,7 @@ def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_long
f" oknardia_mountdim2apartment.iQuantity,"
f" oknardia_win_mountdim.id AS mID, "
f" oknardia_setkit.id AS setID,"
f" (oknardia_setkit.dSetCommercialUntil > NOW()) AS bCommercial,"
f" (oknardia_setkit.dSetCommercialUntil > CURRENT_TIMESTAMP) AS bCommercial,"
f" oknardia_pvcprofiles.id AS pwc_id,"
f" oknardia_merchantbrand.id AS brand_id "
f"FROM oknardia_priceoffer"
@@ -314,8 +487,10 @@ def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_long
return {'META_DATA_PUBLISH': time_for_meta, 'PRICE_FRAME': price_frame, 'N': n_begin}
def report_one_win_price(request: HttpRequest, win_width_mm: str = '670', win_height_mm: str = '2160',
win_id: str = '16') -> HttpResponse:
def report_one_win_price(request: HttpRequest,
win_width_mm: str | int = DEFAULT_WIN_WIDTH_MM,
win_height_mm: str | int = DEFAULT_WIN_HEIGHT_MM,
win_id: str | int = DEFAULT_WIN_ID) -> HttpResponse:
""" Формируем выдачу цен для единичного ТИПОВОГО окна (т.е. проема из серийного дома).
@@ -328,46 +503,78 @@ def report_one_win_price(request: HttpRequest, win_width_mm: str = '670', win_he
time_start = time.perf_counter()
to_template: dict[str, object] = {}
try:
# т.к. для вызова GetFlapDim4BigPictures нужно иметь внутри queryset поле iQuantity нельзя использовать
# простой запрос (см. следующую строку).
# qWinInfo = Win_MountDim.objects.filter(id=int(win_id))
# Придется сделать запрос немного сложнее:
q_win_info = Win_MountDim.objects.raw(
f'SELECT oknardia_win_mountdim.iWinWidth,'
f' oknardia_win_mountdim.iWinHight, oknardia_win_mountdim.iWinDepth,'
f' oknardia_win_mountdim.sFlapConfig, oknardia_win_mountdim.bIsNearDoor,'
f' oknardia_win_mountdim.bIsDoor, oknardia_win_mountdim.sDescripion,'
f' oknardia_win_mountdim.id, 0 as iQuantity '
f'FROM oknardia_win_mountdim '
f'WHERE oknardia_win_mountdim.id = {int(win_id)};'
win_info_rows = (
Win_MountDim.objects
.filter(id=int(win_id))
.values(
'id',
'iWinWidth',
'iWinHight',
'iWinDepth',
'sFlapConfig',
'bIsNearDoor',
'bIsDoor',
'sDescripion',
)
)
list_win_info = list(q_win_info)
list_win_info = [
SimpleNamespace(
id=item['id'],
iWinWidth=item['iWinWidth'],
iWinHight=item['iWinHight'],
iWinDepth=item['iWinDepth'],
sFlapConfig=item['sFlapConfig'],
bIsNearDoor=item['bIsNearDoor'],
bIsDoor=item['bIsDoor'],
sDescripion=item['sDescripion'],
iQuantity=0,
)
for item in win_info_rows
]
# Если размеры типового проема не совпадают с размерами из базы, то подменяем
# на правильные и перевызываем страницу
canonical_width_mm = int(list_win_info[0].iWinWidth * 10)
canonical_height_mm = int(list_win_info[0].iWinHight * 10)
if (list_win_info[0].iWinWidth * 10 != int(win_width_mm)) or \
(list_win_info[0].iWinHight * 10 != int(win_height_mm)):
return redirect(f"/tsena-odnogo-okna/{list_win_info[0].iWinWidth * 10}x{list_win_info[0].iWinHight * 10}"
f"mm/tip{win_id}")
return redirect(
_one_win_price_canonical_path(
win_width_mm=canonical_width_mm,
win_height_mm=canonical_height_mm,
win_id=win_id,
),
permanent=True,
)
except (ObjectDoesNotExist, ValueError, IndexError, TypeError):
return redirect("/tsena-odnogo-okna/670x2160mm/tip16")
return redirect(
_one_win_price_canonical_path(
win_width_mm=DEFAULT_WIN_WIDTH_MM,
win_height_mm=DEFAULT_WIN_HEIGHT_MM,
win_id=DEFAULT_WIN_ID,
),
permanent=True,
)
# все хорошо, засылаем картинку в шаблон
to_template.update(get_flaps_for_big_pictures(list_win_info))
# получаем варианты схемы открывания (для графиков)
q_offer_flap_variation = PriceOffer.objects.raw(
f'SELECT'
f' COUNT(oknardia_priceoffer.sOfferFlapConfig) AS id,'
f' "" AS IMG_MINI,'
f' "" AS STR_NUM,'
f' oknardia_priceoffer.sOfferFlapConfig '
f'FROM oknardia_priceoffer '
f'WHERE oknardia_priceoffer.sOfferActive <> 0'
f' AND oknardia_priceoffer.kOffer2MountDim_id = {int(win_id)} '
f'GROUP BY oknardia_priceoffer.sOfferFlapConfig,'
f' oknardia_priceoffer.sOfferActive,'
f' oknardia_priceoffer.kOffer2MountDim_id '
f'ORDER BY id DESC;'
flap_variations = (
PriceOffer.objects.filter(
sOfferActive=True,
kOffer2MountDim_id=int(win_id),
)
.values('sOfferFlapConfig')
.annotate(id=Count('sOfferFlapConfig'))
.order_by('-id')
)
list_offer_flap_variation = list(q_offer_flap_variation)
list_offer_flap_variation = [
SimpleNamespace(
id=item['id'],
sOfferFlapConfig=item['sOfferFlapConfig'],
IMG_MINI='',
STR_NUM='',
)
for item in flap_variations
]
for i in range(0, len(list_offer_flap_variation)):
if i < 3:
list_offer_flap_variation[i].STR_NUM = "вариант " + pytils.numeral.in_words(i + 1)
@@ -387,15 +594,13 @@ def report_one_win_price(request: HttpRequest, win_width_mm: str = '670', win_he
"варианта схем",
"вариантов схем"))})
#
q = PriceOffer.objects.raw(f'SELECT'
f' COUNT(oknardia_priceoffer.kOfferFromUser_id) AS id,'
f' oknardia_priceoffer.kOfferFromUser_id,'
f' oknardia_priceoffer.kOffer2MountDim_id '
f'FROM oknardia_priceoffer '
f'WHERE oknardia_priceoffer.kOffer2MountDim_id = {int(win_id)} '
f'GROUP BY oknardia_priceoffer.kOffer2MountDim_id,'
f' oknardia_priceoffer.kOfferFromUser_id;')
to_template.update({'NUM_TOTAL_FIRM_N_WORD': pytils.numeral.get_plural(len(list(q)),
firms_count = (
PriceOffer.objects.filter(kOffer2MountDim_id=int(win_id))
.values('kOfferFromUser_id')
.distinct()
.count()
)
to_template.update({'NUM_TOTAL_FIRM_N_WORD': pytils.numeral.get_plural(firms_count,
("компании", "компаний", "компаний"))})
q = PriceOffer.objects.filter(kOffer2MountDim_id=int(win_id))
to_template.update({'NUM_TOTAL_OFFER_N_WORD': pytils.numeral.get_plural(q.count(),
@@ -403,33 +608,36 @@ def report_one_win_price(request: HttpRequest, win_width_mm: str = '670', win_he
"готовых расчётов"))})
to_template.update({'NUM_ARCHIVE_OFFER': q.filter(sOfferActive=0).count()})
#
q_seria_for_win = PriceOffer.objects.raw(
f'SELECT'
f' oknardia_seria_info.sName, oknardia_seria_info.id AS id,'
f' "" AS sNameLat,'
f' COUNT(oknardia_mountdim2apartment.id) AS num_variation_of_apartment '
f'FROM oknardia_apartment_type'
f' INNER JOIN oknardia_mountdim2apartment'
f' ON oknardia_mountdim2apartment.kApartment_id = oknardia_apartment_type.id'
f' INNER JOIN oknardia_win_mountdim'
f' ON oknardia_mountdim2apartment.kMountDim_id = oknardia_win_mountdim.id'
f' INNER JOIN oknardia_seria_info'
f' ON oknardia_apartment_type.kSeria_id = oknardia_seria_info.id '
f'WHERE oknardia_win_mountdim.id = {int(win_id)} '
f'GROUP BY oknardia_win_mountdim.id,'
f' oknardia_seria_info.sName,'
f' oknardia_seria_info.id '
f'ORDER BY oknardia_seria_info.sName;'
seria_for_win = (
MountDim2Apartment.objects.filter(kMountDim_id=int(win_id), kApartment__kSeria__isnull=False)
.values('kApartment__kSeria__id', 'kApartment__kSeria__sName')
.annotate(num_variation_of_apartment=Count('id'))
.order_by('kApartment__kSeria__sName')
)
list_seria_for_win = list(q_seria_for_win)
for i in list_seria_for_win:
i.sNameLat = pytils.translit.slugify(i.sName)
i.num_variation_of_apartment = pytils.numeral.sum_string(i.num_variation_of_apartment,
pytils.numeral.MALE,
("типовую планировку квартиры",
"типовые планировки квартир",
"типовых планировок квартир"))
to_template.update(report_price_frame(0, 1, 0, 0, 0, 0, int(win_id)))
list_seria_for_win = []
for seria_item in seria_for_win:
seria_name = seria_item['kApartment__kSeria__sName']
list_seria_for_win.append(SimpleNamespace(
id=seria_item['kApartment__kSeria__id'],
sName=seria_name,
sNameLat=pytils.translit.slugify(seria_name),
num_variation_of_apartment=pytils.numeral.sum_string(
seria_item['num_variation_of_apartment'],
pytils.numeral.MALE,
("типовую планировку квартиры",
"типовые планировки квартир",
"типовых планировок квартир"),
),
))
to_template.update(report_price_frame(apartment_id=0,
mount_dim_per_offer= 1,
address_longitude= 0,
address_latitude= 0,
frame_begin_n= 0,
brand_id= 0,
win_id=int(win_id)
)
)
to_template.update({
'SERIA_FOR_WIN': list_seria_for_win,
'WIN_ID': int(win_id),
@@ -444,16 +652,25 @@ def report_one_win_price(request: HttpRequest, win_width_mm: str = '670', win_he
return render(request, "price/price_offers_for_one_window.html", to_template)
def next_one_win_price(request: HttpRequest, win_id='16', frame_begin_n="0"):
def next_one_win_price(request: HttpRequest,
win_id: str | int = DEFAULT_WIN_ID,
frame_begin_n: str | int = 0):
""" Возвращает очередной фреймом ценовых предложений для выдачи с одиночным окном.
:param request: HttpRequest -- входящий http-запрос
:param win_id: str -- id типового окна
:param frame_begin_n: str -- Номер записи с которой начинается фрейм с ценами
:param frame_begin_n: str -- Номер записи, с которой начинается фрейм с ценами
:return: HttpResponse --
"""
time_start = time.perf_counter()
to_template: dict[str, object] = report_price_frame(0, 1, 0, 0, int(frame_begin_n), 0, int(win_id))
to_template: dict[str, object] = report_price_frame(apartment_id=0,
mount_dim_per_offer=1,
address_longitude=0,
address_latitude=0,
frame_begin_n=int(frame_begin_n),
brand_id=0,
win_id=int(win_id)
)
to_template.update({'MOUNT_DIM_PER_OFFER': 1,
'WIN_ID': int(win_id),
'ticks': float(time.perf_counter() - time_start)})
@@ -633,7 +850,11 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
return redirect("/")
# получаем данные для фрейма ценовых предложений
price_frame = report_price_frame(apart_id, mount_dim_per_offer, address_longitude, address_latitude)
price_frame = report_price_frame(apartment_id=apart_id,
mount_dim_per_offer=mount_dim_per_offer,
address_longitude=address_longitude,
address_latitude=address_latitude
)
to_template.update(price_frame)
# print u"строк в querySet:", CountMountDimInFramePage
# dimension_to_template.update({'DISCOUNT_TXT': DiscountTXT})
@@ -704,8 +925,12 @@ def next_price_frame(request: HttpRequest, apart_id: str = "1", mount_dim_per_o
"""
time_start = time.perf_counter()
# получаем данные для фрейма ценовых предложений
price_frame = report_price_frame(int(apart_id), int(mount_dim_per_offer), float(address_longitude),
float(address_latitude), int(frame_begin_n))
price_frame = report_price_frame(apartment_id=int(apart_id),
mount_dim_per_offer=int(mount_dim_per_offer),
address_longitude=float(address_longitude),
address_latitude=float(address_latitude),
frame_begin_n=int(frame_begin_n)
)
to_template: dict[str, object] = price_frame
to_template.update({'APPARTMENT_ID': apart_id,
'MOUNT_DIM_PER_OFFER': mount_dim_per_offer,

250
oknardia/web/test_prices.py Normal file
View File

@@ -0,0 +1,250 @@
from datetime import timedelta
from decimal import Decimal
from unittest.mock import patch
from django.contrib.auth.models import User
from django.db import connection
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.utils import timezone
from oknardia.models import (
Apartment_Type,
Glazing,
MerchantBrand,
MerchantOffice,
MountDim2Apartment,
OurUser,
PVCprofiles,
PriceOffer,
Seria_Info,
SetKit,
)
from web.prices import redirect_one_win_price_legacy, report_one_win_price
class ReportOneWinPriceTests(TestCase):
"""Регрессионные тесты для ORM-версии report_one_win_price."""
def setUp(self) -> None:
self.factory = RequestFactory()
django_user = User.objects.create_user(username="price-tester", password="secret")
self.our_user = OurUser.objects.create(kDjangoUser=django_user)
# Тестовая SQLite-схема в проекте может быть legacy-вариантом с flap_config вместо sFlapConfig.
# Для тестов report_one_win_price явно добавляем sFlapConfig, чтобы код проверялся в целевом режиме.
with connection.cursor() as cursor:
cursor.execute("PRAGMA table_info(oknardia_win_mountdim)")
existing_columns = {row[1] for row in cursor.fetchall()}
if "sFlapConfig" not in existing_columns:
cursor.execute("ALTER TABLE oknardia_win_mountdim ADD COLUMN sFlapConfig varchar(32)")
# if "flap_config" in existing_columns:
# cursor.execute(
# "UPDATE oknardia_win_mountdim SET sFlapConfig = flap_config "
# "WHERE sFlapConfig IS NULL"
# )
self.brand = MerchantBrand.objects.create(
sMerchantName="Оконный бренд",
sMerchantMainURL="https://example.com",
)
self.office = MerchantOffice.objects.create(
sOfficeName="Оконный бренд — офис",
kMerchantName=self.brand,
sOfficePhones="+7(495)123-45-67",
sOfficeAddress="Москва, Тестовая улица, 1",
sOfficeDiscountMetaFormula="{'discount': {'10000': 5}}",
)
self.our_user.kMerchantOffice = self.office
self.our_user.save(update_fields=["kMerchantOffice"])
with connection.cursor() as cursor:
insert_columns = [
"iWinWidth",
"iWinHight",
"iWinDepth",
"sFlapConfig",
"sDescripion",
"bIsDoor",
"bIsNearDoor",
"iWinLimit",
"dMountXYZDataCreate",
"dMountXYZModify",
]
insert_values = [
Decimal("67.0"),
Decimal("216.0"),
Decimal("15.0"),
"[>]",
"Тестовый проём",
0,
0,
Decimal("5.0"),
]
if "flap_config" in existing_columns:
insert_columns.insert(3, "flap_config")
insert_values.insert(3, "[>]")
columns_sql = ", ".join(insert_columns)
placeholders_sql = ", ".join(["?"] * len(insert_values)) + ", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP"
cursor.execute(
f"INSERT INTO oknardia_win_mountdim ({columns_sql}) VALUES ({placeholders_sql})",
insert_values,
)
self.window_id = cursor.lastrowid
self.seria = Seria_Info.objects.create(sName="П-44")
self.apartment = Apartment_Type.objects.create(
sNameApartment="1-комнатная",
kSeria=self.seria,
)
MountDim2Apartment.objects.create(
kApartment=self.apartment,
kMountDim_id=self.window_id,
iQuantity=1,
)
self.glazing = Glazing.objects.create(
sGlazingName="Тестовый стеклопакет",
sGlazingBriefDescription="Двухкамерный стеклопакет",
sGlazingMark="4-10-4-10-4",
sGlazingToning="нет",
kGlazing2User=self.our_user,
)
self.profile = PVCprofiles.objects.create(
sProfileName="Profile Test",
sProfileBriefDescription="Профиль для теста",
sProfileManufacturer="Test Manufacturer",
sProfileSealDescription="чёрный",
sProfileReinforcement="сталь",
kProfile2User=self.our_user,
fProfileRating=4.2,
)
self.set_kit = SetKit.objects.create(
sSetName="Тестовый набор",
kSet2User=self.our_user,
kSet2PVCprofiles=self.profile,
kSet2Glazing=self.glazing,
sSetImplementAll="Фурнитура",
sSetImplementHandles="Ручки",
sSetImplementHinges="Петли",
sSetImplementLatch="Запоры",
sSetImplementLimiter="Ограничитель",
sSetImplementCatch="Фиксатор",
sSetSill="Подоконник",
sSetSlope="Откос",
sSetPanes="Отлив",
sSetDelivery="Доставка",
bSetDelivery=True,
sSetUninstallInstall="Монтаж",
bSetUninstallInstall=True,
sSetOtherConditions="Прочие условия",
sSetClimateControl="Климат",
fSetRating=4.5,
dSetCommercialUntil=timezone.now() + timedelta(days=30),
)
self.active_offer = PriceOffer.objects.create(
kOffer2MountDim_id=self.window_id,
kOfferFromUser=self.our_user,
kOffer2SetKit=self.set_kit,
sOfferFlapConfig="[>]",
fOfferPrice=Decimal("12345.00"),
sOfferActive=True,
)
self.archived_offer = PriceOffer.objects.create(
kOffer2MountDim_id=self.window_id,
kOfferFromUser=self.our_user,
kOffer2SetKit=self.set_kit,
sOfferFlapConfig="[<]",
fOfferPrice=Decimal("11111.00"),
sOfferActive=False,
)
@patch("web.prices.get_last_all_user_visit_list", return_value=[])
@patch("web.prices.get_last_user_visit_list", return_value=[])
@patch("web.prices.get_last_user_visit_cookies", return_value=[])
@patch("web.prices.get_flaps_for_mini_pictures", return_value="img/test-mini.png")
@patch(
"web.prices.get_flaps_for_big_pictures",
return_value={
"FLAP_DIM": [{
"iWinWidth": Decimal("67.0"),
"iWinHight": Decimal("216.0"),
"iWinWidth_mm": 670,
"iWinHight_mm": 2160,
}],
"WIN_DIM": [],
},
)
def test_report_one_win_price_renders_expected_context(
self,
mocked_big_pictures,
mocked_mini_pictures,
mocked_cookies,
mocked_last_visits,
mocked_all_visits,
):
"""Вьюха должна собирать тот же ключевой контекст, но уже без raw SQL."""
request = self.factory.get(
f"/catalog/standard_opening/price-670x2160mm-tip{self.window_id}",
)
captured = {}
def fake_render(_request, template_name, context):
captured["template_name"] = template_name
captured["context"] = context
return HttpResponse("ok")
with patch("web.prices.render", side_effect=fake_render):
response = report_one_win_price(request, "670", "2160", str(self.window_id))
context = captured["context"]
self.assertEqual(response.status_code, 200)
self.assertEqual(captured["template_name"], "price/price_offers_for_one_window.html")
self.assertEqual(context["WIN_ID"], self.window_id)
self.assertEqual(context["MOUNT_DIM_PER_OFFER"], 1)
self.assertEqual(context["NUM_ARCHIVE_OFFER"], 1)
self.assertIn("2", context["NUM_TOTAL_OFFER_N_WORD"])
self.assertEqual(len(context["LIST_FLAP_VARIATION"]), 1)
self.assertEqual(context["LIST_FLAP_VARIATION"][0].sOfferFlapConfig, "[>]")
self.assertTrue(context["LIST_FLAP_VARIATION"][0].STR_NUM.startswith("вариант"))
self.assertEqual(context["LIST_FLAP_VARIATION"][0].IMG_MINI, "img/test-mini.png")
self.assertEqual(len(context["SERIA_FOR_WIN"]), 1)
self.assertEqual(context["SERIA_FOR_WIN"][0].sName, self.seria.sName)
self.assertEqual(len(context["PRICE_FRAME"]), 1)
self.assertEqual(context["PRICE_FRAME"][0]["SETS_NAME"], self.set_kit.sSetName)
self.assertEqual(context["PRICE_FRAME"][0]["MERCHANT"], self.brand.sMerchantName)
self.assertEqual(context["PRICE_FRAME"][0]["DIM"][0]["IMG_MINI"], "img/test-mini.png")
self.assertIn("META_DATA_PUBLISH", context)
self.assertTrue(mocked_big_pictures.called)
self.assertTrue(mocked_mini_pictures.called)
self.assertTrue(mocked_cookies.called)
self.assertTrue(mocked_last_visits.called)
self.assertTrue(mocked_all_visits.called)
def test_report_one_win_price_redirects_to_canonical_dimensions(self):
"""Если SEO-размеры в URL неверные, вьюха должна редиректить на канонический URL."""
request = self.factory.get(
f"/catalog/standard_opening/price-999x999mm-tip{self.window_id}",
)
response = report_one_win_price(request, "999", "999", str(self.window_id))
self.assertEqual(response.status_code, 301)
self.assertEqual(
response["Location"],
f"/catalog/standard_opening/price-670x2160mm-tip{self.window_id}/",
)
def test_legacy_one_win_url_redirects_to_canonical_url(self):
"""Старый URL страницы одного окна должен отдавать 301 на новый канонический путь."""
request = self.factory.get(
f"/tsena-odnogo-okna/670x2160mm/tip{self.window_id}",
)
response = redirect_one_win_price_legacy(request, "670", "2160", str(self.window_id))
self.assertEqual(response.status_code, 301)
self.assertEqual(
response["Location"],
f"/catalog/standard_opening/price-670x2160mm-tip{self.window_id}/",
)