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

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

View File

@@ -205,8 +205,12 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CAPTCHA_PUBLIC_KEY = env('CAPTCHA_PUBLIC_KEY', default='') CAPTCHA_PUBLIC_KEY = env('CAPTCHA_PUBLIC_KEY', default='')
CAPTCHA_PRIVATE_KEY = env('CAPTCHA_PRIVATE_KEY', default='') CAPTCHA_PRIVATE_KEY = env('CAPTCHA_PRIVATE_KEY', default='')
# МАГИЧЕСКИЕ ЧИСЛА
# если непонятно какая серия выбрана через каталог (finger fix) выбираем серию типового строения: # если непонятно какая серия выбрана через каталог (finger fix) выбираем серию типового строения:
DEFAULT_SERIA_ID_FOR_CATALOG = 843 # СЕРИЯ 1-515/9 -- дом в котором я живу DEFAULT_SERIA_ID_FOR_CATALOG = 843 # СЕРИЯ 1-515/9 -- дом в котором я живу
DEFAULT_WIN_WIDTH_MM = 670 # Ширина типового окна для ID=16 (если не выбрано)
DEFAULT_WIN_HEIGHT_MM = 2160 # Высота типового окна для ID=16 (если не выбрано)
DEFAULT_WIN_ID = 16 # ID типового окна (если не выбрано)
# количество коммерческих предложений во фрейме отчета # количество коммерческих предложений во фрейме отчета
OFFER_PER_FRAME = 5 OFFER_PER_FRAME = 5

View File

