diff --git a/README.md b/README.md
index 5807fbb..9463d08 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,7 @@
* Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии).
* Добавлена management-команда `regenerate_seria_prerender` для оффлайн-пересборки pre-render шаблонов `catalog_seria_info` (все или выбранные root-серии), с режимами `--dry-run` и `--force`; серверный reload (Gunicon? uWSGI или что там еще будет) должен быть вынесен из кода приложения в оркестрацию (cron/systemd/deploy step).
* Рефакторинг `standard_opening`: raw SQL -> ORM, упрощена дедублекация, убраны лишние запросы и переменные контекста, добавлены комментарии, SEO-описание и keywords, стандартизирован хвост контекста с визитами и `ticks` через общий helper внутри `catalog_openings.py`.
-*
-*
+* Рефакторинг `catalog_company` и `catalog_company_detail` (`/catalog/company`): raw SQL → ORM для получения списка компаний и их наборов, вынесены вспомогательные функции (`_get_company_statistics`, `_get_company_sets_detail`, `_format_company_for_template`, `_format_set_for_template`, `_clean_text_field`, `_lowercase_first_char`), упрощена логика форматирования данных, добавлены подробные комментарии и docstring для каждой функции, использованы `select_related` и `annotate` для оптимизации запросов, добавлена защита от `Http404` при неправильных slugs. Улучшены SEO-атрибуты, и добавлена разметка shema.org.
*
*
diff --git a/oknardia/templates/catalog/catalog_company.html b/oknardia/templates/catalog/catalog_company.html
index 42caf0d..6f9f10a 100755
--- a/oknardia/templates/catalog/catalog_company.html
+++ b/oknardia/templates/catalog/catalog_company.html
@@ -1,58 +1,81 @@
{% extends "base.html" %}
{% load static %}{% load filters %}
-{% block Title %}Каталог изготовителей и поставщиков окон{% endblock %}
+{% block Title %}Каталог оконных компаний: производители и поставщики окон, рейтинг и цены{% endblock %}
{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %}
-{% block Description %}Каталог изготовителей окон, партнёры «Окнардия», рейтинг, {% for i in COMPANIES %}{{ i.sMerchantName }}, {% endfor %} средняя цена окна{% endblock %}
+{% block Description %}Актуальный каталог оконных компаний России. Сравните производителей и поставщиков пластиковых окон по рейтингу, ассортименту, средней цене и дате последнего обновления.{% endblock %}
-{% block Keywords %}Оконные компании, {% for i in COMPANIES %}{{ i.sMerchantName }}, {% endfor %} изготовители окон, производители окон, постащики окон, партнёры, каталог компаний, каталог оконных компаний, oknardia, окнардия {{ META_KEYWORDS|default:"" }} {% endblock %}
+{% block Keywords %}оконные компании, каталог компаний, производители окон, поставщики окон, рейтинг оконных компаний, сравнить цены на окна, oknardia, окнардия{% endblock %}
-{% block Date4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %}
+{% block Author4Meta %}: Каталог «Окнардия»{% endblock %}
-{% block Last4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %}
-
-{% block Author4Meta %}: Каталог изготовителей окон{% endblock %}
-
-{% block CopyrightAuthor4Meta %}: Каталог изготовителей окон{% endblock %}
+{% block CopyrightAuthor4Meta %}: Каталог «Окнардия»{% endblock %}
{% block Top_Meta1 %}{# #}
- {% if IMG_FOR_BLOG %}
- {% else %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
+ {# Удалить: — устаревший тег #}
+
+
{# #}{% endblock %}
+{% block ADD_TO_HEAD %}{% comment %}
+JSON-LD для страницы-списка компаний: CollectionPage + ItemList с элементами Organization.
+Это понятнее для поисковиков, чем legacy microdata на метатегах.
+{% endcomment %}
+
+{% endblock %}
+
+
{% block Main_Content %}
{# #}
@@ -90,7 +113,3 @@
{% include "report/report_log_user_visit.html" %}
{% endblock %}
-
-
-
-
diff --git a/oknardia/templates/catalog/catalog_company_detail.html b/oknardia/templates/catalog/catalog_company_detail.html
index 03b251b..f7d4308 100755
--- a/oknardia/templates/catalog/catalog_company_detail.html
+++ b/oknardia/templates/catalog/catalog_company_detail.html
@@ -5,30 +5,20 @@
{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %}
-{% block Description %}«{{ COMPANY }}», описание компании «{{ COMPANY }}», оконные наборы от «{{ COMPANY }}» и их состав, характеристики «{{ COMPANY }}», рейтинг «{{ COMPANY }}», средние цены и отклонение цен «{{ COMPANY }}».{% endblock %}
+{% block Description %}Производитель окон «{{ COMPANY }}» в каталоге Окнардии: оконные наборы, их состав и характеристики, независимый рейтинг качества, средние цены на замену оконных конструкций в типовых домах.{% endblock %}
-{% block Keywords %}{{ COMPANY }}, компания {{ COMPANY }}, окна {{ COMPANY }}, изготовитель окон {{ COMPANY }}, производитель окон {{ COMPANY }}, поставщик окон {{ COMPANY }}, партнёр, каталог компаний, каталог оконных компаний, oknardia, окнардия {{ META_KEYWORDS|default:"" }} {% endblock %}
+{% block Keywords %}{{ COMPANY }}, компания {{ COMPANY }}, окна {{ COMPANY }}, изготовитель окон {{ COMPANY }}, производитель окон {{ COMPANY }}, поставщик окон {{ COMPANY }}, партнёр, каталог компаний, каталог оконных компаний, oknardia, окнардия{% endblock %}
-{% block Date4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %}
-{% block Last4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %}
+{% block Author4Meta %}Каталог изготовителей окон{% endblock %}
-{% block Author4Meta %}: Каталог изготовителей окон{% endblock %}
-
-{% block CopyrightAuthor4Meta %}: Каталог изготовителей окон{% endblock %}
+{% block CopyrightAuthor4Meta %}Каталог изготовителей окон{% endblock %}
{% block Top_Meta1 %}{# #}
- {% if IMG_FOR_BLOG %}
- {% else %}
- {% endif %}
-
-
-
-
-
+ {# Microdata (itemprop) убрана — заменена на JSON-LD в блоке ADD_TO_HEAD ниже (чище, надёжнее) #}
-
+ {# Удалить: — тег Google News 2011 г., отменён в 2014, поисковики игнорируют #}
@@ -36,24 +26,59 @@
-
+
-
+
-
-
+
+
-
+ {# Удалить: — устарело с 2015, Twitter его не использует #}
{# #}{% endblock %}
+{% block Top_JS5 %}
+ {% endblock %}
-{% block Top_JS5 %}{% endblock %}
+{% block ADD_TO_HEAD %}{% comment %}
+ JSON-LD разметка Schema.org для страницы производителя окон.
+ Тип LocalBusiness описывает компанию-поставщика окон: название, контакты, адрес, геокоординаты,
+ логотип и ссылку на официальный сайт производителя.
+ Данные берутся из первого набора в SETS (все наборы принадлежат одному офису/бренду),
+ поэтому достаточно SETS.0 для контактной информации.
+ Документация: https://schema.org/LocalBusiness #}{% endcomment %}
+
+{% endblock %}
{% block Main_Content %}
diff --git a/oknardia/web/catalog_companies.py b/oknardia/web/catalog_companies.py
index 7cc9d62..882ba12 100644
--- a/oknardia/web/catalog_companies.py
+++ b/oknardia/web/catalog_companies.py
@@ -1,11 +1,27 @@
# -*- coding: utf-8 -*-
+"""
+Каталог производителей и компаний.
+
+Модуль предоставляет views для отображения:
+1. Списка всех производителей с их ключевыми показателями (рейтинг, количество
+ предложений, среднюю цену и т.п.)
+2. Детальную информацию о конкретном производителе со всеми его оконными наборами
+
+Все запросы переведены на Django ORM для лучшей производительности и чистоты кода.
+"""
from django.shortcuts import render, redirect
-from django.http import HttpRequest, HttpResponse
-from django.utils import timezone
+from django.http import HttpRequest, HttpResponse, Http404
+from django.db.models import Count, Avg, Max, Min, DecimalField
from oknardia.models import (
MerchantBrand,
+ SetKit,
+ PriceOffer,
+)
+from web.report1 import (
+ get_last_all_user_visit_list,
+ get_last_user_visit_cookies,
+ get_last_user_visit_list
)
-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 get_rating_set_for_stars
import django.utils.dateformat
import time
@@ -14,148 +30,500 @@ import re
import pytils
+def _get_company_statistics() -> list:
+ """
+ Получает список компаний (MerchantBrand) с агрегированной статистикой.
+
+ Статистика включает:
+ - Количество оконных наборов от компании
+ - Средний рейтинг наборов
+ - Количество ценовых предложений
+ - Среднюю цену предложений
+ - Дату последнего обновления цены
+
+ Оптимизировано для минимизации запросов к БД.
+
+ Returns:
+ list: Список словарей с данными компаний
+ """
+ # 1. Статистика по наборам (SetKit) для каждой компании
+ set_stats = (
+ SetKit.objects
+ .filter(kSet2User__kMerchantOffice__kMerchantName__isnull=False)
+ .values('kSet2User__kMerchantOffice__kMerchantName_id')
+ .annotate(
+ num_sets=Count('id', distinct=True),
+ avg_rating=Avg('fSetRating')
+ )
+ )
+ set_stats_dict = {
+ stat['kSet2User__kMerchantOffice__kMerchantName_id']: {
+ 'num_sets': stat['num_sets'],
+ 'avg_rating': stat['avg_rating'] or 0
+ }
+ for stat in set_stats
+ }
+
+ # 2. Статистика по ценовым предложениям (PriceOffer)
+ companies_data = (
+ PriceOffer.objects
+ .filter(
+ sOfferActive=True,
+ kOfferFromUser__kMerchantOffice__kMerchantName__isnull=False
+ )
+ .values('kOfferFromUser__kMerchantOffice__kMerchantName_id')
+ .annotate(
+ num_offers=Count('id', distinct=True),
+ price_avg=Avg('fOfferPrice', output_field=DecimalField()),
+ last_update=Max('dOfferModify')
+ )
+ .order_by('-last_update')
+ )
+
+ # 3. Получаем все объекты MerchantBrand одним запросом (решение проблемы N+1)
+ company_ids = [
+ offer['kOfferFromUser__kMerchantOffice__kMerchantName_id']
+ for offer in companies_data
+ ]
+ merchants = MerchantBrand.objects.in_bulk(company_ids)
+
+ # 4. Собираем финальный результат
+ result = []
+ for offer in companies_data:
+ company_id = offer['kOfferFromUser__kMerchantOffice__kMerchantName_id']
+ merchant = merchants.get(company_id)
+
+ if not merchant:
+ continue
+
+ set_stat = set_stats_dict.get(company_id, {
+ 'num_sets': 0,
+ 'avg_rating': 0
+ })
+
+ result.append({
+ 'id': merchant.id,
+ 'sMerchantName': merchant.sMerchantName,
+ 'pMerchantLogo': merchant.pMerchantLogo,
+ 'NumSets': set_stat['num_sets'],
+ 'RatingAVG': set_stat['avg_rating'],
+ 'NumOffers': offer['num_offers'],
+ 'PriceAVG': offer['price_avg'],
+ 'lastUpdate': offer['last_update']
+ })
+
+ # Сортируем по среднему рейтингу (убывание)
+ result.sort(key=lambda x: x['RatingAVG'], reverse=True)
+
+ return result
+
+
+def _format_company_for_template(company_data: dict) -> dict:
+ """
+ Форматирует данные компании для вывода в шаблон.
+
+ Применяет:
+ - Конвертацию времени в читаемый формат (e.g., "3 дня назад")
+ - Склонение существительных (plural forms)
+ - Вычисление звёзд рейтинга
+ - Скатывание имени в slug для URL
+
+ Args:
+ company_data (dict): Словарь с данными компании
+
+ Returns:
+ dict: Отформатированные данные компании
+ """
+ formatted = company_data.copy()
+
+ # Вычисляем звёзды на основе рейтинга
+ formatted['STARS'] = get_rating_set_for_stars(
+ formatted['RatingAVG']
+ )
+
+ # Применяем правильные формы множественного числа
+ formatted['NumSets'] = pytils.numeral.get_plural(
+ formatted['NumSets'],
+ "оконный набор, оконных набора, оконных наборов"
+ )
+ formatted['NumOffers'] = pytils.numeral.get_plural(
+ formatted['NumOffers'],
+ "вариант, варианта, вариантов"
+ )
+
+ # Конвертируем время последнего обновления в читаемый формат
+ if formatted['lastUpdate']:
+ timestamp = int(
+ django.utils.dateformat.format(
+ formatted['lastUpdate'],
+ 'U'
+ )
+ )
+ formatted['lastUpdate'] = pytils.dt.distance_of_time_in_words(
+ timestamp
+ )
+
+ # Генерируем slug из имени компании для URL
+ formatted['sMerchantMainURL'] = pytils.translit.slugify(
+ formatted['sMerchantName']
+ )
+
+ return formatted
+
+
def catalog_company(request: HttpRequest) -> HttpResponse:
- time_start = time.perf_counter()
- to_template: dict[str, object] = {} # словарь, для передачи шаблону
- q_company = MerchantBrand.objects.raw('SELECT'
- ' oknardia_merchantbrand.id,'
- ' oknardia_merchantbrand.sMerchantName,'
- ' oknardia_merchantbrand.pMerchantLogo,'
- ' oknardia_merchantbrand.sMerchantMainURL,'
- ' COUNT(oknardia_priceoffer.id) AS NumOffers,'
- ' AVG(oknardia_priceoffer.fOfferPrice) AS PriceAVG,'
- ' MAX(oknardia_priceoffer.dOfferModify) AS lastUpdate,'
- ' Q.NumSets,'
- ' Q.RatingAVG,'
- ' 1 AS STARS '
- 'FROM (SELECT'
- ' COUNT(oknardia_setkit.sSetName) AS NumSets,'
- ' oknardia_merchantoffice.kMerchantName_id AS Q_ID,'
- ' AVG(oknardia_setkit.fSetRating) AS RatingAVG'
- ' FROM oknardia_merchantoffice'
- ' INNER JOIN oknardia_ouruser'
- ' ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id'
- ' INNER JOIN oknardia_setkit'
- ' ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id'
- ' GROUP BY oknardia_merchantoffice.id,'
- ' oknardia_merchantoffice.kMerchantName_id) AS Q,'
- ' oknardia_ouruser'
- ' INNER JOIN oknardia_merchantoffice'
- ' ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id'
- ' INNER JOIN oknardia_priceoffer'
- ' ON oknardia_priceoffer.kOfferFromUser_id = oknardia_ouruser.id'
- ' INNER JOIN oknardia_merchantbrand'
- ' ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id'
- ' WHERE Q_ID = oknardia_merchantoffice.kMerchantName_id '
- 'GROUP BY oknardia_merchantoffice.kMerchantName_id '
- 'ORDER BY Q.RatingAVG DESC;')
- list_company = list(q_company)
- for i in list_company:
- i.STARS = get_rating_set_for_stars(i.RatingAVG)
- i.NumSets = pytils.numeral.get_plural(i.NumSets, u"оконный набор, оконных набора, оконных наборов")
- i.NumOffers = pytils.numeral.get_plural(i.NumOffers, u"вариант, варианта, вариантов")
- i.lastUpdate = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.lastUpdate, 'U')))
- i.sMerchantMainURL = pytils.translit.slugify(i.sMerchantName)
- # print("NAME:", i.sMerchantName, "\tNumSets:", i.NumSets, "\tNumOffers:", i.NumOffers,
- # "\t:AverageRating:", i.RatingAVG, "\tAveragePrice:", i.PriceAVG, "\tSTARS:", i.STARS)
- to_template.update({
- 'COMPANIES': list_company,
- # получаем последние визиты клиента через куки
- 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
- # получаем последние визиты всех посетителей из базы
- # id2log, log_visit = get_last_all_user_visit_list()
+ """
+ Показывает список всех производителей с ключевыми показателями.
+
+ GET параметры: опционально могут использоваться для фильтрации
+
+ Контекст шаблона:
+ - COMPANIES (list): Список компаний с статистикой
+ - LAST_VISIT (list): Последние визиты текущего пользователя
+ - LOG_VISIT (list): Последние визиты всех пользователей
+
+ Args:
+ request (HttpRequest): HTTP запрос от клиента
+
+ Returns:
+ HttpResponse: Отрендеренная HTML страница со списком компаний
+ """
+ # Получаем статистику по компаниям с использованием ORM
+ companies_list = _get_company_statistics()
+
+ # Форматируем каждую компанию для вывода в шаблон
+ formatted_companies = [
+ _format_company_for_template(company)
+ for company in companies_list
+ ]
+
+ # Получаем информацию о посещениях для персонализации
+ to_template: dict[str, object] = {
+ 'COMPANIES': formatted_companies,
+ '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.perf_counter() - time_start)
- })
+ }
+
return render(request, "catalog/catalog_company.html", to_template)
-def catalog_company_detail(request: HttpRequest, company_id: str, company_name_slug: str) -> HttpResponse:
+def _lowercase_first_char(text: str) -> str:
+ """
+ Преобразует первый символ строки в нижний регистр.
+
+ Args:
+ text (str): Исходная строка
+
+ Returns:
+ str: Строка с строчным первым символом (если длина > 0)
+ """
+ return text[0].lower() + text[1:] if len(text) > 0 else text
+
+
+def _clean_text_field(text: str, empty_values: list) -> str:
+ """
+ Очищает текстовое поле, удаляя типичные маркеры "пусто" и преобразуя
+ первый символ в нижний регистр.
+
+ Args:
+ text (str): Исходный текст
+ empty_values (list): Список значений, которые считаются "пустыми"
+
+ Returns:
+ str: Очищенный текст или пустая строка если значение в empty_values
+ """
+ if text.lower() in empty_values:
+ return ""
+ return _lowercase_first_char(text)
+
+
+def _get_company_sets_detail(company_id: int) -> list:
+ """
+ Получает все оконные наборы для компании с полной статистикой по ценам.
+
+ Использует оптимизированные select_related и prefetch_related для минимизации
+ запросов к БД. Группирует данные по наборам (SetKit) с уникальностью.
+
+ Args:
+ company_id (int): ID компании (MerchantBrand)
+
+ Returns:
+ list: Список словарей с данными наборов, отсортированные по рейтингу
+ """
+ # Получаем активные ценовые предложения для компаний с агрегацией по наборам
+ price_stats = (
+ PriceOffer.objects
+ .filter(
+ sOfferActive=True,
+ kOfferFromUser__kMerchantOffice__kMerchantName_id=company_id
+ )
+ .values('kOffer2SetKit_id')
+ .annotate(
+ num_offers=Count('id'),
+ price_avg=Avg('fOfferPrice', output_field=DecimalField()),
+ last_update=Max('dOfferModify'),
+ early_creation=Min('dOfferCreate')
+ )
+ )
+
+ # Преобразуем в словарь для быстрого доступа по ID набора
+ price_stats_dict = {
+ stat['kOffer2SetKit_id']: {
+ 'num_offers': stat['num_offers'],
+ 'price_avg': stat['price_avg'],
+ 'last_update': stat['last_update'],
+ 'early_creation': stat['early_creation']
+ }
+ for stat in price_stats
+ }
+
+ # Получаем все наборы компании с их зависимостями
+ # select_related оптимизирует ForeignKey запросы (профиль, стеклопакет)
+ sets_queryset = (
+ SetKit.objects
+ .filter(
+ kSet2User__kMerchantOffice__kMerchantName_id=company_id
+ )
+ .select_related(
+ 'kSet2User',
+ 'kSet2User__kMerchantOffice',
+ 'kSet2User__kMerchantOffice__kMerchantName',
+ 'kSet2PVCprofiles',
+ 'kSet2Glazing'
+ )
+ .order_by('-fSetRating')
+ )
+
+ # Собираем результат, комбинируя данные SetKit с агрегированной статистикой
+ result = []
+ seen_set_ids = set()
+
+ for setkit in sets_queryset:
+ # Пропускаем дубликаты наборов (может быть несколько ценовых предложений
+ # для одного набора)
+ if setkit.id in seen_set_ids:
+ continue
+ seen_set_ids.add(setkit.id)
+
+ # Получаем статистику по ценам для этого набора
+ price_stat = price_stats_dict.get(setkit.id, {
+ 'num_offers': 0,
+ 'price_avg': None,
+ 'last_update': None,
+ 'early_creation': None
+ })
+
+ # Собираем все данные в один объект
+ result.append({
+ 'setkit': setkit,
+ 'num_offers': price_stat['num_offers'],
+ 'price_avg': price_stat['price_avg'],
+ 'last_update': price_stat['last_update'],
+ 'early_creation': price_stat['early_creation'],
+ 'merchant_office': setkit.kSet2User.kMerchantOffice,
+ 'merchant_brand': setkit.kSet2User.kMerchantOffice.kMerchantName,
+ 'profile': setkit.kSet2PVCprofiles,
+ 'glazing': setkit.kSet2Glazing
+ })
+
+ return result
+
+
+def _format_set_for_template(set_data: dict, empty_values: list) -> dict:
+ """
+ Форматирует данные оконного набора для вывода в шаблон.
+
+ Применяет:
+ - Преобразование URL в удобный для отображения формат
+ - Разделение email адресов на части (для обфускации)
+ - Вычисление звёзд рейтинга
+ - Конвертация времени в читаемый формат
+ - Создание slugs для названий и производителей
+ - Склонение числительных(контуры, швы и т.п.)
+ - Очистку пустых полей от стандартных маркеров ("нет", "—" и т.п.)
+
+ Args:
+ set_data (dict): Данные набора с объектами моделей
+ empty_values (list): Список значений, считаемых "пустыми"
+
+ Returns:
+ dict: Отформатированные данные для шаблона
+ """
+ set_kit = set_data['setkit']
+ merchant_office = set_data['merchant_office']
+ merchant_brand = set_data['merchant_brand']
+ profile = set_data['profile']
+ glazing = set_data['glazing']
+
+ formatted = {
+ # Ключи ниже оставлены в legacy-формате, т.к. шаблон использует именно их имена.
+ 'sSetName': set_kit.sSetName,
+ 'sMerchantName': merchant_brand.sMerchantName,
+ 'sMerchantDescription': merchant_brand.sMerchantDescription,
+ 'fSetRating': {
+ 'RATING': set_kit.fSetRating,
+ 'STARS': get_rating_set_for_stars(set_kit.fSetRating)
+ },
+ 'num_offers': set_data['num_offers'],
+ 'price_avg': set_data['price_avg'],
+ 'bSetDelivery': set_kit.bSetDelivery,
+ 'bSetUninstallInstall': set_kit.bSetUninstallInstall,
+ 'sSetImplementAll': set_kit.sSetImplementAll,
+ 'sSetImplementHandles': set_kit.sSetImplementHandles,
+ 'sMerchantMainURL': {
+ 'URL': merchant_office.kMerchantName.sMerchantMainURL,
+ 'URL_VIEW': re.sub(
+ r"^https?://|/$|www\.",
+ "",
+ merchant_office.kMerchantName.sMerchantMainURL
+ )
+ },
+ 'sOfficePhones': merchant_office.sOfficePhones,
+ 'sOfficeDescription': merchant_office.sOfficeDescription,
+ 'sOfficeEmails': merchant_office.sOfficeEmails,
+ 'sOfficeName': merchant_office.sOfficeName,
+ 'sOfficeAddress': merchant_office.sOfficeAddress,
+ 'fOfficeGeoCode_Latitude': merchant_office.fOfficeGeoCode_Latitude,
+ 'fOfficeGeoCode_Longitude': merchant_office.fOfficeGeoCode_Longitude,
+ 'sOfficeDiscountMetaFormula': merchant_office.sOfficeDiscountMetaFormula,
+ 'pMerchantLogo': merchant_office.kMerchantName.pMerchantLogo,
+ 'idPVC': profile.id,
+ 'sProfileBriefDescription': profile.sProfileBriefDescription,
+ 'iProfileCameras': profile.iProfileCameras,
+ 'sProfileName': {
+ 'NAME': profile.sProfileName,
+ 'NAME_T': pytils.translit.slugify(profile.sProfileName)
+ },
+ 'sProfileManufacturer': {
+ 'NAME': profile.sProfileManufacturer,
+ 'NAME_T': pytils.translit.slugify(profile.sProfileManufacturer)
+ },
+ 'sProfileColor': profile.sProfileColor,
+ 'sProfileSealDescription': profile.sProfileSealDescription,
+ 'fProfileSeals': pytils.numeral.sum_string(
+ profile.fProfileSeals,
+ pytils.numeral.MALE,
+ "контур, контура, контуров"
+ ),
+ 'sGlazingBriefDescription': glazing.sGlazingBriefDescription,
+ 'sGlazingManufacturer': glazing.sGlazingManufacturer,
+ 'sGlazingMark': glazing.sGlazingMark,
+ 'sGlazingToning': glazing.sGlazingToning,
+ 'sSetImplementCatch': _clean_text_field(set_kit.sSetImplementCatch, empty_values),
+ 'sSetClimateControl': _clean_text_field(set_kit.sSetClimateControl, empty_values),
+ 'sProfileReinforcement': _lowercase_first_char(profile.sProfileReinforcement),
+ 'sSetSill': _lowercase_first_char(set_kit.sSetSill),
+ 'sSetPanes': _lowercase_first_char(set_kit.sSetPanes),
+ 'sSetSlope': _lowercase_first_char(set_kit.sSetSlope),
+ 'sSetUninstallInstall': _lowercase_first_char(set_kit.sSetUninstallInstall),
+ 'sSetDelivery': _lowercase_first_char(set_kit.sSetDelivery),
+ 'sSetOtherConditions': _lowercase_first_char(set_kit.sSetOtherConditions),
+ }
+
+ # Конвертируем даты в читаемый формат
+ if set_data['last_update']:
+ timestamp = int(django.utils.dateformat.format(set_data['last_update'], 'U'))
+ formatted['lastUpdate'] = pytils.dt.distance_of_time_in_words(timestamp)
+
+ if set_data['early_creation']:
+ timestamp = int(django.utils.dateformat.format(set_data['early_creation'],'U'))
+ formatted['earlyCreation'] = pytils.dt.distance_of_time_in_words(timestamp)
+
+ # Разделяем email на части для обфускации (показываем середину отдельно)
+ # На фронтенде JS собирает все обратно в валидный e-mail
+ if formatted['sOfficeEmails']:
+ try:
+ email_len = len(formatted['sOfficeEmails'])
+ k = random.randint(1, max(1, int(email_len / 2) - 1))
+ formatted['sOfficeEmails'] = [
+ formatted['sOfficeEmails'][0:k],
+ formatted['sOfficeEmails'][k:-k],
+ formatted['sOfficeEmails'][-k:]
+ ]
+ except (ValueError, ZeroDivisionError):
+ # Если ошибка при случайном разделении, оставляем как есть
+ pass
+
+ return formatted
+
+
+def catalog_company_detail(
+ request: HttpRequest,
+ company_id: str,
+ company_name_slug: str
+) -> HttpResponse:
+ """
+ Показывает детальную информацию о компании и все её оконные наборы.
+
+ Производит редирект если slug в URL не совпадает с актуальным.
+
+ GET параметры: опционально могут использоваться для фильтрации
+
+ Контекст шаблона:
+ - COMPANY (str): Название компании
+ - COMPANY_ID (int): ID компании
+ - COMPANY_T (str): Slug компании
+ - SETS (list): Список оконных наборов с их полной информацией
+ - IMG_FOR_BLOG (str): Логотип компании
+ - LIST_NOT (list): Стандартные маркеры "пусто"
+ - LAST_VISIT (list): Последние визиты текущего пользователя
+ - LOG_VISIT (list): Последние визиты всех пользователей
+ - ticks (float): Время выполнения представления (в секундах)
+
+ Args:
+ request (HttpRequest): HTTP запрос от клиента
+ company_id (str): ID компании в виде строки
+ company_name_slug (str): Slug названия компании из URL
+
+ Returns:
+ HttpResponse: Отрендеренная HTML страница с деталью компании или редирект
+ """
time_start = time.perf_counter()
- to_template: dict[str, object] = {} # словарь, для передачи шаблону
- company_id = int(company_id)
- q_by_id = MerchantBrand.objects.get(id=company_id)
- if pytils.translit.slugify(q_by_id.sMerchantName) != company_name_slug:
- return redirect('/catalog/company/%d-%s' % (company_id, pytils.translit.slugify(q_by_id.sMerchantName)))
- to_template.update({'COMPANY': q_by_id.sMerchantName})
- to_template.update({'COMPANY_ID': company_id})
- to_template.update({'COMPANY_T': company_name_slug})
- list_not = [u"нет", u"—", ""]
- to_template.update({'LIST_NOT': list_not})
- q_sets = MerchantBrand.objects.raw(f"SELECT"
- f" COUNT(oknardia_priceoffer.id) AS NumOffers,"
- f" AVG(oknardia_priceoffer.fOfferPrice) AS priceAVG,"
- f" MAX(oknardia_priceoffer.dOfferModify) AS lastUpdate,"
- f" MIN(oknardia_priceoffer.dOfferCreate) AS earlyCreation,"
- f" oknardia_merchantbrand.*,"
- f" oknardia_merchantoffice.*,"
- f" oknardia_merchantoffice.id AS idMERCH,"
- f" oknardia_setkit.*,"
- f" oknardia_setkit.id AS idSET,"
- f" oknardia_pvcprofiles.*,"
- f" oknardia_pvcprofiles.id AS idPVC,"
- f" oknardia_glazing.*, "
- f" oknardia_glazing.id AS idGLAZ "
- f"FROM oknardia_ouruser"
- 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" INNER JOIN oknardia_setkit"
- f" ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id"
- f" INNER JOIN oknardia_priceoffer"
- 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_glazing"
- f" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id "
- f"WHERE oknardia_merchantbrand.id = {company_id} "
- f"AND oknardia_priceoffer.sOfferActive = TRUE "
- f"GROUP BY oknardia_merchantoffice.id,"
- f" oknardia_setkit.id,"
- f" oknardia_setkit.fSetRating "
- f"ORDER BY oknardia_setkit.fSetRating DESC;")
- list_sets = list(q_sets)
- for i in list_sets:
- i.sMerchantMainURL = {"URL": i.sMerchantMainURL,
- "URL_VIEW": re.sub(r"(?:^http://|^https://|/$|www\.)", "", i.sMerchantMainURL)}
- k = random.randint(1, int(len(i.sOfficeEmails)/2) - 1)
- i.sOfficeEmails = [i.sOfficeEmails[0:k], i.sOfficeEmails[k:-k], i.sOfficeEmails[-k:]]
- to_template.update({'IMG_FOR_BLOG': i.pMerchantLogo})
- i.fSetRating = {"RATING": i.fSetRating,
- "STARS": get_rating_set_for_stars(i.fSetRating)}
- i.lastUpdate = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.lastUpdate, 'U')))
- i.earlyCreation = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.earlyCreation, 'U')))
- i.sProfileName = {"NAME": i.sProfileName,
- "NAME_T": pytils.translit.slugify(i.sProfileName)}
- i.sProfileManufacturer = {"NAME": i.sProfileManufacturer,
- "NAME_T": pytils.translit.slugify(i.sProfileManufacturer)}
- i.fProfileSeals = pytils.numeral.sum_string(i.fProfileSeals, pytils.numeral.MALE, u"контур, контура, контуров")
- if i.sSetImplementCatch.lower() in list_not:
- i.sSetImplementCatch = ""
- if i.sSetClimateControl.lower() in list_not:
- i.sSetClimateControl = ""
- if len(i.sProfileReinforcement) > 0:
- i.sProfileReinforcement = i.sProfileReinforcement[0].lower()+i.sProfileReinforcement[1:]
- if len(i.sSetSill) > 0:
- i.sSetSill = i.sSetSill[0].lower()+i.sSetSill[1:]
- if len(i.sSetPanes) > 0:
- i.sSetPanes = i.sSetPanes[0].lower()+i.sSetPanes[1:]
- if len(i.sSetSlope) > 0:
- i.sSetSlope = i.sSetSlope[0].lower()+i.sSetSlope[1:]
- if len(i.sSetUninstallInstall) > 0:
- i.sSetUninstallInstall = i.sSetUninstallInstall[0].lower()+i.sSetUninstallInstall[1:]
- if len(i.sSetDelivery) > 0:
- i.sSetDelivery = i.sSetDelivery[0].lower()+i.sSetDelivery[1:]
- if len(i.sSetOtherConditions) > 0:
- i.sSetOtherConditions = i.sSetOtherConditions[0].lower()+i.sSetOtherConditions[1:]
- to_template.update({
- 'SETS': list_sets,
- # получаем последние визиты клиента через куки
- 'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
- # получаем последние визиты всех посетителей из базы
- # id2log, log_visit = get_last_all_user_visit_list()
+ company_id_int = int(company_id)
+
+ # Получаем компанию или возвращаем 404
+ try:
+ company = MerchantBrand.objects.get(id=company_id_int)
+ except MerchantBrand.DoesNotExist:
+ raise Http404("Компания не найдена")
+
+ # Проверяем что slug совпадает (для SEO и красивых URL)
+ actual_slug = pytils.translit.slugify(company.sMerchantName)
+ if actual_slug != company_name_slug:
+ return redirect(
+ f'/catalog/company/{company_id_int}-{actual_slug}'
+ )
+
+ # Типичные маркеры, которые означают что поле пусто
+ empty_values = ["нет", "—", ""]
+
+ # Получаем все наборы компании с ценовой статистикой
+ sets_list = _get_company_sets_detail(company_id_int)
+
+ # Форматируем каждый набор для вывода в шаблон
+ formatted_sets = [
+ _format_set_for_template(set_data, empty_values)
+ for set_data in sets_list
+ ]
+
+ to_template: dict[str, object] = {
+ 'COMPANY': company.sMerchantName,
+ 'COMPANY_ID': company_id_int,
+ 'COMPANY_T': company_name_slug,
+ 'SETS': formatted_sets,
+ 'HEADER': f'Изготовитель окон «{company.sMerchantName}»',
+ 'META_KEYWORDS': company.sMerchantName,
+ 'IMG_FOR_BLOG': company.pMerchantLogo,
+ 'LIST_NOT': empty_values,
+ '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.perf_counter() - time_start)
- })
+ }
+
+ # Добавляем метрику выполнения представления
+ to_template['ticks'] = float(time.perf_counter() - time_start)
+
return render(request, "catalog/catalog_company_detail.html", to_template)