From 9e2708963833d560731977539ba84bfc43fd51cc Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 20 Apr 2026 00:54:11 +0300 Subject: [PATCH] =?UTF-8?q?=20minor:=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20catalog=5Fprofiles.py=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=81=D0=B5?= =?UTF-8?q?=20=D0=B2=D1=8C=D1=8E=D1=85=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oknardia/oknardia/urls.py | 14 +- oknardia/web/catalog.py | 376 ----------------------------- oknardia/web/catalog_profiles.py | 389 +++++++++++++++++++++++++++++++ 3 files changed, 396 insertions(+), 383 deletions(-) create mode 100644 oknardia/web/catalog_profiles.py diff --git a/oknardia/oknardia/urls.py b/oknardia/oknardia/urls.py index 9397347..cb058f5 100644 --- a/oknardia/oknardia/urls.py +++ b/oknardia/oknardia/urls.py @@ -18,9 +18,8 @@ from django.contrib import admin from django.urls import include, path, re_path from django.conf.urls.static import static from oknardia.settings import * -from web import views, autocomplete_addr, user_manager, blog, diagrams, report1, report2, catalog, prices, service - - +from web import views, autocomplete_addr, user_manager, blog, diagrams, report1, report2, catalog, prices, service, \ + catalog_profiles urlpatterns = [ path('admin/', admin.site.urls), @@ -57,13 +56,13 @@ urlpatterns = [ re_path(r'^stat/rating[/*]$', report2.ratings), re_path(r'^stat/rating/profiles_rank[/*]$', report2.profiles_rating), # --- Каталог - # --- --- Каталог профилей re_path(r'^catalog[/*]$', catalog.catalog_root), - re_path(r'^catalog/profile[/*]$', catalog.catalog_profile), + # --- --- Каталог профилей + re_path(r'^catalog/profile[/*]$', catalog_profiles.catalog_profile), re_path(r'^catalog/profile/(?P\d+)-(?P\S*)' - r'/(?P\d+)-(?P\S*)[/*]$', catalog.catalog_profile_model), + r'/(?P\d+)-(?P\S*)[/*]$', catalog_profiles.catalog_profile_model), re_path(r'^catalog/profile/(?P\d+)-(?P\S*)[/*]$', - catalog.catalog_profile_manufacture), + catalog_profiles.catalog_profile_manufacture), # --- --- Каталог серий типового строительства re_path(r'^catalog/seria[/*]$', catalog.catalog_seria), re_path(r'^catalog/seria/(?P[^/]*)/all(?P\d+)[/*]$', catalog.catalog_seria_info), @@ -100,6 +99,7 @@ urlpatterns = [ ] + if DEBUG: # ___ ____ _ _____ _ _ _____ _ # | | | | \ ___| |_ _ _ ___ |_ _|___ ___| | |_ ___ ___ | _ |___ ___ ___| | diff --git a/oknardia/web/catalog.py b/oknardia/web/catalog.py index 113fe0d..5408425 100644 --- a/oknardia/web/catalog.py +++ b/oknardia/web/catalog.py @@ -46,382 +46,6 @@ def catalog_root(request: HttpRequest) -> HttpResponse: response = render(request, "catalog/catalog_root.html", to_template) return response - -def catalog_profile(request: HttpRequest) -> HttpResponse: - """ - КАТАЛОГ ПРОФИЛЕЙ: страница со списком производителей и моделей (марками) профилей - - :param request: HttpRequest -- входящий http-запрос - :return response: HttpResponse -- исходящий http-ответ - """ - time_start = time.time() - # Берём только те поля, которые реально нужны для построения страницы каталога. - # Это позволяет не тащить лишние данные из БД и сразу работать с простыми словарями. - profile_rows = list( - PVCprofiles.objects.values( - "id", - "sProfileName", - "sProfileBriefDescription", - "sProfileManufacturer", - ).order_by("sProfileManufacturer", "sProfileBriefDescription") - ) - profile_count = len(profile_rows) - to_template = { - 'CATALOG_PROFILE_NUM': pytils.numeral.get_plural(profile_count, "профиль,профиля,профилей") - } - # Локальный помощник: slug нужен несколько раз, а повторять одну и ту же строку не хочется. - def make_slug(value: str) -> str: - return pytils.translit.slugify(value).lower() - - list_profile_manufactures = [] - tmp_profile_manufacture = "" - for profile in profile_rows: - if profile["sProfileManufacturer"] == "": - # Пустой производитель в каталоге только мешает: не создаём для него отдельную группу. - continue - - if tmp_profile_manufacture != profile["sProfileManufacturer"]: - # Новый производитель — открываем новую группу карточек. - tmp_profile_manufacture = profile["sProfileManufacturer"] - list_profile_manufactures.append({ - "PROF_MAN_ID": profile["id"], - "PROF_MAN": profile["sProfileManufacturer"], - "PROF_MAN_T": make_slug(profile["sProfileManufacturer"]), - "PROF_MAN_LIST": [{ - "PROF_NAME_ID": profile["id"], - "PROF_NAME": profile["sProfileBriefDescription"], - "PROF_NAME_T": make_slug(profile["sProfileName"]), - }] - }) - else: - # Если производитель уже встречался, просто дописываем новую модель в его список. - list_profile_manufactures[-1]["PROF_MAN_LIST"].append({ - "PROF_NAME_ID": profile["id"], - "PROF_NAME": profile["sProfileBriefDescription"], - "PROF_NAME_T": make_slug(profile["sProfileName"]), - }) - - to_template.update({ - 'CATALOG_PROFILE_MAN1_NAME2': list_profile_manufactures, - 'CATALOG_MANUFACT_NUM': len(list_profile_manufactures), - 'CATALOG_MANUFACT_NUM_W': - pytils.numeral.sum_string(len(list_profile_manufactures), pytils.numeral.MALE, ("производитель", - "производителя", - "производителей")), - 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]), - 'LOG_VISIT': get_last_all_user_visit_list(), - 'ticks': float(time.time() - time_start), - }) - return render(request, "catalog/catalog_of_profiles.html", to_template) - - -def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture_name: str, - model_id: int, model_name: str) -> HttpResponse: - """ - КАТАЛОГ ПРОФИЛЕЙ: страница с описанием марки профиля - - :param request: HttpRequest -- входящий http-запрос - :param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription - :param manufacture_name: название производителя (транслитерированное pytils.translit.slugify()) - :param model_id: id модели (марки) профиля - :param model_name: модель (марка) профиля (транслитерированное pytils.translit.slugify(sProfileName)) - :return response: HttpResponse -- исходящий http-ответ - """ - time_start = time.time() - manufacture_id = int(manufacture_id) - model_id = int(model_id) - q_pvc_by_id = PVCprofiles.objects.get(id=model_id) - manufacturer_slug = pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) - model_slug = pytils.translit.slugify(q_pvc_by_id.sProfileName) - if manufacturer_slug != manufacture_name \ - or model_slug != model_name \ - or manufacture_id != model_id: - return redirect(f"/catalog/profile/{model_id}-{manufacturer_slug}/" - f"{model_id}-{model_slug}") - - # Локальные помощники держат вьюху короче и не размазывают однотипную логику по коду. - def make_slug(value: str) -> str: - return pytils.translit.slugify(value).lower() - - def build_other_list(value: str) -> list[str]: - # Убираем пустые куски, чтобы не плодить «пустые» характеристики в шаблоне. - result = [] - for chunk in (part.strip() for part in value.split(";")): - if not chunk: - continue - if ":" in chunk: - head, tail = chunk.split(":", 1) - result.append(f"{head.strip()}:{tail.strip()}") - else: - result.append(f"{chunk}") - return result - - def update_pub_dat(current_pub_dat: datetime | None, candidate_pub_dat: datetime | None) -> datetime | None: - # На странице оставляем дату публикации/обновления только если она реально новее карточки профиля. - if candidate_pub_dat is None: - return current_pub_dat - if current_pub_dat is None or candidate_pub_dat.replace(tzinfo=None) > current_pub_dat.replace(tzinfo=None): - return candidate_pub_dat - return current_pub_dat - - def apply_rating_colors(rating: dict, rating_pairs: tuple[tuple[str, str], ...], multiplier: int, - gray: bool = False) -> None: - # Один маленький helper вместо россыпи почти одинаковых строк: меняется только множитель и формат RGB. - for rating_key, template_key in rating_pairs: - color = int(255 - rating[rating_key] * multiplier) - if gray: - to_template[template_key] = f"{color},{color},{color}" - else: - to_template[template_key] = f"{color},255,{color}" - - def merchant_row_to_dict(row: dict) -> dict: - # Один маппер для строки с партнёром: ключи шаблона остаются как были. - merchant_name = row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"] - return { - "MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"], - "MERCHANT_NAME": merchant_name, - "MERCHANT_NAME_T": make_slug(merchant_name), - "MERCHANT_LOGO_URL": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo"], - "MERCHANT_OFFERS": row["offers_by_merchant"], - } - - def profile_row_to_dict(profile: dict) -> dict: - # И то же самое для списка соседних профилей производителя. - return { - "PROFILE_NAME": profile["sProfileBriefDescription"], - "PROFILE_ID": profile["id"], - "PROFILE_URL": make_slug(profile["sProfileName"]), - "PROFILE_RATING": profile["fProfileRating"], - "PROFILE_RATING_STARS": get_rating_set_for_stars(profile["fProfileRating"]), - } - - to_template: dict[str, object] = {"CATALOG_MODEL": q_pvc_by_id, - "CATALOG_MAN2URL": manufacture_name, - "CATALOG_URL": f"{manufacture_id}-{manufacture_name}", - "CATALOG_URL2": f"{manufacture_id}-{manufacture_name}/{model_id}-{model_name}", - "PROFILE_RATING_STARS": get_rating_set_for_stars(q_pvc_by_id.fProfileRating)} - try: - got_json = json.loads(q_pvc_by_id.sProfileDescription) - # раскрашиваем кружочки рейтинга напротив характеристик профиля - rating_pairs = ( - (RANK_PVCP_CAMERAS_NUM_NAME, "RANK_PVCP_CAMERAS_COLOR"), - (RANK_PVCP_SEALS_NAME, "RANK_PVCP_SEALS_COLOR"), - (RANK_PVCP_THICKNESS_NAME, "RANK_PVCP_THICKNESS_COLOR"), - (RANK_PVCP_G_THICKNESS_NAME, "RANK_PVCP_G_THICKNESS_COLOR"), - (RANK_PVCP_RABBET_NAME, "RANK_PVCP_RABBET_COLOR"), - (RANK_PVCP_HEAT_TRANSFER_NAME, "RANK_PVCP_HEAT_TRANSFER_COLOR"), - (RANK_PVCP_SOUNDPROOFING_NAME, "RANK_PVCP_SOUNDPROOFING_COLOR"), - (RANK_PVCP_HEIGHT_NAME, "RANK_PVCP_HEIGHT_COLOR"), - ) - if KEY_RATING in got_json: - # кружочки зелёные - apply_rating_colors(got_json[KEY_RATING], rating_pairs, 255) - elif KEY_RATING_VIRTUAL in got_json: - # кружочки серые - apply_rating_colors(got_json[KEY_RATING_VIRTUAL], rating_pairs, 64, gray=True) - else: - pass - if KEY_HTML in got_json: - to_template.update({"EXTRA_INFO": got_json[KEY_HTML]}) - except (TypeError, ValueError, KeyError): - pass - to_template.update({"LIST_OTHER": build_other_list(q_pvc_by_id.sProfileOther)}) - # Партнёров считаем через ORM: так код проще читать и легче переносить между СУБД. - q_merchant = ( - PriceOffer.objects.filter( - kOffer2SetKit__kSet2PVCprofiles_id=model_id, - sOfferActive=True, - ) - .values( - "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id", - "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName", - "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo", - ) - .annotate(offers_by_merchant=Count("id")) - .order_by("-offers_by_merchant", "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName") - ) - to_template.update({'MERCHANTS': [merchant_row_to_dict(row) for row in q_merchant]}) - # Близкие профили этого же производителя нужны для быстрых переходов по карточкам. - q_profiles = ( - PVCprofiles.objects.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer) - .exclude(id=model_id) - .values("id", "fProfileRating", "sProfileBriefDescription", "sProfileName") - .order_by("fProfileRating") - ) - to_template.update({'PROFILES': [profile_row_to_dict(profile) for profile in q_profiles]}) - # Описание профиля берём через связку каталог -> блог: это один ORM-запрос вместо сырого SQL. - q_profiles_detail = ( - Catalog2Profile.objects.filter( - kProfile_id=model_id, - sCatalogCardType=CATALOG_RECORD_FOR_PROFILE_MODEL, - kBlogCatalog__isnull=False, - ) - .select_related("kBlogCatalog") - .order_by("kBlogCatalog__iCatalogSort") - ) - profile_blog_posts = [row.kBlogCatalog for row in q_profiles_detail if row.kBlogCatalog is not None] - to_template.update({'PROFILE_DETAIL': profile_blog_posts}) - # Картинка и дата публикации для meta-тегов берутся из связанного блога, если он есть. - if profile_blog_posts: - for blog_post in profile_blog_posts: - if blog_post.sImgForBlogSocial: - to_template['IMG_FOR_BLOG'] = blog_post.sImgForBlogSocial - break - - pub_dat: datetime = q_pvc_by_id.dProfileModify - if profile_blog_posts: - profile_blog_dat: datetime | None = max((post.dPostDataModify for post in profile_blog_posts), default=pub_dat) - pub_dat = update_pub_dat(pub_dat, profile_blog_dat) or pub_dat - to_template['PUB_DAT'] = pub_dat - to_template.update( - { - 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]), - 'LOG_VISIT': get_last_all_user_visit_list(), - 'ticks': float(time.time() - time_start), - } - ) - return render(request, "catalog/catalog_of_profiles_model.html", to_template) - - -def catalog_profile_manufacture(request: HttpRequest, manufacture_id: int, manufacture_name: str) -> HttpResponse: - """ - КАТАЛОГ ПРОФИЛЕЙ: страница с описанием производителя профилей и списком марки производимых им профилей - - :param request: HttpRequest -- входящий http-запрос - :param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription - :param manufacture_name: название производителя (транслитерированное pytils.translit.slugify()) - :return response: HttpResponse -- исходящий http-ответ - """ - time_start = time.time() - manufacture_id = int(manufacture_id) - q_pvc_by_id = PVCprofiles.objects.get(id=manufacture_id) - if pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) != manufacture_name: - return redirect(f'/catalog/profile/{manufacture_id}-' - f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}') - else: - q_pvc_by_id = PVCprofiles.objects.order_by('id') \ - .filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer).first() - if q_pvc_by_id.id != manufacture_id: - return redirect(f'/catalog/profile/{q_pvc_by_id.id}-' - f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}') - to_template = {'CATALOG_MANUFACT': q_pvc_by_id.sProfileManufacturer, - 'CATALOG_MAN2URL': manufacture_name, - 'CATALOG_URL': f"{manufacture_id}-{manufacture_name}"} - try: - # получаем информацию о производителе (статью из блога) - manufacture_description = list(PVCprofiles.objects.raw( - f"SELECT " - f" oknardia_blogposts.* " - f"FROM oknardia_catalog2profile" - f" RIGHT OUTER JOIN oknardia_pvcprofiles" - f" ON oknardia_catalog2profile.kProfile_id = oknardia_pvcprofiles.id" - f" LEFT OUTER JOIN oknardia_blogposts" - f" ON oknardia_catalog2profile.kBlogCatalog_id = oknardia_blogposts.id " - f"WHERE oknardia_catalog2profile.sCatalogCardType = {CATALOG_RECORD_FOR_PROFILE_MANUFACTURER} " - f" AND oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}'" - f" AND oknardia_blogposts.bCatalog IS TRUE " - f"GROUP BY oknardia_blogposts.bCatalog " - f"LIMIT 1;" - ))[0] - to_template.update({'PUB_DAT': manufacture_description.dPostDataModify}) - if PATH_FOR_IMG_BLOG in manufacture_description.sImgForBlogSocial: - to_template.update({'IMG_FOR_BLOG': manufacture_description.sImgForBlogSocial}) - to_template.update({'HEADER': manufacture_description.sPostHeader, - 'CONTENT': re.sub(r'', '', manufacture_description.sPostContent, - 0, re.IGNORECASE)}) - to_template.update({'TIZER': re.sub(r'||', - '', to_template["CONTENT"], 0, re.IGNORECASE)}) - except (ObjectDoesNotExist, IndexError, TypeError, KeyError,): - pass - q_profiles = PVCprofiles.objects.raw( - f"SELECT oknardia_pvcprofiles.id," - f" oknardia_pvcprofiles.fProfileRating," - f" oknardia_pvcprofiles.sProfileBriefDescription," - f" oknardia_pvcprofiles.sProfileName " - f"FROM oknardia_pvcprofiles " - f"WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}' " - f"ORDER BY oknardia_pvcprofiles.fProfileRating;" - ) - list_profiles = [] - for i in q_profiles: - list_profiles.append({ - "PROFILE_NAME": i.sProfileBriefDescription, - "PROFILE_ID": i.id, - "PROFILE_URL": pytils.translit.slugify(i.sProfileName).lower(), - "PROFILE_RATING": i.fProfileRating, - "PROFILE_RATING_STARS": get_rating_set_for_stars(i.fProfileRating), - }) - to_template.update({'PROFILES': list_profiles}) - try: - q_share_of_offers = list(PVCprofiles.objects.raw( - f"SELECT" - f" 1 AS id," - f" SUM(Q1.offers_by_model) AS offers_by_maufacture," - f" Q2.tatal_offers-SUM(Q1.offers_by_model) AS offers_other " - f"FROM (SELECT COUNT(oknardia_priceoffer.id) AS offers_by_model" - f" FROM oknardia_priceoffer" - f" LEFT OUTER JOIN oknardia_setkit" - f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id" - f" RIGHT OUTER JOIN oknardia_pvcprofiles" - f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id" - f" WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}') Q1," - f" (SELECT COUNT(oknardia_priceoffer.id) AS tatal_offers" - f" FROM oknardia_priceoffer) AS Q2 " - f"LIMIT 1;" - ))[0] - to_template.update({ - 'OFFERS_BY_MAUFACTURE': q_share_of_offers.offers_by_maufacture, - 'OFFERS_OTHER': q_share_of_offers.offers_other, - 'OFFERS_ANGLE': 90 + 180 * normalize(q_share_of_offers.offers_by_maufacture, - q_share_of_offers.offers_other + q_share_of_offers.offers_by_maufacture) - }) - if q_share_of_offers is not None and q_share_of_offers.offers_by_maufacture != 0: - q_merchant = PVCprofiles.objects.raw( - f"SELECT" - f" COUNT(oknardia_priceoffer.id) AS offers_by_merchant," - f" oknardia_merchantbrand.sMerchantName," - f" oknardia_merchantbrand.pMerchantLogo," - f" oknardia_merchantbrand.id " - f"FROM oknardia_priceoffer" - f" INNER JOIN oknardia_setkit" - f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id" - f" INNER JOIN oknardia_pvcprofiles" - f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.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_merchantbrand" - f" ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id " - f"WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}' " - f"GROUP BY oknardia_merchantbrand.sMerchantName," - f" oknardia_merchantbrand.pMerchantLogo," - f" oknardia_merchantbrand.id " - f"ORDER BY offers_by_merchant DESC;" - ) - list_merchant = [] - for i in q_merchant: - list_merchant.append({ - "MERCHANT_ID": i.id, - "MERCHANT_NAME": i.sMerchantName, - "MERCHANT_NAME_T": pytils.translit.slugify(i.sMerchantName), - "MERCHANT_LOGO_URL": i.pMerchantLogo, - "MERCHANT_OFFERS": i.offers_by_merchant - }) - to_template.update({'MERCHANTS': list_merchant}) - except (ObjectDoesNotExist, IndexError, TypeError): # вообще-то, запрос q_share_of_offers всегда что-то вернёт, - 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.time() - time_start) - }) - return render(request, "catalog/catalog_of_profiles_manufacture.html", to_template) - - # Каталог типовый серий зданий (пока переадресация) def catalog_seria(request: HttpRequest) -> HttpResponse: """ diff --git a/oknardia/web/catalog_profiles.py b/oknardia/web/catalog_profiles.py new file mode 100644 index 0000000..8a47fd2 --- /dev/null +++ b/oknardia/web/catalog_profiles.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +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.settings import * +from oknardia.models import Catalog2Profile, PVCprofiles, PriceOffer +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 +import time +import json +import re +import pytils + +def catalog_profile(request: HttpRequest) -> HttpResponse: + """ + КАТАЛОГ ПРОФИЛЕЙ: страница со списком производителей и моделей (марками) профилей + + :param request: HttpRequest -- входящий http-запрос + :return response: HttpResponse -- исходящий http-ответ + """ + time_start = time.time() + # Берём только те поля, которые реально нужны для построения страницы каталога. + # Это позволяет не тащить лишние данные из БД и сразу работать с простыми словарями. + profile_rows = list( + PVCprofiles.objects.values( + "id", + "sProfileName", + "sProfileBriefDescription", + "sProfileManufacturer", + ).order_by("sProfileManufacturer", "sProfileBriefDescription") + ) + profile_count = len(profile_rows) + to_template = { + 'CATALOG_PROFILE_NUM': pytils.numeral.get_plural(profile_count, "профиль,профиля,профилей") + } + # Локальный помощник: slug нужен несколько раз, а повторять одну и ту же строку не хочется. + def make_slug(value: str) -> str: + return pytils.translit.slugify(value).lower() + + list_profile_manufactures = [] + tmp_profile_manufacture = "" + for profile in profile_rows: + if profile["sProfileManufacturer"] == "": + # Пустой производитель в каталоге только мешает: не создаём для него отдельную группу. + continue + + if tmp_profile_manufacture != profile["sProfileManufacturer"]: + # Новый производитель — открываем новую группу карточек. + tmp_profile_manufacture = profile["sProfileManufacturer"] + list_profile_manufactures.append({ + "PROF_MAN_ID": profile["id"], + "PROF_MAN": profile["sProfileManufacturer"], + "PROF_MAN_T": make_slug(profile["sProfileManufacturer"]), + "PROF_MAN_LIST": [{ + "PROF_NAME_ID": profile["id"], + "PROF_NAME": profile["sProfileBriefDescription"], + "PROF_NAME_T": make_slug(profile["sProfileName"]), + }] + }) + else: + # Если производитель уже встречался, просто дописываем новую модель в его список. + list_profile_manufactures[-1]["PROF_MAN_LIST"].append({ + "PROF_NAME_ID": profile["id"], + "PROF_NAME": profile["sProfileBriefDescription"], + "PROF_NAME_T": make_slug(profile["sProfileName"]), + }) + + to_template.update({ + 'CATALOG_PROFILE_MAN1_NAME2': list_profile_manufactures, + 'CATALOG_MANUFACT_NUM': len(list_profile_manufactures), + 'CATALOG_MANUFACT_NUM_W': + pytils.numeral.sum_string(len(list_profile_manufactures), pytils.numeral.MALE, ("производитель", + "производителя", + "производителей")), + 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]), + 'LOG_VISIT': get_last_all_user_visit_list(), + 'ticks': float(time.time() - time_start), + }) + return render(request, "catalog/catalog_of_profiles.html", to_template) + + +def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture_name: str, + model_id: int, model_name: str) -> HttpResponse: + """ + КАТАЛОГ ПРОФИЛЕЙ: страница с описанием марки профиля + + :param request: HttpRequest -- входящий http-запрос + :param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription + :param manufacture_name: название производителя (транслитерированное pytils.translit.slugify()) + :param model_id: id модели (марки) профиля + :param model_name: модель (марка) профиля (транслитерированное pytils.translit.slugify(sProfileName)) + :return response: HttpResponse -- исходящий http-ответ + """ + time_start = time.time() + manufacture_id = int(manufacture_id) + model_id = int(model_id) + q_pvc_by_id = PVCprofiles.objects.get(id=model_id) + manufacturer_slug = pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) + model_slug = pytils.translit.slugify(q_pvc_by_id.sProfileName) + if manufacturer_slug != manufacture_name \ + or model_slug != model_name \ + or manufacture_id != model_id: + return redirect(f"/catalog/profile/{model_id}-{manufacturer_slug}/" + f"{model_id}-{model_slug}") + + # Локальные помощники держат вьюху короче и не размазывают однотипную логику по коду. + def make_slug(value: str) -> str: + return pytils.translit.slugify(value).lower() + + def build_other_list(value: str) -> list[str]: + # Убираем пустые куски, чтобы не плодить «пустые» характеристики в шаблоне. + result = [] + for chunk in (part.strip() for part in value.split(";")): + if not chunk: + continue + if ":" in chunk: + head, tail = chunk.split(":", 1) + result.append(f"{head.strip()}:{tail.strip()}") + else: + result.append(f"{chunk}") + return result + + def update_pub_dat(current_pub_dat: datetime | None, candidate_pub_dat: datetime | None) -> datetime | None: + # На странице оставляем дату публикации/обновления только если она реально новее карточки профиля. + if candidate_pub_dat is None: + return current_pub_dat + if current_pub_dat is None or candidate_pub_dat.replace(tzinfo=None) > current_pub_dat.replace(tzinfo=None): + return candidate_pub_dat + return current_pub_dat + + def apply_rating_colors(rating: dict, rating_pairs: tuple[tuple[str, str], ...], multiplier: int, + gray: bool = False) -> None: + # Один маленький helper вместо россыпи почти одинаковых строк: меняется только множитель и формат RGB. + for rating_key, template_key in rating_pairs: + color = int(255 - rating[rating_key] * multiplier) + if gray: + to_template[template_key] = f"{color},{color},{color}" + else: + to_template[template_key] = f"{color},255,{color}" + + def merchant_row_to_dict(row: dict) -> dict: + # Один маппер для строки с партнёром: ключи шаблона остаются как были. + merchant_name = row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"] + return { + "MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"], + "MERCHANT_NAME": merchant_name, + "MERCHANT_NAME_T": make_slug(merchant_name), + "MERCHANT_LOGO_URL": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo"], + "MERCHANT_OFFERS": row["offers_by_merchant"], + } + + def profile_row_to_dict(profile: dict) -> dict: + # И то же самое для списка соседних профилей производителя. + return { + "PROFILE_NAME": profile["sProfileBriefDescription"], + "PROFILE_ID": profile["id"], + "PROFILE_URL": make_slug(profile["sProfileName"]), + "PROFILE_RATING": profile["fProfileRating"], + "PROFILE_RATING_STARS": get_rating_set_for_stars(profile["fProfileRating"]), + } + + to_template: dict[str, object] = {"CATALOG_MODEL": q_pvc_by_id, + "CATALOG_MAN2URL": manufacture_name, + "CATALOG_URL": f"{manufacture_id}-{manufacture_name}", + "CATALOG_URL2": f"{manufacture_id}-{manufacture_name}/{model_id}-{model_name}", + "PROFILE_RATING_STARS": get_rating_set_for_stars(q_pvc_by_id.fProfileRating)} + try: + got_json = json.loads(q_pvc_by_id.sProfileDescription) + # раскрашиваем кружочки рейтинга напротив характеристик профиля + rating_pairs = ( + (RANK_PVCP_CAMERAS_NUM_NAME, "RANK_PVCP_CAMERAS_COLOR"), + (RANK_PVCP_SEALS_NAME, "RANK_PVCP_SEALS_COLOR"), + (RANK_PVCP_THICKNESS_NAME, "RANK_PVCP_THICKNESS_COLOR"), + (RANK_PVCP_G_THICKNESS_NAME, "RANK_PVCP_G_THICKNESS_COLOR"), + (RANK_PVCP_RABBET_NAME, "RANK_PVCP_RABBET_COLOR"), + (RANK_PVCP_HEAT_TRANSFER_NAME, "RANK_PVCP_HEAT_TRANSFER_COLOR"), + (RANK_PVCP_SOUNDPROOFING_NAME, "RANK_PVCP_SOUNDPROOFING_COLOR"), + (RANK_PVCP_HEIGHT_NAME, "RANK_PVCP_HEIGHT_COLOR"), + ) + if KEY_RATING in got_json: + # кружочки зелёные + apply_rating_colors(got_json[KEY_RATING], rating_pairs, 255) + elif KEY_RATING_VIRTUAL in got_json: + # кружочки серые + apply_rating_colors(got_json[KEY_RATING_VIRTUAL], rating_pairs, 64, gray=True) + else: + pass + if KEY_HTML in got_json: + to_template.update({"EXTRA_INFO": got_json[KEY_HTML]}) + except (TypeError, ValueError, KeyError): + pass + to_template.update({"LIST_OTHER": build_other_list(q_pvc_by_id.sProfileOther)}) + # Партнёров считаем через ORM: так код проще читать и легче переносить между СУБД. + q_merchant = ( + PriceOffer.objects.filter( + kOffer2SetKit__kSet2PVCprofiles_id=model_id, + sOfferActive=True, + ) + .values( + "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id", + "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName", + "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo", + ) + .annotate(offers_by_merchant=Count("id")) + .order_by("-offers_by_merchant", "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName") + ) + to_template.update({'MERCHANTS': [merchant_row_to_dict(row) for row in q_merchant]}) + # Близкие профили этого же производителя нужны для быстрых переходов по карточкам. + q_profiles = ( + PVCprofiles.objects.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer) + .exclude(id=model_id) + .values("id", "fProfileRating", "sProfileBriefDescription", "sProfileName") + .order_by("fProfileRating") + ) + to_template.update({'PROFILES': [profile_row_to_dict(profile) for profile in q_profiles]}) + # Описание профиля берём через связку каталог -> блог: это один ORM-запрос вместо сырого SQL. + q_profiles_detail = ( + Catalog2Profile.objects.filter( + kProfile_id=model_id, + sCatalogCardType=CATALOG_RECORD_FOR_PROFILE_MODEL, + kBlogCatalog__isnull=False, + ) + .select_related("kBlogCatalog") + .order_by("kBlogCatalog__iCatalogSort") + ) + profile_blog_posts = [row.kBlogCatalog for row in q_profiles_detail if row.kBlogCatalog is not None] + to_template.update({'PROFILE_DETAIL': profile_blog_posts}) + # Картинка и дата публикации для meta-тегов берутся из связанного блога, если он есть. + if profile_blog_posts: + for blog_post in profile_blog_posts: + if blog_post.sImgForBlogSocial: + to_template['IMG_FOR_BLOG'] = blog_post.sImgForBlogSocial + break + + pub_dat: datetime = q_pvc_by_id.dProfileModify + if profile_blog_posts: + profile_blog_dat: datetime | None = max((post.dPostDataModify for post in profile_blog_posts), default=pub_dat) + pub_dat = update_pub_dat(pub_dat, profile_blog_dat) or pub_dat + to_template['PUB_DAT'] = pub_dat + to_template.update( + { + 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]), + 'LOG_VISIT': get_last_all_user_visit_list(), + 'ticks': float(time.time() - time_start), + } + ) + return render(request, "catalog/catalog_of_profiles_model.html", to_template) + + +def catalog_profile_manufacture(request: HttpRequest, manufacture_id: int, manufacture_name: str) -> HttpResponse: + """ + КАТАЛОГ ПРОФИЛЕЙ: страница с описанием производителя профилей и списком марки производимых им профилей + + :param request: HttpRequest -- входящий http-запрос + :param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription + :param manufacture_name: название производителя (транслитерированное pytils.translit.slugify()) + :return response: HttpResponse -- исходящий http-ответ + """ + time_start = time.time() + manufacture_id = int(manufacture_id) + q_pvc_by_id = PVCprofiles.objects.get(id=manufacture_id) + if pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) != manufacture_name: + return redirect(f'/catalog/profile/{manufacture_id}-' + f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}') + else: + q_pvc_by_id = PVCprofiles.objects.order_by('id') \ + .filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer).first() + if q_pvc_by_id.id != manufacture_id: + return redirect(f'/catalog/profile/{q_pvc_by_id.id}-' + f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}') + to_template = {'CATALOG_MANUFACT': q_pvc_by_id.sProfileManufacturer, + 'CATALOG_MAN2URL': manufacture_name, + 'CATALOG_URL': f"{manufacture_id}-{manufacture_name}"} + try: + # получаем информацию о производителе (статью из блога) + manufacture_description = list(PVCprofiles.objects.raw( + f"SELECT " + f" oknardia_blogposts.* " + f"FROM oknardia_catalog2profile" + f" RIGHT OUTER JOIN oknardia_pvcprofiles" + f" ON oknardia_catalog2profile.kProfile_id = oknardia_pvcprofiles.id" + f" LEFT OUTER JOIN oknardia_blogposts" + f" ON oknardia_catalog2profile.kBlogCatalog_id = oknardia_blogposts.id " + f"WHERE oknardia_catalog2profile.sCatalogCardType = {CATALOG_RECORD_FOR_PROFILE_MANUFACTURER} " + f" AND oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}'" + f" AND oknardia_blogposts.bCatalog IS TRUE " + f"GROUP BY oknardia_blogposts.bCatalog " + f"LIMIT 1;" + ))[0] + to_template.update({'PUB_DAT': manufacture_description.dPostDataModify}) + if PATH_FOR_IMG_BLOG in manufacture_description.sImgForBlogSocial: + to_template.update({'IMG_FOR_BLOG': manufacture_description.sImgForBlogSocial}) + to_template.update({'HEADER': manufacture_description.sPostHeader, + 'CONTENT': re.sub(r'', '', manufacture_description.sPostContent, + 0, re.IGNORECASE)}) + to_template.update({'TIZER': re.sub(r'||', + '', to_template["CONTENT"], 0, re.IGNORECASE)}) + except (ObjectDoesNotExist, IndexError, TypeError, KeyError,): + pass + q_profiles = PVCprofiles.objects.raw( + f"SELECT oknardia_pvcprofiles.id," + f" oknardia_pvcprofiles.fProfileRating," + f" oknardia_pvcprofiles.sProfileBriefDescription," + f" oknardia_pvcprofiles.sProfileName " + f"FROM oknardia_pvcprofiles " + f"WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}' " + f"ORDER BY oknardia_pvcprofiles.fProfileRating;" + ) + list_profiles = [] + for i in q_profiles: + list_profiles.append({ + "PROFILE_NAME": i.sProfileBriefDescription, + "PROFILE_ID": i.id, + "PROFILE_URL": pytils.translit.slugify(i.sProfileName).lower(), + "PROFILE_RATING": i.fProfileRating, + "PROFILE_RATING_STARS": get_rating_set_for_stars(i.fProfileRating), + }) + to_template.update({'PROFILES': list_profiles}) + try: + q_share_of_offers = list(PVCprofiles.objects.raw( + f"SELECT" + f" 1 AS id," + f" SUM(Q1.offers_by_model) AS offers_by_maufacture," + f" Q2.tatal_offers-SUM(Q1.offers_by_model) AS offers_other " + f"FROM (SELECT COUNT(oknardia_priceoffer.id) AS offers_by_model" + f" FROM oknardia_priceoffer" + f" LEFT OUTER JOIN oknardia_setkit" + f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id" + f" RIGHT OUTER JOIN oknardia_pvcprofiles" + f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id" + f" WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}') Q1," + f" (SELECT COUNT(oknardia_priceoffer.id) AS tatal_offers" + f" FROM oknardia_priceoffer) AS Q2 " + f"LIMIT 1;" + ))[0] + to_template.update({ + 'OFFERS_BY_MAUFACTURE': q_share_of_offers.offers_by_maufacture, + 'OFFERS_OTHER': q_share_of_offers.offers_other, + 'OFFERS_ANGLE': 90 + 180 * normalize(q_share_of_offers.offers_by_maufacture, + q_share_of_offers.offers_other + q_share_of_offers.offers_by_maufacture) + }) + if q_share_of_offers is not None and q_share_of_offers.offers_by_maufacture != 0: + q_merchant = PVCprofiles.objects.raw( + f"SELECT" + f" COUNT(oknardia_priceoffer.id) AS offers_by_merchant," + f" oknardia_merchantbrand.sMerchantName," + f" oknardia_merchantbrand.pMerchantLogo," + f" oknardia_merchantbrand.id " + f"FROM oknardia_priceoffer" + f" INNER JOIN oknardia_setkit" + f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id" + f" INNER JOIN oknardia_pvcprofiles" + f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.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_merchantbrand" + f" ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id " + f"WHERE oknardia_pvcprofiles.sProfileManufacturer = '{q_pvc_by_id.sProfileManufacturer}' " + f"GROUP BY oknardia_merchantbrand.sMerchantName," + f" oknardia_merchantbrand.pMerchantLogo," + f" oknardia_merchantbrand.id " + f"ORDER BY offers_by_merchant DESC;" + ) + list_merchant = [] + for i in q_merchant: + list_merchant.append({ + "MERCHANT_ID": i.id, + "MERCHANT_NAME": i.sMerchantName, + "MERCHANT_NAME_T": pytils.translit.slugify(i.sMerchantName), + "MERCHANT_LOGO_URL": i.pMerchantLogo, + "MERCHANT_OFFERS": i.offers_by_merchant + }) + to_template.update({'MERCHANTS': list_merchant}) + except (ObjectDoesNotExist, IndexError, TypeError): # вообще-то, запрос q_share_of_offers всегда что-то вернёт, + 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.time() - time_start) + }) + return render(request, "catalog/catalog_of_profiles_manufacture.html", to_template) \ No newline at end of file