@@ -62,9 +62,9 @@ urlpatterns = [
re_path(r'^catalog/profile[/*]$', catalog_profiles.catalog_profile), # СПИСОК ВСЕХ ПРОФИЛЕЙ И ПРОИЗВОДИТЕЛЕЙ re_path(r'^catalog/profile[/*]$', catalog_profiles.catalog_profile), # СПИСОК ВСЕХ ПРОФИЛЕЙ И ПРОИЗВОДИТЕЛЕЙ
re_path(r'^catalog/profile/(?P<manufacture_id>\d+)-(?P<manufacture_name>\S*)' re_path(r'^catalog/profile/(?P<manufacture_id>\d+)-(?P<manufacture_name>\S*)'
r'/(?P<model_id>\d+)-(?P<model_name>\S*)[/*]$', r'/(?P<model_id>\d+)-(?P<model_name>\S*)[/*]$',
catalog_profiles.catalog_profile_model), # КАРТОЧКА ПРОФИЛЯ (ИЛИ ПРОИЗВОДИТЕЛЯ) catalog_profiles.catalog_profile_model), # СТРАНИЦА ОПИСАНИЯ МОДЕЛИ ПРОФИЛЯ
re_path(r'^catalog/profile/(?P<manufacture_id>\d+)-(?P<manufacture_name>\S*)[/*]$', re_path(r'^catalog/profile/(?P<manufacture_id>\d+)-(?P<manufacture_name>\S*)[/*]$',
catalog_profiles.catalog_profile_manufacture), catalog_profiles.catalog_profile_manufacture), # КАРТОЧКА ОПИСАНИЯ ПРОИЗВОДИТЕЛЯ ПРОФИЛЯ
# --- --- КАТАЛОГ СЕРИЙ ТИПОВОГО СТРОИТЕЛЬСТВА # --- --- КАТАЛОГ СЕРИЙ ТИПОВОГО СТРОИТЕЛЬСТВА
re_path(r'^catalog/seria[/*]$', catalog_series.catalog_seria), # СПИСОК ВСЕХ СЕРИЙ ЗДАНИЙ re_path(r'^catalog/seria[/*]$', catalog_series.catalog_seria), # СПИСОК ВСЕХ СЕРИЙ ЗДАНИЙ
re_path(r'^catalog/seria/(?P<seria_name_translit>[^/]*)/all(?P<seria_id>\d+)[/*]$', re_path(r'^catalog/seria/(?P<seria_name_translit>[^/]*)/all(?P<seria_id>\d+)[/*]$',
@@ -75,12 +75,15 @@ urlpatterns = [
# --- --- КАТАЛОГ ПРОИЗВОДИТЕЛЕЙ ОКОН # --- --- КАТАЛОГ ПРОИЗВОДИТЕЛЕЙ ОКОН
re_path(r'^catalog/company[/*]$', catalog_companies.catalog_company), # СПИСОК ВСЕХ ПРОИЗВОДИТЕЛЕЙ ОКОН re_path(r'^catalog/company[/*]$', catalog_companies.catalog_company), # СПИСОК ВСЕХ ПРОИЗВОДИТЕЛЕЙ ОКОН
re_path(r'^catalog/company/(?P<company_id>\d+)-(?P<company_name_slug>\S*)[/*]$', re_path(r'^catalog/company/(?P<company_id>\d+)-(?P<company_name_slug>\S*)[/*]$',
catalog_companies.catalog_company_detail), # КАРТОЧКА ПРОИЗВОДИТЕЛЯ ОКОН catalog_companies.catalog_company_detail), # КАРТОЧКА ПРОИЗВОДИТЕЛЯ-УСТНОАЩИКА ОКОН
# ЦЕНОВЫЕ ПРЕДЛОЖЕНИЯ # ЦЕНОВЫЕ ПРЕДЛОЖЕНИЯ
# --- Одиночное окно # --- ОДИНОЧНОЕ ОКНО
re_path(r'^catalog/standard_opening/price-(?P<win_width_mm>\d+)x(?P<win_height_mm>\d+)mm-tip(?P<win_id>\d+)[/*]$',
prices.report_one_win_price), # КАНОНИЧЕСКИЙ SEO-URL СТРАНИЦЫ ЦЕН ДЛЯ ОДНОГО ПРОЕМА
re_path(r'^tsena-odnogo-okna/(?P<win_width_mm>\d+)x(?P<win_height_mm>\d+)mm/tip(?P<win_id>\d+)[/*]$', re_path(r'^tsena-odnogo-okna/(?P<win_width_mm>\d+)x(?P<win_height_mm>\d+)mm/tip(?P<win_id>\d+)[/*]$',
prices.report_one_win_price), prices.redirect_one_win_price_legacy), # LEGACY-URL: 301 -> КАНОНИЧЕСКИЙ ПУТЬ
re_path(r'^next_price_one_flap_frame/idW(?P<win_id>\d+)N(?P<frame_begin_n>\d+)\S*$', prices.next_one_win_price), re_path(r'^next_price_one_flap_frame/idW(?P<win_id>\d+)N(?P<frame_begin_n>\d+)\S*$',
prices.next_one_win_price), # ПОДГРУЖАЕМЫЙ ФРЕЙМ С ЦЕНОВЫМИ ПРЕДЛОЖЕНИЯМИ ДЛЯ ОДНОГО ПРОЕМА
# --- Ценовая выдача # --- Ценовая выдача
re_path(r'^(?P<build_id>\d+)/(?P<apart_id>\d+)/(?P<slug>[\s\S]*)$', prices.report_price), re_path(r'^(?P<build_id>\d+)/(?P<apart_id>\d+)/(?P<slug>[\s\S]*)$', prices.report_price),
# --- Подгружаемый фрейм ценовая выдачи # --- Подгружаемый фрейм ценовая выдачи

View File

@@ -70,7 +70,7 @@ CollectionPage + ItemList помогают поисковику понять с
"@type": "Thing", "@type": "Thing",
"name": "{{ i.DESCRIPTION|escapejs }}", "name": "{{ i.DESCRIPTION|escapejs }}",
"description": "{{ i.DESCRIPTION_L|escapejs }}", "description": "{{ i.DESCRIPTION_L|escapejs }}",
"url": "{{ request.scheme }}://{{ request.get_host }}/tsena-odnogo-okna/{{ i.W|stringformat:'.0f' }}x{{ i.H|stringformat:'.0f' }}mm/tip{{ i.ID }}", "url": "{{ request.scheme }}://{{ request.get_host }}/catalog/standard_opening/price-{{ i.W|stringformat:'.0f' }}x{{ i.H|stringformat:'.0f' }}mm-tip{{ i.ID }}",
"image": "{{ request.scheme }}://{{ request.get_host }}{% static i.URL2IMG %}", "image": "{{ request.scheme }}://{{ request.get_host }}{% static i.URL2IMG %}",
"additionalProperty": [ "additionalProperty": [
{"@type": "PropertyValue", "name": "Ширина", "value": "{{ i.W|stringformat:'.0f' }} мм"}, {"@type": "PropertyValue", "name": "Ширина", "value": "{{ i.W|stringformat:'.0f' }} мм"},
@@ -133,7 +133,7 @@ CollectionPage + ItemList помогают поисковику понять с
<td data-sort="{% if i.IS_DOOR %}1{% else %}0{% endif %}">{% if i.IS_DOOR %}да{% else %}—{% endif %}</td> <td data-sort="{% if i.IS_DOOR %}1{% else %}0{% endif %}">{% if i.IS_DOOR %}да{% else %}—{% endif %}</td>
<td>{{ i.DESCRIPTION }}</td> <td>{{ i.DESCRIPTION }}</td>
<td>{% for j in i.INCLUDING_IN_SERIA %}<a href="/catalog/seria/{{ j.NAME_T }}/all{{ j.ID }}">{{ j.NAME }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td> <td>{% for j in i.INCLUDING_IN_SERIA %}<a href="/catalog/seria/{{ j.NAME_T }}/all{{ j.ID }}">{{ j.NAME }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
<td><a class="btn btn-default btn-xs" href="/tsena-odnogo-okna/{{ i.W|stringformat:".0f" }}x{{ i.H|stringformat:".0f" }}mm/tip{{ i.ID }}">цены</a></td> <td><a class="btn btn-default btn-xs" href="/catalog/standard_opening/price-{{ i.W|stringformat:".0f" }}x{{ i.H|stringformat:".0f" }}mm-tip{{ i.ID }}">цены</a></td>
</tr>{% endfor %} </tr>{% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -5,11 +5,12 @@
{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %} {% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %}
{# SEO блоки дат:#}
{# - Date4Meta: дата публикации (первого появления) — используем дату модификации данных. #}
{# - Last4Meta: дата последнего обновления — будет по умолчанию now из base.html. #}
{% block Date4Meta %}{{ META_DATA_PUBLISH|date:"Y-m-d" }}{% endblock %} {% block Date4Meta %}{{ META_DATA_PUBLISH|date:"Y-m-d" }}{% endblock %}
{% block Last4Meta %}{{ META_DATA_PUBLISH|date:"Y-m-d" }}{% endblock %} {% block Top_JS4 %}{# Для построения круговой диаграммы #}
{% block Top_JS4 %}
<script type="text/javascript" src="//www.gstatic.com/charts/loader.js"></script> <script type="text/javascript" src="//www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript"> <script type="text/javascript">
google.charts.load("current", {packages: ["corechart"]}); google.charts.load("current", {packages: ["corechart"]});
@@ -35,6 +36,140 @@
} }
</script>{% endblock %} </script>{% endblock %}
{% block ADD_TO_HEAD %}{% comment %}
JSON-LD микроразметка для поисковых систем (Schema.org):
- BreadcrumbList: хлебные крошки для навигации в поиске
- Organization: информация о бренде/компании
- Product: типовое окно с полной информацией
- Рейтинги и цены берутся из таблицы предложений (price_offers_for_one_window_frame.html)
{% endcomment %}<script type="application/ld+json">
[
{
"@context": "https://schema.org/",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Главная",
"item": "{{ request.scheme }}://{{ request.get_host }}/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Каталог",
"item": "{{ request.scheme }}://{{ request.get_host }}/catalog"
},
{
"@type": "ListItem",
"position": 3,
"name": "Оконные проёмы и балконные блоки",
"item": "{{ request.scheme }}://{{ request.get_host }}/catalog/standard_opening/"
},
{
"@type": "ListItem",
"position": 4,
"name": "Окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth_mm|floatformat:0 }}×{{ I_WIN_DIM.iWinHight_mm|floatformat:0 }}{% endfor %} мм",
"item": "{{ request.scheme }}://{{ request.get_host }}{{ request.path }}"
}
]
},
{
"@context": "https://schema.org/",
"@type": "Organization",
"name": "ОКНАРДИЯ — агрегатор цен на окна",
"url": "{{ request.scheme }}://{{ request.get_host }}/",
"logo": "{{ request.scheme }}://{{ request.get_host }}{% static 'img/oknardia_logo.svg' %}",
"description": "Сравнение цен на установку оконных конструкций в типовых жилых домах России",
"contactPoint": {
"@type": "ContactPoint",
"contactType": "Customer Service"
}
},
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Типовое пластиковое окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth|floatformat:0 }}×{{ I_WIN_DIM.iWinHight|floatformat:0 }} см{% endfor %}",
"size": "{% for I_WIN_DIM in FLAP_DIM %}{% if not forloop.first %}, {% endif %}{{ I_WIN_DIM.iWinWidth_mm|floatformat:0 }}×{{ I_WIN_DIM.iWinHight_mm|floatformat:0 }} мм{% endfor %}",
"description": "Цены на пластиковое окно стандартного размера для типовых жилых домов серий {% for I in SERIA_FOR_WIN %}{% if forloop.last %} и {% elif forloop.first %}{% else %}, {% endif %}{{ I.sName }}{% endfor %}. Сравните предложения различных производителей и установщиков, узнайте актуальные цены, технические характеристики стеклопакетов, профилей, фурнитуры и условия доставки/монтажа.",
"image": {
"@type": "ImageObject",
"url": "{{ request.scheme }}://{{ request.get_host }}{% static 'img/oknardia_logo.svg' %}"
},
"brand": {
"@type": "Brand",
"name": "ОКНАРДИЯ"
},
"url": "{{ request.scheme }}://{{ request.get_host }}{{ request.path }}",
"offers": {
"@type": "AggregateOffer",
"priceCurrency": "RUB",
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/InStock",
"offerCount": "{{ NUM_TOTAL_OFFER_N_WORD|safe }}"
},
"datePublished": "{{ META_DATA_PUBLISH|date:'Y-m-d' }}",
"dateModified": "{{ META_DATA_PUBLISH|date:'Y-m-d' }}"
},
{
"@context": "https://schema.org/",
"@type": "ItemList",
"name": "Коммерческие предложения для типового окна",
"numberOfItems": "{{ PRICE_FRAME|length }}",
"itemListElement": [
{% for CurOffer in PRICE_FRAME %}
{
"@type": "ListItem",
"position": {{ forloop.counter }},
"item": {
"@type": "Offer",
"name": "{{ CurOffer.SETS_NAME|striptags|escapejs }}",
"url": "{{ request.scheme }}://{{ request.get_host }}{{ request.path }}#btn{{ CurOffer.SETS_ID }}",
"price": "{{ CurOffer.FIN_PRICE|stringformat:'.2f' }}",
"priceCurrency": "RUB",
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "Organization",
"name": "{{ CurOffer.MERCHANT|striptags|escapejs }}"
},
"itemOffered": {
"@type": "Product",
"name": "{{ CurOffer.SETS_NAME|striptags|escapejs }}",
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "Оконный профиль",
"value": "{{ CurOffer.PVC_NAME|striptags|escapejs }}{% if CurOffer.PVC_MANUFACTURER %} ({{ CurOffer.PVC_MANUFACTURER|striptags|escapejs }}){% endif %}"
}{% if CurOffer.PVC_MANUFACTURER %},
{
"@type": "PropertyValue",
"name": "Производитель профиля",
"value": "{{ CurOffer.PVC_MANUFACTURER|striptags|escapejs }}"
}{% endif %}{% if CurOffer.GLAZING_MARK %},
{
"@type": "PropertyValue",
"name": "Стеклопакет",
"value": "{{ CurOffer.GLAZING_MARK|striptags|escapejs }}"
}{% endif %}
]
},
"dateModified": "{{ CurOffer.SETS_DATA_MODIFY|date:'Y-m-d' }}"{% if CurOffer.SETS_RATING > -0.1 %},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{ CurOffer.SETS_RATING|stringformat:'.2f' }}",
"bestRating": "5",
"worstRating": "0",
"ratingCount": "1"
}{% endif %}
}
}{% if not forloop.last %},{% endif %}
{% endfor %}
]
}
]
</script>{% endblock %}
{% block Description %}Цены на типовое окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth|floatformat:0 }}x{{ I_WIN_DIM.iWinHight|floatformat:0 }} см. для домов серий {% for I in SERIA_FOR_WIN %}{% if forloop.last %} и {% elif forloop.first %}{% else %}, {% endif %}{{ I.sName }}{% endfor %}{% endfor %}.{% endblock %} {% block Description %}Цены на типовое окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth|floatformat:0 }}x{{ I_WIN_DIM.iWinHight|floatformat:0 }} см. для домов серий {% for I in SERIA_FOR_WIN %}{% if forloop.last %} и {% elif forloop.first %}{% else %}, {% endif %}{{ I.sName }}{% endfor %}{% endfor %}.{% endblock %}
{% comment %}{% block Description %}Цены на пластиковые окна для серии {{ BASE_SERIA }} ({{ APART }} квартира, {{ ADDRESS }}) :: {% for CurOffer in PRICE_FRAME %}Поставщик: {{ CurOffer.MERCHANT }}; Комплектация: {{ CurOffer.SETS_NAME }}; Цена: {{ CurOffer.FIN_PRICE }}₽ :: {% endfor %}{% endblock %}{% endcomment %} {% comment %}{% block Description %}Цены на пластиковые окна для серии {{ BASE_SERIA }} ({{ APART }} квартира, {{ ADDRESS }}) :: {% for CurOffer in PRICE_FRAME %}Поставщик: {{ CurOffer.MERCHANT }}; Комплектация: {{ CurOffer.SETS_NAME }}; Цена: {{ CurOffer.FIN_PRICE }}₽ :: {% endfor %}{% endblock %}{% endcomment %}
@@ -146,12 +281,14 @@ $(function () { // инициализация и обработка попове
<span itemscope itemtype="http://schema.org/Product"> <span itemscope itemtype="http://schema.org/Product">
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<h1>Цены на окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth_mm|floatformat:0 }}&times;{{ I_WIN_DIM.iWinHight_mm|floatformat:0 }}{% endfor %}&nbsp;мм. <small>(типовое)</small></h1> <h1 itemprop="name">Цены на окно {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth_mm|floatformat:0 }}&times;{{ I_WIN_DIM.iWinHight_mm|floatformat:0 }}{% endfor %}&nbsp;мм. <small>(типовое)</small></h1>
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<p>Типовой проём {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth|floatformat:1 }}&times;{{ I_WIN_DIM.iWinHight|floatformat:1 }}{% endfor %}&nbsp;cм. представлен в домах серий: {% for I in SERIA_FOR_WIN %}{% if forloop.last %} и {% elif forloop.first %}{% else %}, {% endif %}<a href="/catalog/seria/{{ I.sNameLat }}/all{{ I.id }}">{{ I.sName }}</a>{% endfor %}. База «Окнардии» размещено {{ NUM_TOTAL_OFFER_N_WORD }} цен для окон в такой проем (из них в архиве {{ NUM_ARCHIVE_OFFER }}). Предложено {{ NUM_FLAP_VARIATION_IN_WORD }} открывания от {{ NUM_TOTAL_FIRM_N_WORD }}.</p> <p itemprop="description">Типовой проём {% for I_WIN_DIM in FLAP_DIM %}{{ I_WIN_DIM.iWinWidth|floatformat:1 }}&times;{{ I_WIN_DIM.iWinHight|floatformat:1 }}{% endfor %}&nbsp;cм. представлен в домах серий: {% for I in SERIA_FOR_WIN %}{% if forloop.last %} и {% elif forloop.first %}{% else %}, {% endif %}<a href="/catalog/seria/{{ I.sNameLat }}/all{{ I.id }}">{{ I.sName }}</a>{% endfor %}. База «Окнардии» размещено {{ NUM_TOTAL_OFFER_N_WORD }} цен для окон в такой проем (из них в архиве {{ NUM_ARCHIVE_OFFER }}). Предложено {{ NUM_FLAP_VARIATION_IN_WORD }} открывания от {{ NUM_TOTAL_FIRM_N_WORD }}.</p>
</div> </div>
{# Микроразмектка: названеи продукта #}<meta itemprop="name" content="Окна {{ APART|safe }} ({{ ADDRESS }})" /> {# Микроразметка: название продукта и марка #}
<meta itemprop="brand" content="ОКНАРДИЯ — агрегатор цен на окна" />
<meta itemprop="productionDate" content="{{ META_DATA_PUBLISH|date:'Y-m-d' }}" />
</div> </div>
<div class="row ShowBigFlapPictures"> <div class="row ShowBigFlapPictures">
@@ -176,8 +313,6 @@ $(function () { // инициализация и обработка попове
<div class="col-md-12"> <div class="col-md-12">
<p id="tab-note">В таблице представлены только цены поставщиков из базы «Окнардия». Клик на&nbsp;названии набора отобразит детальную спецификацию каждого предложения: профиль рамы и&nbsp;створки, схему стеклопакета, фурнитуру, элементы отлива, подоконника, откоса, системы <nobr>климат-контроля</nobr>) и&nbsp;сопутствующие услуги. Предложения выводятся блоками. Очередной блок выводится кнопкой &laquo;Ещё коммерческие предложения окон&raquo; под таблицей. Детальные технические характеристики стеклопакетов, профилей и&nbsp;описание сопутствующих услуг можно посмотреть и сравнить с&nbsp;помощью кнопки &laquo;Сравнить выбранные&raquo;.</p> <p id="tab-note">В таблице представлены только цены поставщиков из базы «Окнардия». Клик на&nbsp;названии набора отобразит детальную спецификацию каждого предложения: профиль рамы и&nbsp;створки, схему стеклопакета, фурнитуру, элементы отлива, подоконника, откоса, системы <nobr>климат-контроля</nobr>) и&nbsp;сопутствующие услуги. Предложения выводятся блоками. Очередной блок выводится кнопкой &laquo;Ещё коммерческие предложения окон&raquo; под таблицей. Детальные технические характеристики стеклопакетов, профилей и&nbsp;описание сопутствующих услуг можно посмотреть и сравнить с&nbsp;помощью кнопки &laquo;Сравнить выбранные&raquo;.</p>
</div> </div>
{# Микроразмектка: названеи продукта #}
<meta itemprop="name" content="Окна {{ APART|safe }} ({{ ADDRESS }})"/>
</div> </div>

View File

@@ -6,8 +6,8 @@
{% if forloop.first %} {% if forloop.first %}
<th rowspan="{% if CurOffer.DIM|length == 1 %}2{% else %}{{ CurOffer.DIM|length }}{% endif %}" title="Добавить коммерческое предложение окна к сравнению">{# красивые чекбоксы BEGIN #}<div class="checkbox"><label><input id="CHK{{ CurOffer.SETS_ID }}" type="checkbox" name="ForCompare" value="{{ CurOffer.SETS_ID }}" onChange="ChangeCountCheckedBox({{ CurOffer.SETS_ID }});" /><span class="cr"><i class="cr-icon glyphicon glyphicon-ok"></i></span></label></div>{# красивые чекбоксы END #}</th> <th rowspan="{% if CurOffer.DIM|length == 1 %}2{% else %}{{ CurOffer.DIM|length }}{% endif %}" title="Добавить коммерческое предложение окна к сравнению">{# красивые чекбоксы BEGIN #}<div class="checkbox"><label><input id="CHK{{ CurOffer.SETS_ID }}" type="checkbox" name="ForCompare" value="{{ CurOffer.SETS_ID }}" onChange="ChangeCountCheckedBox({{ CurOffer.SETS_ID }});" /><span class="cr"><i class="cr-icon glyphicon glyphicon-ok"></i></span></label></div>{# красивые чекбоксы END #}</th>
<td rowspan="{% if CurOffer.DIM|length == 1 %}2{% else %}{{ CurOffer.DIM|length }}{% endif %}"{% if CurOffer.IS_COMMERCIAL %} style="background-image: url(/media/{{ CurOffer.MERCHANT_LOGO }})"{% endif %} title="Краткая спецификация коммерческого предложения"> <td rowspan="{% if CurOffer.DIM|length == 1 %}2{% else %}{{ CurOffer.DIM|length }}{% endif %}"{% if CurOffer.IS_COMMERCIAL %} style="background-image: url(/media/{{ CurOffer.MERCHANT_LOGO }})"{% endif %} title="Краткая спецификация коммерческого предложения">
<span itemprop="description"> <span>
<h3 class="set-name shake-trigger" id="btn{{ CurOffer.SETS_ID }}"><a href="javascript://" onclick="show_dtl({{ CurOffer.SETS_ID }})">{{ CurOffer.MERCHANT }}{{ CurOffer.SETS_NAME }}<i class="glyphicon glyphicon-chevron-down shake-vertical"></i></a></h3> <h3 class="set-name shake-trigger" id="btn{{ CurOffer.SETS_ID }}"><a href="javascript://" onclick="show_dtl({{ CurOffer.SETS_ID }})">{{ CurOffer.MERCHANT }} {{ CurOffer.SETS_NAME }}<i class="glyphicon glyphicon-chevron-down shake-vertical"></i></a></h3>
<DiV id="dtl{{ CurOffer.SETS_ID }}" class="collapse">■ Профиль: <a href="/catalog/profile/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_MANUFACTURER_T }}/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_NAME_T }}">{{ CurOffer.PVC_NAME|safe }}</a> (<a href="/catalog/profile/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_MANUFACTURER_T }}">{{ CurOffer.PVC_MANUFACTURER }}</a>) <DiV id="dtl{{ CurOffer.SETS_ID }}" class="collapse">■ Профиль: <a href="/catalog/profile/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_MANUFACTURER_T }}/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_NAME_T }}">{{ CurOffer.PVC_NAME|safe }}</a> (<a href="/catalog/profile/{{ CurOffer.PVC_ID }}-{{ CurOffer.PVC_MANUFACTURER_T }}">{{ CurOffer.PVC_MANUFACTURER }}</a>)
&nbsp;{{ CurOffer.GLAZING_NAME_B|safe }} <nobr>({{ CurOffer.GLAZING_MARK }})</nobr> &nbsp;{{ CurOffer.GLAZING_NAME_B|safe }} <nobr>({{ CurOffer.GLAZING_MARK }})</nobr>
@@ -29,17 +29,15 @@
</DiV> </DiV>
<!-- Дата обновления --> <!-- Дата обновления -->
<nobr class="badge badge4price" title="Дата обновления коммерческого предложения окон — {{ CurOffer.SETS_DATA_MODIFY|date:"d.M.Y" }}"><b class="glyphicon glyphicon-calendar"></b> {{ CurOffer.SETS_DATA_MODIFY|date:"d.M.Y" }}</nobr> <nobr class="badge badge4price" title="Дата обновления коммерческого предложения окон — {{ CurOffer.SETS_DATA_MODIFY|date:"d.M.Y" }}"><b class="glyphicon glyphicon-calendar"></b> {{ CurOffer.SETS_DATA_MODIFY|date:"d.M.Y" }}</nobr>
<!-- Звездочки рейтинга --> <!-- Звездочки рейтинга с микроразметкой Rating -->
<nobr class="badge badge4price" title="Рейтинг «Окнардии»{% if CurOffer.SETS_RATING > -0.1 %} — {{ CurOffer.SETS_RATING|stringformat:".2f" }} баллов{% endif %}"><a <nobr class="badge badge4price" title="Рейтинг «Окнардии»{% if CurOffer.SETS_RATING > -0.1 %} — {{ CurOffer.SETS_RATING|stringformat:".2f" }} баллов{% endif %}">
<a
href="javascript://" href="javascript://"
id-set="{{ CurOffer.SETS_ID }}" id-set="{{ CurOffer.SETS_ID }}"
data-trigger="focus" tabindex="0" data-trigger="focus" tabindex="0"
title="{% if CurOffer.SETS_RATING > 0.01 %}<b> Рейтинг {{ CurOffer.SETS_RATING|stringformat:".2f" }}</b> для оконого набора «{{ CurOffer.SETS_NAME }}» компании «{{ CurOffer.MERCHANT }}» состоит&nbsp;из:{% else %}Рейтинг не присвоен{% endif %}" title="{% if CurOffer.SETS_RATING > 0.01 %}<b> Рейтинг {{ CurOffer.SETS_RATING|stringformat:".2f" }}</b> для оконого набора «{{ CurOffer.SETS_NAME }}» компании «{{ CurOffer.MERCHANT }}» состоит&nbsp;из:{% else %}Рейтинг не присвоен{% endif %}"
data-toggle="popover">рейтинг</a>:&nbsp;{% for Star in CurOffer.SETS_RATING_STARTS %}{% if Star == 0 %}<b class="glyphicon glyphicon-star-empty"></b>{% else %}<b class="glyphicon glyphicon-star"></b>{% endif %}{% endfor %} {% if CurOffer.SETS_RATING > -0.1 %} {{ CurOffer.SETS_RATING|stringformat:".2f" }}{% endif %}</nobr> data-toggle="popover">рейтинг</a>:&nbsp;{% for Star in CurOffer.SETS_RATING_STARTS %}{% if Star == 0 %}<b class="glyphicon glyphicon-star-empty"></b>{% else %}<b class="glyphicon glyphicon-star"></b>{% endif %}{% endfor %} {% if CurOffer.SETS_RATING > -0.1 %}{{ CurOffer.SETS_RATING|stringformat:".2f" }}{% endif %}</nobr>
</span> </span>
<span itemprop="brand" itemscope itemtype="http://schema.org/Brand">
<meta itemprop="name" content="{{ CurOffer.MERCHANT }}" />
<meta itemprop="logo" content="{{ request.scheme }}://{{ request.get_host }}/media/{{ CurOffer.MERCHANT_LOGO }}" />
</span></td> </span></td>
<!--- Конец большой ячейки со спецификацией оконного предложения ---> <!--- Конец большой ячейки со спецификацией оконного предложения --->
{% endif %} {% endif %}
@@ -50,10 +48,8 @@
<td class="rnw" title="Стоимость {{ CurOffer.TOTAL|stringformat:".2f" }} рублей за все окна квартиры {{ APART|safe }}.">{{ CurOffer.TOTAL|stringformat:".2f"|price_format }}</td> <td class="rnw" title="Стоимость {{ CurOffer.TOTAL|stringformat:".2f" }} рублей за все окна квартиры {{ APART|safe }}.">{{ CurOffer.TOTAL|stringformat:".2f"|price_format }}</td>
<th{% if CurOffer.DISCOUNT_COLOR2 != "" %} style="background-color:{{ CurOffer.DISCOUNT_COLOR2 }};"{% endif %} title="{% if CurOffer.DISCOUNT < 0.1 %}Нет скидки{% else %}Скидка — {{ CurOffer.DISCOUNT|stringformat:".1f" }}%{% endif %}">{% if CurOffer.DISCOUNT < 0.1 %}{% else %}&minus;{{ CurOffer.DISCOUNT|stringformat:".1f" }}%{% endif %}</th> <th{% if CurOffer.DISCOUNT_COLOR2 != "" %} style="background-color:{{ CurOffer.DISCOUNT_COLOR2 }};"{% endif %} title="{% if CurOffer.DISCOUNT < 0.1 %}Нет скидки{% else %}Скидка — {{ CurOffer.DISCOUNT|stringformat:".1f" }}%{% endif %}">{% if CurOffer.DISCOUNT < 0.1 %}{% else %}&minus;{{ CurOffer.DISCOUNT|stringformat:".1f" }}%{% endif %}</th>
<th{% if CurOffer.DISCOUNT_COLOR1 != "" %} style="background-color:{{ CurOffer.DISCOUNT_COLOR1 }};"{% endif %} itemprop="offers" itemscope itemtype="http://schema.org/Offer" title="Итого за все окна с учетом скидки: {{ CurOffer.FIN_PRICE|stringformat:".2f" }} рублей"> <th{% if CurOffer.DISCOUNT_COLOR1 != "" %} style="background-color:{{ CurOffer.DISCOUNT_COLOR1 }};"{% endif %} title="Итого за все окна с учетом скидки: {{ CurOffer.FIN_PRICE|stringformat:".2f" }} рублей">
Итого: {{ CurOffer.FIN_PRICE|stringformat:".2f"|price_format }}&thinsp;<small class="glyphicon glyphicon-ruble" aria-label="₽ (руб.)" title="₽ (руб.)"></small> Итого: {{ CurOffer.FIN_PRICE|stringformat:".2f"|price_format }}&thinsp;<small class="glyphicon glyphicon-ruble" aria-label="₽ (руб.)" title="₽ (руб.)"></small>
<meta itemprop="price" content="{{ CurOffer.FIN_PRICE }}" />
<meta itemprop="priceCurrency" content="RUB" />
</th> </th>
{% if CurOffer.DIM|length == 1 %} {% if CurOffer.DIM|length == 1 %}

View File

@@ -6,7 +6,7 @@
<nobr>{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0×{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0&thinsp;мм.</nobr><br />{% if not I_WIN_DIM.iQuantity == 0 %} <nobr>{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0×{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0&thinsp;мм.</nobr><br />{% if not I_WIN_DIM.iQuantity == 0 %}
<nobr><b>{{ I_WIN_DIM.iQuantity }}&thinsp;шт.</b>{% for I_II in I_WIN_DIM.qStr %}<span class="color-bullet" style="background-image:url('{% static 'img/svg/mark' %}{{ I_II }}.svg');"></span>{% endfor %}</nobr><br />{% endif %} <nobr><b>{{ I_WIN_DIM.iQuantity }}&thinsp;шт.</b>{% for I_II in I_WIN_DIM.qStr %}<span class="color-bullet" style="background-image:url('{% static 'img/svg/mark' %}{{ I_II }}.svg');"></span>{% endfor %}</nobr><br />{% endif %}
{{ I_WIN_DIM.sDescription }}{% if not I_WIN_DIM.iQuantity == 0 %}<br /> {{ I_WIN_DIM.sDescription }}{% if not I_WIN_DIM.iQuantity == 0 %}<br />
<a href="/tsena-odnogo-okna/{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0mm/tip{{ I_WIN_DIM.id }}">цены только этого типового окна</a>{% endif %} <a href="/catalog/standard_opening/price-{{ I_WIN_DIM.iWinWidth|stringformat:".0f" }}0x{{ I_WIN_DIM.iWinHight|stringformat:".0f" }}0mm-tip{{ I_WIN_DIM.id }}">цены только этого типового окна</a>{% endif %}
</div> </div>
</div>{% endfor %}{% comment %} </div>{% endfor %}{% comment %}
<script type="text/javascript"> <script type="text/javascript">

View File

@@ -157,7 +157,7 @@ TechArticle: описывает страницу как технический
</tr>{% templatetag openblock %} endfor {% templatetag closeblock %} </tr>{% templatetag openblock %} endfor {% templatetag closeblock %}
<tr class="trZ"> <tr class="trZ">
<td style="font-size: xx-small;vertical-align:text-top">© 2015-{% now "Y" %}, данные: oknardia.ru</td>{% templatetag openblock %} for i in WIN_OFFER_AND_MERCHANT {% templatetag closeblock %} <td style="font-size: xx-small;vertical-align:text-top">© 2015-{% now "Y" %}, данные: oknardia.ru</td>{% templatetag openblock %} for i in WIN_OFFER_AND_MERCHANT {% templatetag closeblock %}
<td class="cntr" style="background:#f9f9f9;"><a href="/tsena-odnogo-okna/{% templatetag openvariable %} i.WIN_W|floatformat:0 {% templatetag closevariable %}0x{% templatetag openvariable %} i.WIN_H|floatformat:0 {% templatetag closevariable %}0mm/tip{% templatetag openvariable %} i.WIN_ID {% templatetag closevariable %}" class="badge" title="Ценовых предложений для окна: {% templatetag openvariable %} i.WIN_OFFER {% templatetag closevariable %}"><small class="glyphicon glyphicon-tags" aria-hidden="true"></small>&nbsp;{% templatetag openvariable %} i.WIN_OFFER {% templatetag closevariable %}</a></td>{% templatetag openblock %} endfor {% templatetag closeblock %} <td class="cntr" style="background:#f9f9f9;"><a href="/catalog/standard_opening/price-{% templatetag openvariable %} i.WIN_W|floatformat:0 {% templatetag closevariable %}0x{% templatetag openvariable %} i.WIN_H|floatformat:0 {% templatetag closevariable %}0mm-tip{% templatetag openvariable %} i.WIN_ID {% templatetag closevariable %}" class="badge" title="Ценовых предложений для окна: {% templatetag openvariable %} i.WIN_OFFER {% templatetag closevariable %}"><small class="glyphicon glyphicon-tags" aria-hidden="true"></small>&nbsp;{% templatetag openvariable %} i.WIN_OFFER {% templatetag closevariable %}</a></td>{% templatetag openblock %} endfor {% templatetag closeblock %}
<td></td> <td></td>
</tr> </tr>
</table> </table>

View File

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

View File

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

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

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