From babfdf78a1fa73d91fd31498ed8ca845c0e40f50 Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 13 Nov 2022 02:06:07 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=87?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B9=D1=82=D0=B8=D0=BD=D0=B3=D0=B0?= =?UTF-8?q?=20=D0=BE=D0=BA=D0=BE=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oknardia/oknardia/urls.py | 16 ++-- .../templates/rating/profiles_rating.html | 67 ++++++++++++++ oknardia/web/add_func.py | 92 ++++++++++--------- oknardia/web/report2.py | 90 ++++++++++++++++++ public/static/js/sortable-table.js | 50 ++++++++++ 5 files changed, 267 insertions(+), 48 deletions(-) create mode 100755 oknardia/templates/rating/profiles_rating.html create mode 100644 oknardia/web/report2.py create mode 100755 public/static/js/sortable-table.js diff --git a/oknardia/oknardia/urls.py b/oknardia/oknardia/urls.py index 2c83134..115b3f9 100644 --- a/oknardia/oknardia/urls.py +++ b/oknardia/oknardia/urls.py @@ -18,7 +18,7 @@ from django.contrib import admin from django.urls import path, re_path from django.conf.urls.static import static from oknardia.settings import * -from web import views, autocomplete_addr, user_manager, blog, diagrams +from web import views, autocomplete_addr, user_manager, blog, diagrams, report2 urlpatterns = [ path('admin/', admin.site.urls), @@ -41,13 +41,15 @@ urlpatterns = [ re_path(r'^change_password$', user_manager.change_password), # ОБРАБОТЧИКИ СПИСКА ПУБЛИКАЦИЙ И САМИХ ПУБЛИКАЦИЙ БЛОГА re_path(r'^blog/*$', blog.blog_list), - re_path(r'^blog/P(?P\d{1,})/*$', blog.blog_list_posts), - re_path(r'^blogpost/(?P\d{1,})/(?P\d{1,})/\S*/*$', blog.blog_post), - re_path(r'^blogpost/(?P\d{1,})/\S*/*$', blog.blog_post), + re_path(r'^blog/P(?P\d+)/*$', blog.blog_list_posts), + re_path(r'^blogpost/(?P\d+)/(?P\d+)/\S*/*$', blog.blog_post), + re_path(r'^blogpost/(?P\d+)/\S*/*$', blog.blog_post), # САТИЧЕСКИЕ СТРАНИЦЫ - re_path(r'^tariff$', views.tariff), - re_path(r'^contact', views.contact), - re_path(r'^stat_all$', diagrams.statistic_menu), + re_path(r'^tariff[/*]$', views.tariff), + re_path(r'^contact[/*]$', views.contact), + re_path(r'^stat_all[/*]$', diagrams.statistic_menu), + re_path(r'^stat/rating[/*]$', report2.ratings), + re_path(r'^stat/rating/profiles_rank[/*]$', report2.profiles_rating), ] diff --git a/oknardia/templates/rating/profiles_rating.html b/oknardia/templates/rating/profiles_rating.html new file mode 100755 index 0000000..ac8e6f3 --- /dev/null +++ b/oknardia/templates/rating/profiles_rating.html @@ -0,0 +1,67 @@ +{% extends "base.html" %}{% load static %} + +{% block Title %}: Тарифы и услуги{% endblock %} + +{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %} + +{# block Date4Meta %}{{ META_DATA_PUBLISH|date:"c" }}{% endblock #} + +{# block Last4Meta %}{{ META_DATA_PUBLISH|date:"c" }}{% endblock #} + +{% block Description %}Тарифы и услуги маркетплейс-агрегатора Окнардия. Размещение предложений пластиковых и деревянных окон, обновление цен на окна, рекламные баннеры и виджеты на сайт оконной компании.{% endblock %} + +{% block Keywords %}типовые проекты зданий, панельное строительство, {% for CountSeria in SERIA_NAV_DIM %}серия {{ CountSeria.SERIA_R }}, {{ CountSeria.SERIA_R }}, {% endfor %}, года постройки, регионы постройки, распространённость{% endblock %} + +{% block Top_JS5 %} + {% endblock %} + +{% block Main_Content %}
+
+

Рейтинг оконных профилей базы «Окнардия»

+
+
+
+ + + + + + + {% for k in KEYS %} + {% endfor %} + + + {% for i in TABLE %} + + + + + {% for j in i.K_ARR %} + {% endfor %} + {% endfor %} + + +
ПроизводительМаркаРейтингK{{ forloop.counter }}
{{ forloop.counter }}.{{ i.BRAND }}{{ i.NAME }} + {% for Star in i.RATING_STAR %}{% if Star == 0 %}{% else %}{% endif %}{% endfor %} {% if i.RATING_N >= 0 %}{{ i.RATING_N|stringformat:".2f" }}{% endif %} 
+
+
+

Характеристики оконных профилей по которым производится ранжирование:

+
    {% for k in KEYS %} +
  • K{{ forloop.counter }}: {{ k }}
  • {% endfor %} +
+
+

Условные обозначения:

+

    — Цвето­вое коди­ров­ание окон­ных про­филей присут­ству­ющих в пред­ложе­ниях участ­ников «Окнар­дии» (реаль­ный рей­тинг). Более насы­щен­ный зелё­ный озна­чает лучшую харак­те­ристику. Харак­те­рис­тики профилей с реаль­ным рей­тингом предос­тавлены компа­ниями-участ­никами.

+

    — Цвето­вое коди­рова­ние окон­ных про­филей из базы «Окнар­дии» по кото­рым нет ком­мерче­ских пред­лож­ений участ­ников (вир­туаль­ный рей­тинг). Более яркий цвет озна­чает лучшую харак­терис­тику. Харак­терис­тики про­филей с вир­туаль­ным рей­тин­гом взяты из незави­симый источ­ников.

+
+

Методика формирования рейтинга «Окнардии», порядок ранжирования и веса характеристик оконных профилей описаны в нашем блоге .

+
+
+
{% endblock %} + +{% comment %} +{% block Top_Nav_Bar %} + {# ОТЛАДКА, ГАСИМ ВЕРХНЕЕ МЕНЮ #} +{% endblock %} +{% endcomment %} + diff --git a/oknardia/web/add_func.py b/oknardia/web/add_func.py index 4c18158..3a1807f 100644 --- a/oknardia/web/add_func.py +++ b/oknardia/web/add_func.py @@ -9,8 +9,8 @@ import math def safe_html_spec_symbols(s: str) -> str: """ Очистка строки от HTML-разметки типографа - :param s: строка которую надо очистить - :return: str: + :param s: str -- строка которую надо очистить + :return: str: str -- очищенная строка """ # очистка строки от некоторых спец-символов HTML result = s.replace('­', '­') @@ -40,50 +40,60 @@ def safe_html_spec_symbols(s: str) -> str: # ), "ru", reversed=True).replace(u" ", u"-").replace(u"'", u"").replace(u"/", u"~").replace(u"\\", u"~").replace(u"--", u"-") -def Rus2Url (RusString): - return re.sub(r'^-|-$', '', - re.sub(r'-{1,}', '-', - re.sub(r'<[\s\S]*?>|&[\S]*?;|[\W]', '-', - re.sub(r'\+', '-plus', translit(RusString, "ru", reversed=True)) - ) - ) - ).lower() +# def Rus2Url (RusString): +# return re.sub(r'^-|-$', '', +# re.sub(r'-{1,}', '-', +# re.sub(r'<[\s\S]*?>|&[\S]*?;|[\W]', '-', +# re.sub(r'\+', '-plus', translit(RusString, "ru", reversed=True)) +# ) +# ) +# ).lower() +# +# +# # Суммирует все цифры в строке через произвольные (не цифровые) разделители +# def SummThrought(StringWSlash): +# StringWSlash = re.sub( r"[^0-9]", u",", StringWSlash) +# ListTerms = StringWSlash.split(u',') +# Summ = 0 +# for Count in ListTerms: +# try: +# Summ += int(Count) +# except: +# pass +# return Summ +# +# +def get_rating_set_for_stars(rating: float = 0.) -> list: + """ Возвращает массив 1 и 0 для отрисовки звёздочек. - -# Суммирует все цифры в строке через произвольные (не цифровые) разделители -def SummThrought(StringWSlash): - StringWSlash = re.sub( r"[^0-9]", u",", StringWSlash) - ListTerms = StringWSlash.split(u',') - Summ = 0 - for Count in ListTerms: - try: - Summ += int(Count) - except: - pass - return Summ - - -# возвращает массив 1 и 0 для отрисовки зввездочек. -def GetRatingSet4Star ( fRating ): + :param rating: float -- рейтинг + :return: list: list -- массив 1 и 0 для отрисовки звёздочек + """ # if fRating < 0.01: # return [] - RatingSet = [] + rating_set = [] for CountStar in range(RARING_STAR): - if RARING_SET_MIN+CountStar*(RARING_SET_MAX-RARING_SET_MIN)/RARING_STAR+1 <= fRating: - RatingSet.append(1) + if RARING_SET_MIN+CountStar * (RARING_SET_MAX - RARING_SET_MIN) / RARING_STAR + 1. <= rating: + rating_set.append(1) else: - RatingSet.append(0) - return RatingSet + rating_set.append(0) + return rating_set +# +# +# # рассчитывает дистанцию в км. между двумя геокоординатами +# def GetGeoDistance(lon1, lat1, lat2, lon2): +# lonA, latA, latB, lonB = map(math.radians, [lon1, lat1, lat2, lon2]) +# distance = 2 * math.asin(math.sqrt(math.sin((latB - latA) / 2) ** 2 + math.cos(latA) * math.cos(latB) * math.sin( +# (lonB - lonA) / 2) ** 2)) * 6371.032 # РАДИУС ЗЕМЛИ 6371.032 КМ. +# return distance -# рассчитывает дистанцию в км. между двумя геокоординатами -def GetGeoDistance(lon1, lat1, lat2, lon2): - lonA, latA, latB, lonB = map(math.radians, [lon1, lat1, lat2, lon2]) - distance = 2 * math.asin(math.sqrt(math.sin((latB - latA) / 2) ** 2 + math.cos(latA) * math.cos(latB) * math.sin( - (lonB - lonA) / 2) ** 2)) * 6371.032 # РАДИУС ЗЕМЛИ 6371.032 КМ. - return distance +def normalize(val: float, val_max: float = 5., val_min: float = 0.) -> float: + """ Нормализация значения - -# нормализация -def Normalize(Val, ValMax=5, ValMin=0): - return float(Val-ValMin)/float(ValMax-ValMin) + :param val: float -- значение которое надо нормализовать + :param val_max: float -- максимальное значение в нормализуемом диапазоне + :param val_min: float -- минимальное значение в нормализуемом диапазоне + :return: float: float -- нормализованное значение + """ + return float(val - val_min) / float(val_max - val_min) diff --git a/oknardia/web/report2.py b/oknardia/web/report2.py new file mode 100644 index 0000000..da4c4de --- /dev/null +++ b/oknardia/web/report2.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# __author__ = 'Sergei Erjemin' +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse +from oknardia.models import PVCprofiles +from oknardia.settings import * +from web.add_func import normalize, get_rating_set_for_stars +from time import time +import json +import pytils + + +def ratings(request: HttpRequest) -> HttpResponse: + """ Страница "Рейтинги" в главном меню + + Т.к. пока есть данные и рейтинги только профилей, то делаем редирект на неё + + :param request: HttpRequest -- входящий http-запрос + :return: HttpResponse -- исходящий http-ответ + """ + return redirect('/stat/rating/profiles_rank/') + + +def profiles_rating(request: HttpRequest) -> HttpResponse: + """ Формирует таблицу с рейтингами оконных профилей + + :param request: HttpRequest -- входящий http-запрос + :return: HttpResponse -- исходящий http-ответ + """ + time_start = time() + template = "rating/profiles_rating.html" + to_template = {} + q = PVCprofiles.objects.order_by("fProfileRating", "fProfileSeals", "fProfileHeatTransf", + "fProfileSoundproofing", "iProfileHeight", "iProfileRabbet", + "iProfileGlazingThickness", "iProfileThickness", "iProfileCameras") + table = [] + keys = [RANK_PVCP_HEAT_TRANSFER_NAME, RANK_PVCP_SOUNDPROOFING_NAME, RANK_PVCP_SEALS_NAME, + RANK_PVCP_HEIGHT_NAME, RANK_PVCP_G_THICKNESS_NAME, RANK_PVCP_THICKNESS_NAME, + RANK_PVCP_RABBET_NAME, RANK_PVCP_CAMERAS_NUM_NAME, RANK_PVCP_CAMERAS_POPULARITY_NAME] + to_template.update({'KEYS': keys}) + for i in q: + try: + received_json = json.loads(i.sProfileDescription) + except json.decoder.JSONDecodeError: + continue + if KEY_RATING in received_json: + rating_real = True + r = received_json[KEY_RATING] + color = int(255 - normalize(i.fProfileRating) * 255) + rating_color = f"{color},255,{color}" + rating_color2 = False + elif KEY_RATING_VIRTUAL in received_json: + rating_real = False + r = received_json[KEY_RATING_VIRTUAL] + color = int(255 - normalize(i.fProfileRating) * 64) + color2 = int(220 - normalize(i.fProfileRating) * 127) + rating_color = f"{color},{color},{color}" + rating_color2 = f"{color2},{color2},{color2}" + else: + continue + # if keys == []: + # keys = sorted(r.keys()) + # to_template.update({'KEYS': keys}) + k_arr = [] + for j in keys: + if rating_real: # Это рейтинг на профили, по которым есть ценник (зелененький) + clr = int(255 - r[j] * 255) + k_arr.append({"COLOR": f"{clr},255,{clr}", "VAL": r[j], "KEY": j}) + else: # Это "потенциальный" рейтинг, без реальных ценовых предложений (серенький) + clr = int(255 - r[j] * 64) + k_arr.append({"COLOR": f"{clr},{clr},{clr}", "VAL": r[j], "KEY": j}) + table.append({ + "ID": i.id, + "R_REAL": rating_real, + "BRAND": i.sProfileManufacturer, + "BRAND_URL": pytils.translit.slugify(i.sProfileManufacturer), + "NAME": i.sProfileName, + "NAME_URL": pytils.translit.slugify(i.sProfileName), + "K_ARR": k_arr, + "RATING_STAR": get_rating_set_for_stars(i.fProfileRating), + "RATING_N": i.fProfileRating, + "RATING_COLOR": rating_color, + "RATING_COLOR2": rating_color2 + }) + + # получаем данные для фрейма ценовых предложений + to_template.update({'TABLE': table}) + to_template.update({'ticks': float(time() - time_start)}) + response = render(request, template, to_template) + return response diff --git a/public/static/js/sortable-table.js b/public/static/js/sortable-table.js new file mode 100755 index 0000000..920a1a0 --- /dev/null +++ b/public/static/js/sortable-table.js @@ -0,0 +1,50 @@ +/** + * Created by Sergei on 22.июл.2017. + * скрипт сортировки таблицы взят из: https://bootsnipp.com/snippets/4OZQx + */ +(function (document) { + let LightTableSorter = (function (Arr) { + let _th, _cellIndex, _order = ''; + function _text(row) { + return row.cells.item(_cellIndex).getAttribute("data-sort").toLowerCase().replace(".", ""); + } + function _sort(a, b) { + let va = _text(a), vb = _text(b), n = parseInt(va, 10); + if (n) va = n, vb = parseInt(vb, 10); + return va > vb ? 1 : va < vb ? -1 : 0; + } + function _toggle() { + let c = _order !== 'asc' ? 'asc' : 'desc'; + _th.className = (_th.className.replace(_order, '') + ' ' + c).trim(); + _order = c; + } + function _reset() { + _th.className = _th.className.replace('asc', '').replace('desc', ''); + _order = ''; + } + function onClickEvent(e) { + if (_th && _cellIndex !== e.target.cellIndex) _reset(); + _th = e.target; + _cellIndex = _th.cellIndex; + let tbody = _th.offsetParent.getElementsByTagName('tbody')[0], rows = tbody.rows; + if (rows) { + rows = Arr.sort.call(Arr.slice.call(rows, 0), _sort); + if (_order === 'asc') Arr.reverse.call(rows); + _toggle(); + tbody.innerHtml = ''; + Arr.forEach.call(rows, function (row){tbody.appendChild(row);}); + } + } + return { + init: function () { + let ths = document.querySelectorAll('table.rang>thead>tr>th'); + Arr.forEach.call(ths, function (th) {th.onclick = onClickEvent;}); + } + }; + })(Array.prototype); + if ($("div").is("#to_load")) + window.LightTableSorter = LightTableSorter; + else document.addEventListener('readystatechange', function(){ + if (document.readyState === 'complete') LightTableSorter.init(); + }, false); +})(document); \ No newline at end of file