mod: рефакторинг "каталога компаний" (вьюшки и шаблоны)
This commit is contained in:
@@ -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 шаблонов, гео+статистика серии).
|
* Рефакторинг `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).
|
* Добавлена 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`.
|
* Рефакторинг `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.
|
||||||
*
|
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,81 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}{% load filters %}
|
{% load static %}{% load filters %}
|
||||||
|
|
||||||
{% block Title %}Каталог изготовителей и поставщиков окон{% endblock %}
|
{% block Title %}Каталог оконных компаний: производители и поставщики окон, рейтинг и цены{% endblock %}
|
||||||
|
|
||||||
{% block Add_Body_Attribute %} style="padding-top:70px;"{% 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 CopyrightAuthor4Meta %}: Каталог «Окнардия»{% endblock %}
|
||||||
|
|
||||||
{% block Author4Meta %}: Каталог изготовителей окон{% endblock %}
|
|
||||||
|
|
||||||
{% block CopyrightAuthor4Meta %}: Каталог изготовителей окон{% endblock %}
|
|
||||||
|
|
||||||
{% block Top_Meta1 %}{# <!-- BEGIN Дополнительные Metatags --> #}
|
{% block Top_Meta1 %}{# <!-- BEGIN Дополнительные Metatags --> #}
|
||||||
<meta itemprop="author" content="Каталог «Окнардия»" />{% if IMG_FOR_BLOG %}
|
|
||||||
<meta itemprop="image" content="https://oknardia.ru/media/{{ IMG_FOR_BLOG }}" />{% else %}
|
|
||||||
<meta itemprop="image" content="https://oknardia.ru/static/img/MerDY3gpU0w.jpg" />{% endif %}
|
|
||||||
<meta itemprop="datePublished" content="{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}" />
|
|
||||||
<span itemprop="publisher" itemscope itemtype="http://schema.org/Organization"><meta itemprop="name" content="Каталог «Окнардия»" /></span>
|
|
||||||
<span itemprop="author" itemscope itemtype="http://schema.org/Person"><meta itemprop="name" content="Каталог «Окнардия»" /></span>
|
|
||||||
<meta itemprop="articleSection" content="Каталог производителей окон" />
|
|
||||||
<meta itemprop="headline" content="Компании-партнёры «Окнардии», их рейтинг, число оконных наборов и вариантов расчёта цен для типовых проёмов, средняя цена окна..." />
|
|
||||||
<meta name="news_keywords" content="{{ HEADER }}" />
|
|
||||||
<link rel="canonical" href="https://oknardia.ru/catalog/company/" />
|
<link rel="canonical" href="https://oknardia.ru/catalog/company/" />
|
||||||
<link rel="standout" href="https://oknardia.ru/catalog/company/" />
|
|
||||||
<!-- Разметка для соц-сетей Facebook Open Graph -->
|
<!-- Разметка для соц-сетей Facebook Open Graph -->
|
||||||
<meta property="fb:admins" name="admins" content="100000084781830" />
|
|
||||||
<meta property="fb:pages" content="276108456054163" />
|
|
||||||
<meta property="fb:app_id" content="258354027974262" />
|
|
||||||
<meta property="fb:profile_id" name="profile_id" content="https://www.facebook.com/oknardia/" />
|
|
||||||
<meta property="og:locale" content="ru_RU" />
|
<meta property="og:locale" content="ru_RU" />
|
||||||
<meta property="og:site_name" content="oknardia.ru" />
|
<meta property="og:site_name" content="oknardia.ru" />
|
||||||
<meta property="og:url" content="https://oknardia.ru//catalog/company/" />
|
<meta property="og:url" content="https://oknardia.ru/catalog/company/" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:title" content="Каталог производителей окон | oknardia.ru" />
|
<meta property="og:title" content="Каталог оконных компаний: производители и поставщики окон, рейтинг и цены | oknardia.ru" />
|
||||||
<meta property="og:description" content="Компании-партнеры «Окнардии», их средний рейтинг, число оконных наборов и вариантов расчета цен для типовых проёмов, средняя цена окна..." />
|
<meta property="og:description" content="Актуальный каталог оконных компаний России. Сравните производителей и поставщиков пластиковых окон по рейтингу, ассортименту, средней цене и дате последнего обновления." />
|
||||||
<meta property="og:image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<meta property="og:image" content="https://oknardia.ru/static/img/MerDY3gpU0w.jpg" />
|
||||||
<link rel="image_src" href="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<link rel="image_src" href="https://oknardia.ru/static/img/MerDY3gpU0w.jpg" />
|
||||||
<!-- Разметка для соц-сетей Twitter Card -->
|
<!-- Разметка для соц-сетей Twitter Card -->
|
||||||
<meta name="twitter:title" content="Каталог производителей окон | oknardia.ru" />
|
<meta name="twitter:title" content="Каталог оконных компаний: производители и поставщики окон, рейтинг и цены | oknardia.ru" />
|
||||||
<meta name="twitter:description" content="Компании-партнеры «Окнардии», их средний рейтинг, число оконных наборов и вариантов расчета цен для типовых проёмов, средняя цена окна..." />
|
<meta name="twitter:description" content="Актуальный каталог оконных компаний России. Сравните производителей и поставщиков пластиковых окон по рейтингу, ассортименту, средней цене и дате последнего обновления." />
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:site" content="@oknardia" />
|
<meta name="twitter:site" content="@oknardia" />
|
||||||
<meta name="twitter:domain" content="oknardia.ru" />
|
{# Удалить: <meta name="twitter:domain"> — устаревший тег #}
|
||||||
<meta property="twitter:url" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<meta name="twitter:image" content="https://oknardia.ru/static/img/MerDY3gpU0w.jpg" />
|
||||||
<meta name="relap-image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}">
|
<meta name="relap-image" content="https://oknardia.ru/static/img/MerDY3gpU0w.jpg">
|
||||||
{# <!-- END Дополнительные Metatags --> #}{% endblock %}
|
{# <!-- END Дополнительные Metatags --> #}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block ADD_TO_HEAD %}{% comment %}
|
||||||
|
JSON-LD для страницы-списка компаний: CollectionPage + ItemList с элементами Organization.
|
||||||
|
Это понятнее для поисковиков, чем legacy microdata на метатегах.
|
||||||
|
{% endcomment %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "CollectionPage",
|
||||||
|
"name": "Каталог оконных компаний: производители и поставщики окон",
|
||||||
|
"description": "Актуальный каталог оконных компаний России с рейтингами, средней ценой и составом наборов.",
|
||||||
|
"url": "https://oknardia.ru/catalog/company/",
|
||||||
|
"inLanguage": "ru-RU",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Окнардия",
|
||||||
|
"url": "https://oknardia.ru"
|
||||||
|
},
|
||||||
|
"mainEntity": {
|
||||||
|
"@type": "ItemList",
|
||||||
|
"name": "Производители и поставщики окон",
|
||||||
|
"numberOfItems": {{ COMPANIES|length }},
|
||||||
|
"itemListElement": [
|
||||||
|
{% for i in COMPANIES %}
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": {{ forloop.counter }},
|
||||||
|
"item": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "{{ i.sMerchantName|escapejs }}",
|
||||||
|
"url": "https://oknardia.ru/catalog/company/{{ i.id }}-{{ i.sMerchantMainURL }}",
|
||||||
|
"logo": "https://oknardia.ru/media/{{ i.pMerchantLogo }}"
|
||||||
|
}
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block Main_Content %}
|
{% block Main_Content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{# <!--- Хлебные крошки: НАЧАЛО --> #}<div class="row">
|
{# <!--- Хлебные крошки: НАЧАЛО --> #}<div class="row">
|
||||||
@@ -90,7 +113,3 @@
|
|||||||
{% include "report/report_log_user_visit.html" %}
|
{% include "report/report_log_user_visit.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>{% endblock %}
|
</div>{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,30 +5,20 @@
|
|||||||
|
|
||||||
{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %}
|
{% 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 %}{# <!-- BEGIN Дополнительные Metatags --> #}
|
{% block Top_Meta1 %}{# <!-- BEGIN Дополнительные Metatags --> #}
|
||||||
<meta itemprop="author" content="Каталог «Окнардия»" />{% if IMG_FOR_BLOG %}
|
{# Microdata (itemprop) убрана — заменена на JSON-LD в блоке ADD_TO_HEAD ниже (чище, надёжнее) #}
|
||||||
<meta itemprop="image" content="https://oknardia.ru/media/{{ IMG_FOR_BLOG }}" />{% else %}
|
|
||||||
<meta itemprop="image" content="https://oknardia.ru/static/img/MerDY3gpU0w.jpg" />{% endif %}
|
|
||||||
<meta itemprop="datePublished" content="{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}" />
|
|
||||||
<span itemprop="publisher" itemscope itemtype="http://schema.org/Organization"><meta itemprop="name" content="Каталог «Окнардия»" /></span>
|
|
||||||
<span itemprop="author" itemscope itemtype="http://schema.org/Person"><meta itemprop="name" content="Каталог «Окнардия»" /></span>
|
|
||||||
<meta itemprop="articleSection" content="Каталог производителей окон" />
|
|
||||||
<meta itemprop="headline" content="Изготовитель окон «{{ COMPANY }}», описание, производимые им оконные наборы и их состав, характеристики, рейтинг, средние цены и отклонение цен." />
|
|
||||||
<meta name="news_keywords" content="{{ HEADER|striptags }}" />
|
<meta name="news_keywords" content="{{ HEADER|striptags }}" />
|
||||||
<link rel="canonical" href="https://oknardia.ru/catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}" />
|
<link rel="canonical" href="https://oknardia.ru/catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}" />
|
||||||
<link rel="standout" href="https://oknardia.ru/catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}" />
|
{# Удалить: <link rel="standout"> — тег Google News 2011 г., отменён в 2014, поисковики игнорируют #}
|
||||||
<!-- Разметка для соц-сетей Facebook Open Graph -->
|
<!-- Разметка для соц-сетей Facebook Open Graph -->
|
||||||
<meta property="fb:admins" name="admins" content="100000084781830" />
|
<meta property="fb:admins" name="admins" content="100000084781830" />
|
||||||
<meta property="fb:pages" content="276108456054163" />
|
<meta property="fb:pages" content="276108456054163" />
|
||||||
@@ -36,24 +26,59 @@
|
|||||||
<meta property="fb:profile_id" name="profile_id" content="https://www.facebook.com/oknardia/" />
|
<meta property="fb:profile_id" name="profile_id" content="https://www.facebook.com/oknardia/" />
|
||||||
<meta property="og:locale" content="ru_RU" />
|
<meta property="og:locale" content="ru_RU" />
|
||||||
<meta property="og:site_name" content="oknardia.ru" />
|
<meta property="og:site_name" content="oknardia.ru" />
|
||||||
<meta property="og:url" content="https://oknardia.ru//catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}" />
|
<meta property="og:url" content="https://oknardia.ru/catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:title" content="Окна «{{ COMPANY }}» | oknardia.ru" />
|
<meta property="og:title" content="Окна «{{ COMPANY }}» | oknardia.ru" />
|
||||||
<meta property="og:description" content="Окна «{{ COMPANY }}», описание окон «{{ COMPANY }}», производимые им оконные наборы и их состав, характеристики, рейтинг, средние цены и отклонение цен." />
|
<meta property="og:description" content="«{{ COMPANY }}» — оконные наборы, состав и характеристики, независимый рейтинг качества, средние цены на установку. Агрегатор Окнардия." />
|
||||||
<meta property="og:image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<meta property="og:image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
||||||
<link rel="image_src" href="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<link rel="image_src" href="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
||||||
<!-- Разметка для соц-сетей Twitter Card -->
|
<!-- Разметка для соц-сетей Twitter Card -->
|
||||||
<meta name="twitter:title" content="Производителей окон «{{ COMPANY }}» | oknardia.ru" />
|
<meta name="twitter:title" content="Производитель окон «{{ COMPANY }}» | oknardia.ru" />
|
||||||
<meta name="twitter:description" content="Изготовитель окон «{{ COMPANY }}», описание, производимые им оконные наборы и их состав, характеристики, рейтинг, средние цены и отклонение цен." />
|
<meta name="twitter:description" content="«{{ COMPANY }}» в каталоге Окнардии: наборы, характеристики, рейтинг и цены на установку окон в типовых домах." />
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:site" content="@oknardia" />
|
<meta name="twitter:site" content="@oknardia" />
|
||||||
<meta name="twitter:domain" content="oknardia.ru" />
|
{# Удалить: <meta name="twitter:domain"> — устарело с 2015, Twitter его не использует #}
|
||||||
<meta property="twitter:url" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
<meta property="twitter:url" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}" />
|
||||||
<meta name="relap-image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}">
|
<meta name="relap-image" content="{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{% endif %}{{ IMG_FOR_BLOG|default:'https://oknardia.ru/static/img/MerDY3gpU0w.jpg' }}">
|
||||||
{# <!-- END Дополнительные Metatags --> #}{% endblock %}
|
{# <!-- END Дополнительные Metatags --> #}{% endblock %}
|
||||||
|
|
||||||
|
{% block Top_JS5 %}
|
||||||
|
<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>{% endblock %}
|
||||||
|
|
||||||
{% block Top_JS5 %}<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>{% endblock %}
|
{% block ADD_TO_HEAD %}{% comment %}
|
||||||
|
JSON-LD разметка Schema.org для страницы производителя окон.
|
||||||
|
Тип LocalBusiness описывает компанию-поставщика окон: название, контакты, адрес, геокоординаты,
|
||||||
|
логотип и ссылку на официальный сайт производителя.
|
||||||
|
Данные берутся из первого набора в SETS (все наборы принадлежат одному офису/бренду),
|
||||||
|
поэтому достаточно SETS.0 для контактной информации.
|
||||||
|
Документация: https://schema.org/LocalBusiness #}{% endcomment %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "LocalBusiness",
|
||||||
|
"name": "{{ COMPANY|escapejs }}",
|
||||||
|
"description": "Производитель окон «{{ COMPANY|escapejs }}»: оконные наборы, характеристики профилей и стеклопакетов, цены на установку в типовых домах.",
|
||||||
|
"url": "https://oknardia.ru/catalog/company/{{ COMPANY_ID }}-{{ COMPANY_T }}",
|
||||||
|
"image": "{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{{ IMG_FOR_BLOG }}{% else %}https://oknardia.ru/static/img/MerDY3gpU0w.jpg{% endif %}",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "{% if IMG_FOR_BLOG %}https://oknardia.ru/media/{{ IMG_FOR_BLOG }}{% else %}https://oknardia.ru/static/img/MerDY3gpU0w.jpg{% endif %}"
|
||||||
|
}{% if SETS %},
|
||||||
|
"telephone": "{{ SETS.0.sOfficePhones|striptags|escapejs }}",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "{{ SETS.0.sOfficeAddress|escapejs }}",
|
||||||
|
"addressCountry": "RU"
|
||||||
|
},
|
||||||
|
"geo": {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
"latitude": {{ SETS.0.fOfficeGeoCode_Latitude|stringformat:".7f" }},
|
||||||
|
"longitude": {{ SETS.0.fOfficeGeoCode_Longitude|stringformat:".7f" }}
|
||||||
|
},
|
||||||
|
"sameAs": "{{ SETS.0.sMerchantMainURL.URL|escapejs }}"{% endif %}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block Main_Content %}
|
{% block Main_Content %}
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Каталог производителей и компаний.
|
||||||
|
|
||||||
|
Модуль предоставляет views для отображения:
|
||||||
|
1. Списка всех производителей с их ключевыми показателями (рейтинг, количество
|
||||||
|
предложений, среднюю цену и т.п.)
|
||||||
|
2. Детальную информацию о конкретном производителе со всеми его оконными наборами
|
||||||
|
|
||||||
|
Все запросы переведены на Django ORM для лучшей производительности и чистоты кода.
|
||||||
|
"""
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse, Http404
|
||||||
from django.utils import timezone
|
from django.db.models import Count, Avg, Max, Min, DecimalField
|
||||||
from oknardia.models import (
|
from oknardia.models import (
|
||||||
MerchantBrand,
|
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
|
from web.add_func import get_rating_set_for_stars
|
||||||
import django.utils.dateformat
|
import django.utils.dateformat
|
||||||
import time
|
import time
|
||||||
@@ -14,148 +30,500 @@ import re
|
|||||||
import pytils
|
import pytils
|
||||||
|
|
||||||
|
|
||||||
def catalog_company(request: HttpRequest) -> HttpResponse:
|
def _get_company_statistics() -> list:
|
||||||
time_start = time.perf_counter()
|
"""
|
||||||
to_template: dict[str, object] = {} # словарь, для передачи шаблону
|
Получает список компаний (MerchantBrand) с агрегированной статистикой.
|
||||||
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 '
|
Returns:
|
||||||
'FROM (SELECT'
|
list: Список словарей с данными компаний
|
||||||
' COUNT(oknardia_setkit.sSetName) AS NumSets,'
|
"""
|
||||||
' oknardia_merchantoffice.kMerchantName_id AS Q_ID,'
|
# 1. Статистика по наборам (SetKit) для каждой компании
|
||||||
' AVG(oknardia_setkit.fSetRating) AS RatingAVG'
|
set_stats = (
|
||||||
' FROM oknardia_merchantoffice'
|
SetKit.objects
|
||||||
' INNER JOIN oknardia_ouruser'
|
.filter(kSet2User__kMerchantOffice__kMerchantName__isnull=False)
|
||||||
' ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id'
|
.values('kSet2User__kMerchantOffice__kMerchantName_id')
|
||||||
' INNER JOIN oknardia_setkit'
|
.annotate(
|
||||||
' ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id'
|
num_sets=Count('id', distinct=True),
|
||||||
' GROUP BY oknardia_merchantoffice.id,'
|
avg_rating=Avg('fSetRating')
|
||||||
' oknardia_merchantoffice.kMerchantName_id) AS Q,'
|
)
|
||||||
' oknardia_ouruser'
|
)
|
||||||
' INNER JOIN oknardia_merchantoffice'
|
set_stats_dict = {
|
||||||
' ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id'
|
stat['kSet2User__kMerchantOffice__kMerchantName_id']: {
|
||||||
' INNER JOIN oknardia_priceoffer'
|
'num_sets': stat['num_sets'],
|
||||||
' ON oknardia_priceoffer.kOfferFromUser_id = oknardia_ouruser.id'
|
'avg_rating': stat['avg_rating'] or 0
|
||||||
' INNER JOIN oknardia_merchantbrand'
|
}
|
||||||
' ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id'
|
for stat in set_stats
|
||||||
' WHERE Q_ID = oknardia_merchantoffice.kMerchantName_id '
|
}
|
||||||
'GROUP BY oknardia_merchantoffice.kMerchantName_id '
|
|
||||||
'ORDER BY Q.RatingAVG DESC;')
|
# 2. Статистика по ценовым предложениям (PriceOffer)
|
||||||
list_company = list(q_company)
|
companies_data = (
|
||||||
for i in list_company:
|
PriceOffer.objects
|
||||||
i.STARS = get_rating_set_for_stars(i.RatingAVG)
|
.filter(
|
||||||
i.NumSets = pytils.numeral.get_plural(i.NumSets, u"оконный набор, оконных набора, оконных наборов")
|
sOfferActive=True,
|
||||||
i.NumOffers = pytils.numeral.get_plural(i.NumOffers, u"вариант, варианта, вариантов")
|
kOfferFromUser__kMerchantOffice__kMerchantName__isnull=False
|
||||||
i.lastUpdate = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.lastUpdate, 'U')))
|
)
|
||||||
i.sMerchantMainURL = pytils.translit.slugify(i.sMerchantName)
|
.values('kOfferFromUser__kMerchantOffice__kMerchantName_id')
|
||||||
# print("NAME:", i.sMerchantName, "\tNumSets:", i.NumSets, "\tNumOffers:", i.NumOffers,
|
.annotate(
|
||||||
# "\t:AverageRating:", i.RatingAVG, "\tAveragePrice:", i.PriceAVG, "\tSTARS:", i.STARS)
|
num_offers=Count('id', distinct=True),
|
||||||
to_template.update({
|
price_avg=Avg('fOfferPrice', output_field=DecimalField()),
|
||||||
'COMPANIES': list_company,
|
last_update=Max('dOfferModify')
|
||||||
# получаем последние визиты клиента через куки
|
)
|
||||||
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
|
.order_by('-last_update')
|
||||||
# получаем последние визиты всех посетителей из базы
|
)
|
||||||
# id2log, log_visit = get_last_all_user_visit_list()
|
|
||||||
'LOG_VISIT': get_last_all_user_visit_list(),
|
# 3. Получаем все объекты MerchantBrand одним запросом (решение проблемы N+1)
|
||||||
'ticks': float(time.perf_counter() - time_start)
|
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:
|
||||||
|
"""
|
||||||
|
Показывает список всех производителей с ключевыми показателями.
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, "catalog/catalog_company.html", to_template)
|
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:
|
||||||
time_start = time.perf_counter()
|
"""
|
||||||
to_template: dict[str, object] = {} # словарь, для передачи шаблону
|
Преобразует первый символ строки в нижний регистр.
|
||||||
company_id = int(company_id)
|
|
||||||
q_by_id = MerchantBrand.objects.get(id=company_id)
|
Args:
|
||||||
if pytils.translit.slugify(q_by_id.sMerchantName) != company_name_slug:
|
text (str): Исходная строка
|
||||||
return redirect('/catalog/company/%d-%s' % (company_id, pytils.translit.slugify(q_by_id.sMerchantName)))
|
|
||||||
to_template.update({'COMPANY': q_by_id.sMerchantName})
|
Returns:
|
||||||
to_template.update({'COMPANY_ID': company_id})
|
str: Строка с строчным первым символом (если длина > 0)
|
||||||
to_template.update({'COMPANY_T': company_name_slug})
|
"""
|
||||||
list_not = [u"нет", u"—", ""]
|
return text[0].lower() + text[1:] if len(text) > 0 else text
|
||||||
to_template.update({'LIST_NOT': list_not})
|
|
||||||
q_sets = MerchantBrand.objects.raw(f"SELECT"
|
|
||||||
f" COUNT(oknardia_priceoffer.id) AS NumOffers,"
|
def _clean_text_field(text: str, empty_values: list) -> str:
|
||||||
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.*,"
|
Args:
|
||||||
f" oknardia_merchantoffice.id AS idMERCH,"
|
text (str): Исходный текст
|
||||||
f" oknardia_setkit.*,"
|
empty_values (list): Список значений, которые считаются "пустыми"
|
||||||
f" oknardia_setkit.id AS idSET,"
|
|
||||||
f" oknardia_pvcprofiles.*,"
|
Returns:
|
||||||
f" oknardia_pvcprofiles.id AS idPVC,"
|
str: Очищенный текст или пустая строка если значение в empty_values
|
||||||
f" oknardia_glazing.*, "
|
"""
|
||||||
f" oknardia_glazing.id AS idGLAZ "
|
if text.lower() in empty_values:
|
||||||
f"FROM oknardia_ouruser"
|
return ""
|
||||||
f" INNER JOIN oknardia_merchantoffice"
|
return _lowercase_first_char(text)
|
||||||
f" ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id"
|
|
||||||
f" INNER JOIN oknardia_merchantbrand"
|
|
||||||
f" ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id"
|
def _get_company_sets_detail(company_id: int) -> list:
|
||||||
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"
|
Использует оптимизированные select_related и prefetch_related для минимизации
|
||||||
f" INNER JOIN oknardia_pvcprofiles"
|
запросов к БД. Группирует данные по наборам (SetKit) с уникальностью.
|
||||||
f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id"
|
|
||||||
f" INNER JOIN oknardia_glazing"
|
Args:
|
||||||
f" ON oknardia_setkit.kSet2Glazing_id = oknardia_glazing.id "
|
company_id (int): ID компании (MerchantBrand)
|
||||||
f"WHERE oknardia_merchantbrand.id = {company_id} "
|
|
||||||
f"AND oknardia_priceoffer.sOfferActive = TRUE "
|
Returns:
|
||||||
f"GROUP BY oknardia_merchantoffice.id,"
|
list: Список словарей с данными наборов, отсортированные по рейтингу
|
||||||
f" oknardia_setkit.id,"
|
"""
|
||||||
f" oknardia_setkit.fSetRating "
|
# Получаем активные ценовые предложения для компаний с агрегацией по наборам
|
||||||
f"ORDER BY oknardia_setkit.fSetRating DESC;")
|
price_stats = (
|
||||||
list_sets = list(q_sets)
|
PriceOffer.objects
|
||||||
for i in list_sets:
|
.filter(
|
||||||
i.sMerchantMainURL = {"URL": i.sMerchantMainURL,
|
sOfferActive=True,
|
||||||
"URL_VIEW": re.sub(r"(?:^http://|^https://|/$|www\.)", "", i.sMerchantMainURL)}
|
kOfferFromUser__kMerchantOffice__kMerchantName_id=company_id
|
||||||
k = random.randint(1, int(len(i.sOfficeEmails)/2) - 1)
|
)
|
||||||
i.sOfficeEmails = [i.sOfficeEmails[0:k], i.sOfficeEmails[k:-k], i.sOfficeEmails[-k:]]
|
.values('kOffer2SetKit_id')
|
||||||
to_template.update({'IMG_FOR_BLOG': i.pMerchantLogo})
|
.annotate(
|
||||||
i.fSetRating = {"RATING": i.fSetRating,
|
num_offers=Count('id'),
|
||||||
"STARS": get_rating_set_for_stars(i.fSetRating)}
|
price_avg=Avg('fOfferPrice', output_field=DecimalField()),
|
||||||
i.lastUpdate = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.lastUpdate, 'U')))
|
last_update=Max('dOfferModify'),
|
||||||
i.earlyCreation = pytils.dt.distance_of_time_in_words(int(django.utils.dateformat.format(i.earlyCreation, 'U')))
|
early_creation=Min('dOfferCreate')
|
||||||
i.sProfileName = {"NAME": i.sProfileName,
|
)
|
||||||
"NAME_T": pytils.translit.slugify(i.sProfileName)}
|
)
|
||||||
i.sProfileManufacturer = {"NAME": i.sProfileManufacturer,
|
|
||||||
"NAME_T": pytils.translit.slugify(i.sProfileManufacturer)}
|
# Преобразуем в словарь для быстрого доступа по ID набора
|
||||||
i.fProfileSeals = pytils.numeral.sum_string(i.fProfileSeals, pytils.numeral.MALE, u"контур, контура, контуров")
|
price_stats_dict = {
|
||||||
if i.sSetImplementCatch.lower() in list_not:
|
stat['kOffer2SetKit_id']: {
|
||||||
i.sSetImplementCatch = ""
|
'num_offers': stat['num_offers'],
|
||||||
if i.sSetClimateControl.lower() in list_not:
|
'price_avg': stat['price_avg'],
|
||||||
i.sSetClimateControl = ""
|
'last_update': stat['last_update'],
|
||||||
if len(i.sProfileReinforcement) > 0:
|
'early_creation': stat['early_creation']
|
||||||
i.sProfileReinforcement = i.sProfileReinforcement[0].lower()+i.sProfileReinforcement[1:]
|
}
|
||||||
if len(i.sSetSill) > 0:
|
for stat in price_stats
|
||||||
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:
|
# select_related оптимизирует ForeignKey запросы (профиль, стеклопакет)
|
||||||
i.sSetSlope = i.sSetSlope[0].lower()+i.sSetSlope[1:]
|
sets_queryset = (
|
||||||
if len(i.sSetUninstallInstall) > 0:
|
SetKit.objects
|
||||||
i.sSetUninstallInstall = i.sSetUninstallInstall[0].lower()+i.sSetUninstallInstall[1:]
|
.filter(
|
||||||
if len(i.sSetDelivery) > 0:
|
kSet2User__kMerchantOffice__kMerchantName_id=company_id
|
||||||
i.sSetDelivery = i.sSetDelivery[0].lower()+i.sSetDelivery[1:]
|
)
|
||||||
if len(i.sSetOtherConditions) > 0:
|
.select_related(
|
||||||
i.sSetOtherConditions = i.sSetOtherConditions[0].lower()+i.sSetOtherConditions[1:]
|
'kSet2User',
|
||||||
to_template.update({
|
'kSet2User__kMerchantOffice',
|
||||||
'SETS': list_sets,
|
'kSet2User__kMerchantOffice__kMerchantName',
|
||||||
# получаем последние визиты клиента через куки
|
'kSet2PVCprofiles',
|
||||||
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
|
'kSet2Glazing'
|
||||||
# получаем последние визиты всех посетителей из базы
|
)
|
||||||
# id2log, log_visit = get_last_all_user_visit_list()
|
.order_by('-fSetRating')
|
||||||
'LOG_VISIT': get_last_all_user_visit_list(),
|
)
|
||||||
'ticks': float(time.perf_counter() - time_start)
|
|
||||||
|
# Собираем результат, комбинируя данные 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()
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем метрику выполнения представления
|
||||||
|
to_template['ticks'] = float(time.perf_counter() - time_start)
|
||||||
|
|
||||||
return render(request, "catalog/catalog_company_detail.html", to_template)
|
return render(request, "catalog/catalog_company_detail.html", to_template)
|
||||||
|
|||||||
Reference in New Issue
Block a user