mod: унифицированная slug-офикация
This commit is contained in:
@@ -3,42 +3,117 @@ __author__ = 'Sergei Erjemin'
|
|||||||
|
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
|
from pytils.translit import slugify
|
||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
import html
|
||||||
import urllib3
|
import urllib3
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
|
||||||
def safe_html_spec_symbols(s: str) -> str:
|
def safe_html_spec_symbols(s: str) -> str:
|
||||||
""" Очистка строки от HTML-разметки типографа
|
""" Очистка строки от HTML-разметки и получение чистого текста.
|
||||||
|
|
||||||
:param s: str -- строка которую надо очистить
|
Функция удаляет HTML-теги, содержимое исключённых тегов (script, style, object, embed, applet,
|
||||||
:return: str: str -- очищенная строка
|
iframe, svg, canvas, code, kbd, pre, var, samp, output, noscript, link, meta, form, input,
|
||||||
|
button, textarea, select, base, title, head, body, track, source, picture), заменяет HTML-мнемоники
|
||||||
|
на Unicode-символы и убирает лишние пробелы.
|
||||||
|
|
||||||
|
:param s: str -- строка которую надо очистить
|
||||||
|
:return: str -- очищенная строка с чистым текстом
|
||||||
"""
|
"""
|
||||||
# очистка строки от некоторых спец-символов HTML
|
# Шаг 1: Удаляем содержимое "опасных" и невидимых тегов
|
||||||
result = s.replace('­', '')
|
# Опасные: script, object, embed, applet, iframe, svg, canvas
|
||||||
result = result.replace('<span class="laquo">', '')
|
# Техническое содержимое: style, code, kbd, pre, var, samp, output, noscript
|
||||||
result = result.replace('<span style="margin-right:0.44em;">', '')
|
# Формы: form, input, button, textarea, select
|
||||||
result = result.replace('<span style="margin-left:-0.44em;">', '')
|
# Служебные: meta, link, base, title, head, body, track, source, picture
|
||||||
result = result.replace('<span class="raquo">', '')
|
# Используем флаг IGNORECASE и DOTALL для работы с многострочным контентом
|
||||||
result = result.replace('<span class="point">', '')
|
result = re.sub(
|
||||||
result = result.replace('<span class="thinsp">', ' ')
|
r'<(script|style|code|kbd|pre|var|samp|output|noscript|link|meta|iframe|object|embed|applet|form|input|button|textarea|select|svg|canvas|base|title|head|body|track|source|picture)(?:\s[^>]*)?>.*?</\1>',
|
||||||
result = result.replace('<span class="ensp">', '')
|
'',
|
||||||
result = result.replace('</span>', '')
|
s,
|
||||||
result = result.replace(' ', ' ')
|
flags=re.IGNORECASE | re.DOTALL
|
||||||
result = result.replace('«', '«')
|
)
|
||||||
result = result.replace('»', '»')
|
|
||||||
result = result.replace('…', '…')
|
# Удаляем самозакрывающиеся теги (что-то типа <input/>, <embed/>, и т.д.)
|
||||||
result = result.replace('<nobr>', '')
|
result = re.sub(
|
||||||
result = result.replace('</nobr>', '')
|
r'<(input|embed|meta|link|base|track|source|img)(?:\s[^>]*)?/>',
|
||||||
result = result.replace('—', '—')
|
'',
|
||||||
result = result.replace('№', '№')
|
result,
|
||||||
result = result.replace('<br />', ' ')
|
flags=re.IGNORECASE
|
||||||
result = result.replace('<br>', ' ')
|
)
|
||||||
|
|
||||||
|
# Шаг 2: Удаляем все остальные HTML-теги (в т.ч. самозакрывающиеся)
|
||||||
|
result = re.sub(r'<[^>]+>', '', result)
|
||||||
|
|
||||||
|
# Шаг 3: Заменяем HTML-мнемоники на Unicode-символы (включая числовые и именованные)
|
||||||
|
# html.unescape() обрабатывает: , <, №, € и т.д.
|
||||||
|
result = html.unescape(result)
|
||||||
|
|
||||||
|
# Шаг 4: Очищаем множественные пробелы (в т.ч. табуляцию и переводы строк)
|
||||||
|
result = re.sub(r'\s+', ' ', result)
|
||||||
|
|
||||||
|
# Шаг 5: Убираем пробелы в начале и конце строки
|
||||||
|
result = result.strip()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_slug(text: str, separator: str = '-', max_length: int = 200) -> str:
|
||||||
|
""" Преобразует текст в URL-безопасный слаг (slug).
|
||||||
|
|
||||||
|
Функция очищает текст от HTML-разметки, выполняет транслитерацию русского текста в
|
||||||
|
латиницу, заменяет пробелы и недопустимые символы на разделитель (по умолчанию дефис),
|
||||||
|
и возвращает готовый к использованию в URL слаг.
|
||||||
|
|
||||||
|
Этапы обработки:
|
||||||
|
1. Очистка от HTML-разметки через safe_html_spec_symbols()
|
||||||
|
2. Транслитерация русского текста в латиницу через pytils.translit.slugify()
|
||||||
|
3. Замена множественных разделителей на один
|
||||||
|
4. Удаление разделителя в начале и конце
|
||||||
|
5. Прерывание на max_length символов
|
||||||
|
|
||||||
|
:param text: str -- исходный текст, может содержать HTML и русский текст
|
||||||
|
:param separator: str -- разделитель для слага (по умолчанию дефис '-')
|
||||||
|
pytils.slugify() всегда использует дефис, этот параметр
|
||||||
|
конвертирует результат в нужный разделитель
|
||||||
|
:param max_length: int -- максимальная длина слага в символах (по умолчанию 200)
|
||||||
|
:return: str -- очищенный и готовый к использованию слаг
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
>>> sanitize_slug(' Тест — HTML <b>текст</b> ')
|
||||||
|
'test-html-tekst'
|
||||||
|
>>> sanitize_slug('Привет мир!!! @#$')
|
||||||
|
'privet-mir'
|
||||||
|
>>> sanitize_slug('<p>Русский текст в слаге</p>')
|
||||||
|
'russkii-tekst-v-slage'
|
||||||
|
>>> sanitize_slug('Проверка_слага', separator='_')
|
||||||
|
'proverka_slaga'
|
||||||
|
"""
|
||||||
|
# Шаг 1: Очищаем от HTML и мнемоник, убираем лишние пробелы
|
||||||
|
cleaned = safe_html_spec_symbols(text)
|
||||||
|
|
||||||
|
# Шаг 2: Транслитерируем русский текст в латиницу (pytils.slugify использует дефис)
|
||||||
|
slug = slugify(cleaned)
|
||||||
|
|
||||||
|
# Шаг 3: Конвертируем разделитель если нужен другой (не дефис)
|
||||||
|
if separator != '-':
|
||||||
|
slug = slug.replace('-', separator)
|
||||||
|
|
||||||
|
# Шаг 4: Убираем множественные разделители (например, '---' -> '-')
|
||||||
|
slug = re.sub(f'{re.escape(separator)}+', separator, slug)
|
||||||
|
|
||||||
|
# Шаг 5: Убираем разделитель в начале и конце если он есть
|
||||||
|
slug = slug.strip(separator)
|
||||||
|
|
||||||
|
# Шаг 6: Обрезаем по max_length если нужно (и убираем разделитель в конце)
|
||||||
|
if max_length and len(slug) > max_length:
|
||||||
|
slug = slug[:max_length].rstrip(separator)
|
||||||
|
|
||||||
|
return slug.lower()
|
||||||
|
|
||||||
|
|
||||||
# def Rus2Lat(RusString):
|
# def Rus2Lat(RusString):
|
||||||
# return translit(re.sub(
|
# return translit(re.sub(
|
||||||
# r'<[\s\S]*?>', '', re.sub(r'&[\S]*?;', '-', RusString)
|
# r'<[\s\S]*?>', '', re.sub(r'&[\S]*?;', '-', RusString)
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from oknardia.models import BlogPosts
|
from oknardia.models import BlogPosts
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from web.add_func import safe_html_spec_symbols
|
from web.add_func import safe_html_spec_symbols, sanitize_slug
|
||||||
from time import time
|
from time import time
|
||||||
import re
|
import re
|
||||||
import pytils
|
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ def blog_list_posts(request: HttpRequest, page: str = "0") -> HttpResponse:
|
|||||||
'PUB_DAT': post.dPostDataBegin,
|
'PUB_DAT': post.dPostDataBegin,
|
||||||
'HEADER': post.sPostHeader,
|
'HEADER': post.sPostHeader,
|
||||||
'HEADER_D': safe_html_spec_symbols(post.sPostHeader),
|
'HEADER_D': safe_html_spec_symbols(post.sPostHeader),
|
||||||
'HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(post.sPostHeader)).lower(),
|
'HEADER_T': sanitize_slug(post.sPostHeader).lower(),
|
||||||
'POST_ID': post.id,
|
'POST_ID': post.id,
|
||||||
'USER_STATUS': post.kBlogAuthorUser.get_sUserStatus_display(),
|
'USER_STATUS': post.kBlogAuthorUser.get_sUserStatus_display(),
|
||||||
'USER_AVATAR': post.kBlogAuthorUser.sUserAvatarImg,
|
'USER_AVATAR': post.kBlogAuthorUser.sUserAvatarImg,
|
||||||
@@ -160,20 +159,19 @@ def blog_post(request: HttpRequest, post_id: str = "0", page_back: str = None) -
|
|||||||
to_template.update({'PUB_DAT': q.dPostDataBegin,
|
to_template.update({'PUB_DAT': q.dPostDataBegin,
|
||||||
'PUB_MODIFY': q.dPostDataModify,
|
'PUB_MODIFY': q.dPostDataModify,
|
||||||
'HEADER': q.sPostHeader,
|
'HEADER': q.sPostHeader,
|
||||||
'HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q.sPostHeader)).lower(),
|
'HEADER_T': sanitize_slug(q.sPostHeader).lower(),
|
||||||
'USER_STATUS': q.kBlogAuthorUser.get_sUserStatus_display(),
|
'USER_STATUS': q.kBlogAuthorUser.get_sUserStatus_display(),
|
||||||
'USER_AVATAR': q.kBlogAuthorUser.sUserAvatarImg,
|
'USER_AVATAR': q.kBlogAuthorUser.sUserAvatarImg,
|
||||||
'USER_TITLE': q.kBlogAuthorUser.sUserJobTitle,
|
'USER_TITLE': q.kBlogAuthorUser.sUserJobTitle,
|
||||||
'USER_FROM_ID_OFFICE': q.kBlogAuthorUser.kMerchantOffice,
|
'USER_FROM_ID_OFFICE': q.kBlogAuthorUser.kMerchantOffice,
|
||||||
'CONTENT': re.sub(r'<cut[\s\S]*?>', '', q.sPostContent, 0, re.IGNORECASE)})
|
'CONTENT': re.sub(r'<cut[\s\S]*?>', '', q.sPostContent, 0, re.IGNORECASE)})
|
||||||
to_template.update({'TIZER': safe_html_spec_symbols(
|
content = to_template.get('CONTENT', '')
|
||||||
re.sub('<script[\s\S]*?</script>|<style[\s\S]*?</style>|<iframe[\s\S]*?</iframe>',
|
to_template.update({'TIZER': sanitize_slug(str(content))})
|
||||||
'', to_template["CONTENT"], 0, re.IGNORECASE))})
|
|
||||||
# получаем следующую по дате запись
|
# получаем следующую по дате запись
|
||||||
try:
|
try:
|
||||||
q1 = BlogPosts.objects.filter(dPostDataBegin__gt=q.dPostDataBegin, dPostDataBegin__lt=timezone.now(),
|
q1 = BlogPosts.objects.filter(dPostDataBegin__gt=q.dPostDataBegin, dPostDataBegin__lt=timezone.now(),
|
||||||
bPublished=True, bArchive=False).order_by('dPostDataBegin')[0]
|
bPublished=True, bArchive=False).order_by('dPostDataBegin')[0]
|
||||||
to_template.update({'FORW_HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q1.sPostHeader)).lower(),
|
to_template.update({'FORW_HEADER_T': sanitize_slug(q1.sPostHeader).lower(),
|
||||||
'FORW_ID': q1.id})
|
'FORW_ID': q1.id})
|
||||||
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
|
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
|
||||||
to_template.update({'FORW_DISABLE': True})
|
to_template.update({'FORW_DISABLE': True})
|
||||||
@@ -181,7 +179,7 @@ def blog_post(request: HttpRequest, post_id: str = "0", page_back: str = None) -
|
|||||||
try:
|
try:
|
||||||
q1 = BlogPosts.objects.filter(dPostDataBegin__lt=q.dPostDataBegin, bPublished=True,
|
q1 = BlogPosts.objects.filter(dPostDataBegin__lt=q.dPostDataBegin, bPublished=True,
|
||||||
bArchive=False).order_by('-dPostDataBegin')[0]
|
bArchive=False).order_by('-dPostDataBegin')[0]
|
||||||
to_template.update({'BACK_HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q1.sPostHeader)).lower(),
|
to_template.update({'BACK_HEADER_T': sanitize_slug(q1.sPostHeader).lower(),
|
||||||
'BACK_ID': q1.id})
|
'BACK_ID': q1.id})
|
||||||
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
|
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
|
||||||
to_template.update({'BACK_DISABLE': True})
|
to_template.update({'BACK_DISABLE': True})
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytils.translit
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
|
|
||||||
from oknardia.models import Seria_Info, SetKit
|
from oknardia.models import Seria_Info, SetKit
|
||||||
from web.add_func import get_rating_set_for_stars
|
from web.add_func import get_rating_set_for_stars, sanitize_slug
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ def catalog_sets(request: HttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
Для каждого набора собирается dict с полями набора, профиля, стеклопакета и компании-установщика.
|
Для каждого набора собирается dict с полями набора, профиля, стеклопакета и компании-установщика.
|
||||||
Цепочка FK: SetKit.kSet2User → OurUser.kMerchantOffice → MerchantOffice.kMerchantName (MerchantBrand).
|
Цепочка FK: SetKit.kSet2User → OurUser.kMerchantOffice → MerchantOffice.kMerchantName (MerchantBrand).
|
||||||
Слаги URL формируются через pytils.translit.slugify.
|
Слаги URL формируются через sanitize_slug.
|
||||||
|
|
||||||
:param request: HttpRequest -- входящий http-запрос
|
:param request: HttpRequest -- входящий http-запрос
|
||||||
:return response: HttpResponse -- исходящий http-ответ
|
:return response: HttpResponse -- исходящий http-ответ
|
||||||
@@ -69,15 +68,13 @@ def catalog_sets(request: HttpRequest) -> HttpResponse:
|
|||||||
'glazing': glazing,
|
'glazing': glazing,
|
||||||
# компания-установщик
|
# компания-установщик
|
||||||
'merchant_id': brand.id if brand else None,
|
'merchant_id': brand.id if brand else None,
|
||||||
'merchant_slug': pytils.translit.slugify(brand.sMerchantName) if brand else "",
|
'merchant_slug': sanitize_slug(brand.sMerchantName) if brand else "",
|
||||||
'merchant_name': brand.sMerchantName if brand else "",
|
'merchant_name': brand.sMerchantName if brand else "",
|
||||||
'merchant_logo': str(brand.pMerchantLogo) if brand and brand.pMerchantLogo else "",
|
'merchant_logo': str(brand.pMerchantLogo) if brand and brand.pMerchantLogo else "",
|
||||||
'merchant_url': brand.sMerchantMainURL if brand else "",
|
'merchant_url': brand.sMerchantMainURL if brand else "",
|
||||||
# слаги для ссылок на профиль в каталоге профилей
|
# слаги для ссылок на профиль в каталоге профилей
|
||||||
'profile_manufacturer_slug': pytils.translit.slugify(
|
'profile_manufacturer_slug': sanitize_slug(profile.sProfileManufacturer) if profile else "",
|
||||||
profile.sProfileManufacturer) if profile else "",
|
'profile_slug': sanitize_slug(profile.sProfileName) if profile else "",
|
||||||
'profile_slug': pytils.translit.slugify(
|
|
||||||
profile.sProfileName) if profile else "",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
to_template: dict[str, object] = {
|
to_template: dict[str, object] = {
|
||||||
@@ -99,7 +96,7 @@ def report_all_info_seria_redirect(request: HttpRequest, seria_id: str = "12") -
|
|||||||
seria_id = int(seria_id)
|
seria_id = int(seria_id)
|
||||||
q_seria = Seria_Info.objects.get(id=seria_id)
|
q_seria = Seria_Info.objects.get(id=seria_id)
|
||||||
if q_seria.id == q_seria.kRoot_id:
|
if q_seria.id == q_seria.kRoot_id:
|
||||||
return redirect("f/catalog/seria/{pytils.translit.slugify(q_seria.sName)}/all{seria_id}")
|
return redirect(f"/catalog/seria/{sanitize_slug(q_seria.sName)}/all{seria_id}")
|
||||||
except (Seria_Info.DoesNotExist, ValueError):
|
except (Seria_Info.DoesNotExist, ValueError):
|
||||||
return redirect("/catalog/seria")
|
return redirect("/catalog/seria")
|
||||||
return redirect("/catalog/seria")
|
return redirect("/catalog/seria")
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from oknardia.models import (
|
|||||||
SetKit,
|
SetKit,
|
||||||
PriceOffer,
|
PriceOffer,
|
||||||
)
|
)
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list
|
||||||
from web.add_func import get_rating_set_for_stars
|
from web.add_func import get_rating_set_for_stars, sanitize_slug
|
||||||
import django.utils.dateformat
|
import django.utils.dateformat
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
@@ -131,12 +131,10 @@ def _format_company_for_template(company_data: dict) -> dict:
|
|||||||
dict: Отформатированные данные компании
|
dict: Отформатированные данные компании
|
||||||
"""
|
"""
|
||||||
formatted = company_data.copy()
|
formatted = company_data.copy()
|
||||||
|
|
||||||
# Вычисляем звёзды на основе рейтинга
|
# Вычисляем звёзды на основе рейтинга
|
||||||
formatted['STARS'] = get_rating_set_for_stars(
|
formatted['STARS'] = get_rating_set_for_stars(
|
||||||
formatted['RatingAVG']
|
formatted['RatingAVG']
|
||||||
)
|
)
|
||||||
|
|
||||||
# Применяем правильные формы множественного числа
|
# Применяем правильные формы множественного числа
|
||||||
formatted['NumSets'] = pytils.numeral.get_plural(
|
formatted['NumSets'] = pytils.numeral.get_plural(
|
||||||
formatted['NumSets'],
|
formatted['NumSets'],
|
||||||
@@ -146,7 +144,6 @@ def _format_company_for_template(company_data: dict) -> dict:
|
|||||||
formatted['NumOffers'],
|
formatted['NumOffers'],
|
||||||
"вариант, варианта, вариантов"
|
"вариант, варианта, вариантов"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Конвертируем время последнего обновления в читаемый формат
|
# Конвертируем время последнего обновления в читаемый формат
|
||||||
if formatted['lastUpdate']:
|
if formatted['lastUpdate']:
|
||||||
timestamp = int(
|
timestamp = int(
|
||||||
@@ -158,12 +155,8 @@ def _format_company_for_template(company_data: dict) -> dict:
|
|||||||
formatted['lastUpdate'] = pytils.dt.distance_of_time_in_words(
|
formatted['lastUpdate'] = pytils.dt.distance_of_time_in_words(
|
||||||
timestamp
|
timestamp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Генерируем slug из имени компании для URL
|
# Генерируем slug из имени компании для URL
|
||||||
formatted['sMerchantMainURL'] = pytils.translit.slugify(
|
formatted['sMerchantMainURL'] = sanitize_slug(formatted['sMerchantName'])
|
||||||
formatted['sMerchantName']
|
|
||||||
)
|
|
||||||
|
|
||||||
return formatted
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
@@ -387,11 +380,11 @@ def _format_set_for_template(set_data: dict, empty_values: list) -> dict:
|
|||||||
'iProfileCameras': profile.iProfileCameras,
|
'iProfileCameras': profile.iProfileCameras,
|
||||||
'sProfileName': {
|
'sProfileName': {
|
||||||
'NAME': profile.sProfileName,
|
'NAME': profile.sProfileName,
|
||||||
'NAME_T': pytils.translit.slugify(profile.sProfileName)
|
'NAME_T': sanitize_slug(profile.sProfileName)
|
||||||
},
|
},
|
||||||
'sProfileManufacturer': {
|
'sProfileManufacturer': {
|
||||||
'NAME': profile.sProfileManufacturer,
|
'NAME': profile.sProfileManufacturer,
|
||||||
'NAME_T': pytils.translit.slugify(profile.sProfileManufacturer)
|
'NAME_T': sanitize_slug(profile.sProfileManufacturer)
|
||||||
},
|
},
|
||||||
'sProfileColor': profile.sProfileColor,
|
'sProfileColor': profile.sProfileColor,
|
||||||
'sProfileSealDescription': profile.sProfileSealDescription,
|
'sProfileSealDescription': profile.sProfileSealDescription,
|
||||||
@@ -482,7 +475,7 @@ def catalog_company_detail(
|
|||||||
raise Http404("Компания не найдена")
|
raise Http404("Компания не найдена")
|
||||||
|
|
||||||
# Проверяем что slug совпадает (для SEO и красивых URL)
|
# Проверяем что slug совпадает (для SEO и красивых URL)
|
||||||
actual_slug = pytils.translit.slugify(company.sMerchantName)
|
actual_slug = sanitize_slug(company.sMerchantName)
|
||||||
if actual_slug != company_name_slug:
|
if actual_slug != company_name_slug:
|
||||||
return redirect(
|
return redirect(
|
||||||
f'/catalog/company/{company_id_int}-{actual_slug}'
|
f'/catalog/company/{company_id_int}-{actual_slug}'
|
||||||
|
|||||||
@@ -3,20 +3,14 @@ from django.db.models import F
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from oknardia.models import MountDim2Apartment
|
from oknardia.models import MountDim2Apartment
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list
|
||||||
from web.add_func import get_flaps_for_mini_pictures
|
from web.add_func import get_flaps_for_mini_pictures, sanitize_slug
|
||||||
import time
|
import time
|
||||||
import pytils
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
|
|
||||||
def _make_slug(value: str) -> str:
|
|
||||||
"""Транслитерирует строку в slug (pytils)."""
|
|
||||||
return pytils.translit.slugify(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
|
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
|
||||||
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
|
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
|
||||||
to_template.update({
|
to_template.update({
|
||||||
@@ -73,7 +67,7 @@ def standard_opening(request: HttpRequest) -> HttpResponse:
|
|||||||
serias_for_opening = [
|
serias_for_opening = [
|
||||||
{
|
{
|
||||||
'ID': row['kApartment__kSeria_id'],
|
'ID': row['kApartment__kSeria_id'],
|
||||||
'NAME_T': _make_slug(row['kApartment__kSeria__sName']),
|
'NAME_T': sanitize_slug(row['kApartment__kSeria__sName']),
|
||||||
'NAME': row['kApartment__kSeria__sName'],
|
'NAME': row['kApartment__kSeria__sName'],
|
||||||
}
|
}
|
||||||
for row in rows_for_opening
|
for row in rows_for_opening
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from django.shortcuts import render, redirect
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from oknardia.models import Catalog2Profile, PVCprofiles, PriceOffer
|
from oknardia.models import Catalog2Profile, PVCprofiles, PriceOffer
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list
|
||||||
from web.add_func import normalize, get_rating_set_for_stars
|
from web.add_func import normalize, get_rating_set_for_stars, sanitize_slug
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@@ -17,19 +17,13 @@ import pytils
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Модульные хелперы, общие для всех вьюх этого файла
|
# Модульные хелперы, общие для всех вьюх этого файла
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def make_slug(value: str) -> str:
|
|
||||||
"""Транслитерирует строку в slug (pytils)."""
|
|
||||||
return pytils.translit.slugify(value).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def _merchant_row_to_dict(row: dict) -> dict:
|
def _merchant_row_to_dict(row: dict) -> dict:
|
||||||
"""Преобразует ORM-строку с данными партнёра в словарь для шаблона."""
|
"""Преобразует ORM-строку с данными партнёра в словарь для шаблона."""
|
||||||
merchant_name = row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"]
|
merchant_name = row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"]
|
||||||
return {
|
return {
|
||||||
"MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"],
|
"MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"],
|
||||||
"MERCHANT_NAME": merchant_name,
|
"MERCHANT_NAME": merchant_name,
|
||||||
"MERCHANT_NAME_T": make_slug(merchant_name),
|
"MERCHANT_NAME_T": sanitize_slug(merchant_name),
|
||||||
"MERCHANT_LOGO_URL": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo"],
|
"MERCHANT_LOGO_URL": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo"],
|
||||||
"MERCHANT_OFFERS": row["offers_by_merchant"],
|
"MERCHANT_OFFERS": row["offers_by_merchant"],
|
||||||
}
|
}
|
||||||
@@ -40,7 +34,7 @@ def _profile_row_to_dict(profile: dict) -> dict:
|
|||||||
return {
|
return {
|
||||||
"PROFILE_NAME": profile["sProfileBriefDescription"],
|
"PROFILE_NAME": profile["sProfileBriefDescription"],
|
||||||
"PROFILE_ID": profile["id"],
|
"PROFILE_ID": profile["id"],
|
||||||
"PROFILE_URL": make_slug(profile["sProfileName"]),
|
"PROFILE_URL": sanitize_slug(profile["sProfileName"]),
|
||||||
"PROFILE_RATING": profile["fProfileRating"],
|
"PROFILE_RATING": profile["fProfileRating"],
|
||||||
"PROFILE_RATING_STARS": get_rating_set_for_stars(profile["fProfileRating"]),
|
"PROFILE_RATING_STARS": get_rating_set_for_stars(profile["fProfileRating"]),
|
||||||
}
|
}
|
||||||
@@ -94,11 +88,11 @@ def catalog_profile(request: HttpRequest) -> HttpResponse:
|
|||||||
list_profile_manufactures.append({
|
list_profile_manufactures.append({
|
||||||
"PROF_MAN_ID": profile["id"],
|
"PROF_MAN_ID": profile["id"],
|
||||||
"PROF_MAN": profile["sProfileManufacturer"],
|
"PROF_MAN": profile["sProfileManufacturer"],
|
||||||
"PROF_MAN_T": make_slug(profile["sProfileManufacturer"]),
|
"PROF_MAN_T": sanitize_slug(profile["sProfileManufacturer"]),
|
||||||
"PROF_MAN_LIST": [{
|
"PROF_MAN_LIST": [{
|
||||||
"PROF_NAME_ID": profile["id"],
|
"PROF_NAME_ID": profile["id"],
|
||||||
"PROF_NAME": profile["sProfileBriefDescription"],
|
"PROF_NAME": profile["sProfileBriefDescription"],
|
||||||
"PROF_NAME_T": make_slug(profile["sProfileName"]),
|
"PROF_NAME_T": sanitize_slug(profile["sProfileName"]),
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -106,7 +100,7 @@ def catalog_profile(request: HttpRequest) -> HttpResponse:
|
|||||||
list_profile_manufactures[-1]["PROF_MAN_LIST"].append({
|
list_profile_manufactures[-1]["PROF_MAN_LIST"].append({
|
||||||
"PROF_NAME_ID": profile["id"],
|
"PROF_NAME_ID": profile["id"],
|
||||||
"PROF_NAME": profile["sProfileBriefDescription"],
|
"PROF_NAME": profile["sProfileBriefDescription"],
|
||||||
"PROF_NAME_T": make_slug(profile["sProfileName"]),
|
"PROF_NAME_T": sanitize_slug(profile["sProfileName"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
to_template.update({
|
to_template.update({
|
||||||
@@ -128,17 +122,17 @@ def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture
|
|||||||
|
|
||||||
:param request: HttpRequest -- входящий http-запрос
|
:param request: HttpRequest -- входящий http-запрос
|
||||||
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
|
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
|
||||||
:param manufacture_name: название производителя (транслитерированное pytils.translit.slugify())
|
:param manufacture_name: название производителя (транслитерированное sanitize_slug())
|
||||||
:param model_id: id модели (марки) профиля
|
:param model_id: id модели (марки) профиля
|
||||||
:param model_name: модель (марка) профиля (транслитерированное pytils.translit.slugify(sProfileName))
|
:param model_name: модель (марка) профиля (транслитерированное sanitize_slug(sProfileName))
|
||||||
:return response: HttpResponse -- исходящий http-ответ
|
:return response: HttpResponse -- исходящий http-ответ
|
||||||
"""
|
"""
|
||||||
time_start = time.perf_counter()
|
time_start = time.perf_counter()
|
||||||
manufacture_id = int(manufacture_id)
|
manufacture_id = int(manufacture_id)
|
||||||
model_id = int(model_id)
|
model_id = int(model_id)
|
||||||
q_pvc_by_id = PVCprofiles.objects.get(id=model_id)
|
q_pvc_by_id = PVCprofiles.objects.get(id=model_id)
|
||||||
manufacturer_slug = pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)
|
manufacturer_slug = sanitize_slug(q_pvc_by_id.sProfileManufacturer)
|
||||||
model_slug = pytils.translit.slugify(q_pvc_by_id.sProfileName)
|
model_slug = sanitize_slug(q_pvc_by_id.sProfileName)
|
||||||
if manufacturer_slug != manufacture_name \
|
if manufacturer_slug != manufacture_name \
|
||||||
or model_slug != model_name \
|
or model_slug != model_name \
|
||||||
or manufacture_id != model_id:
|
or manufacture_id != model_id:
|
||||||
@@ -268,21 +262,21 @@ def catalog_profile_manufacture(request: HttpRequest, manufacture_id: int, manuf
|
|||||||
|
|
||||||
:param request: HttpRequest -- входящий http-запрос
|
:param request: HttpRequest -- входящий http-запрос
|
||||||
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
|
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
|
||||||
:param manufacture_name: название производителя (транслитерированное pytils.translit.slugify())
|
:param manufacture_name: название производителя (транслитерированное sanitize_slug())
|
||||||
:return response: HttpResponse -- исходящий http-ответ
|
:return response: HttpResponse -- исходящий http-ответ
|
||||||
"""
|
"""
|
||||||
time_start = time.perf_counter()
|
time_start = time.perf_counter()
|
||||||
manufacture_id = int(manufacture_id)
|
manufacture_id = int(manufacture_id)
|
||||||
q_pvc_by_id = PVCprofiles.objects.get(id=manufacture_id)
|
q_pvc_by_id = PVCprofiles.objects.get(id=manufacture_id)
|
||||||
if pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) != manufacture_name:
|
if sanitize_slug(q_pvc_by_id.sProfileManufacturer) != manufacture_name:
|
||||||
return redirect(f'/catalog/profile/{manufacture_id}-'
|
return redirect(f'/catalog/profile/{manufacture_id}-'
|
||||||
f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}')
|
f'{sanitize_slug(q_pvc_by_id.sProfileManufacturer)}')
|
||||||
else:
|
else:
|
||||||
q_pvc_by_id = PVCprofiles.objects.order_by('id') \
|
q_pvc_by_id = PVCprofiles.objects.order_by('id') \
|
||||||
.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer).first()
|
.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer).first()
|
||||||
if q_pvc_by_id.id != manufacture_id:
|
if q_pvc_by_id.id != manufacture_id:
|
||||||
return redirect(f'/catalog/profile/{q_pvc_by_id.id}-'
|
return redirect(f'/catalog/profile/{q_pvc_by_id.id}-'
|
||||||
f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}')
|
f'{sanitize_slug(q_pvc_by_id.sProfileManufacturer)}')
|
||||||
to_template: dict[str, object] = {'CATALOG_MANUFACT': q_pvc_by_id.sProfileManufacturer,
|
to_template: dict[str, object] = {'CATALOG_MANUFACT': q_pvc_by_id.sProfileManufacturer,
|
||||||
'CATALOG_MAN2URL': manufacture_name,
|
'CATALOG_MAN2URL': manufacture_name,
|
||||||
'CATALOG_URL': f"{manufacture_id}-{manufacture_name}"}
|
'CATALOG_URL': f"{manufacture_id}-{manufacture_name}"}
|
||||||
|
|||||||
@@ -14,18 +14,13 @@ from oknardia.models import (
|
|||||||
Building_Info,
|
Building_Info,
|
||||||
)
|
)
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
||||||
from web.add_func import get_flaps_for_big_pictures
|
from web.add_func import get_flaps_for_big_pictures, sanitize_slug
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
import pytils
|
import pytils
|
||||||
|
|
||||||
|
|
||||||
def _make_slug(value: str) -> str:
|
|
||||||
"""Транслитерирует строку в slug (pytils)."""
|
|
||||||
return pytils.translit.slugify(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
|
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
|
||||||
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
|
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
|
||||||
to_template.update({
|
to_template.update({
|
||||||
@@ -54,7 +49,7 @@ def catalog_seria(request: HttpRequest) -> HttpResponse:
|
|||||||
'ID': row['id'],
|
'ID': row['id'],
|
||||||
'URL': row['sURL2IMG'],
|
'URL': row['sURL2IMG'],
|
||||||
'NAME': row['sName'],
|
'NAME': row['sName'],
|
||||||
'NAME_T': _make_slug(row['sName']),
|
'NAME_T': sanitize_slug(row['sName']),
|
||||||
}
|
}
|
||||||
for row in q_seria
|
for row in q_seria
|
||||||
]
|
]
|
||||||
@@ -87,8 +82,8 @@ def catalog_seria_info(
|
|||||||
try:
|
try:
|
||||||
seria_id = int(seria_id)
|
seria_id = int(seria_id)
|
||||||
q_seria = Seria_Info.objects.only("id", "kRoot_id", "sName").get(id=seria_id)
|
q_seria = Seria_Info.objects.only("id", "kRoot_id", "sName").get(id=seria_id)
|
||||||
if q_seria.id != q_seria.kRoot_id or seria_name_translit != pytils.translit.slugify(q_seria.sName):
|
if q_seria.id != q_seria.kRoot_id or seria_name_translit != sanitize_slug(q_seria.sName):
|
||||||
return redirect(f"/catalog/seria/{pytils.translit.slugify(q_seria.sName)}/all{seria_id}")
|
return redirect(f"/catalog/seria/{sanitize_slug(q_seria.sName)}/all{seria_id}")
|
||||||
except (ObjectDoesNotExist, ValueError):
|
except (ObjectDoesNotExist, ValueError):
|
||||||
return redirect("/catalog/")
|
return redirect("/catalog/")
|
||||||
|
|
||||||
@@ -295,7 +290,7 @@ def all_seria_nav(seria_id: int, q_seria) -> tuple[int, dict]:
|
|||||||
one_seria = {
|
one_seria = {
|
||||||
"SERIA_R": seria_name,
|
"SERIA_R": seria_name,
|
||||||
"ID2URL": seria_id_value,
|
"ID2URL": seria_id_value,
|
||||||
"SERIA_L": pytils.translit.slugify(seria_name),
|
"SERIA_L": sanitize_slug(seria_name),
|
||||||
}
|
}
|
||||||
if seria_id_value == seria_id:
|
if seria_id_value == seria_id:
|
||||||
# Изображение серии: используется в OG-image в шаблоне seria_info
|
# Изображение серии: используется в OG-image в шаблоне seria_info
|
||||||
@@ -309,7 +304,7 @@ def all_seria_nav(seria_id: int, q_seria) -> tuple[int, dict]:
|
|||||||
"THIS_SERIA_DESCRIPTION": seria_description,
|
"THIS_SERIA_DESCRIPTION": seria_description,
|
||||||
# ID и slug серии нужны для canonical URL и JSON-LD в шаблоне
|
# ID и slug серии нужны для canonical URL и JSON-LD в шаблоне
|
||||||
"THIS_SERIA_ID": seria_id_value,
|
"THIS_SERIA_ID": seria_id_value,
|
||||||
"THIS_SERIA_NAME_T": pytils.translit.slugify(seria_name),
|
"THIS_SERIA_NAME_T": sanitize_slug(seria_name),
|
||||||
# URL изображения серии для OG-тегов (путь относительно /media/)
|
# URL изображения серии для OG-тегов (путь относительно /media/)
|
||||||
"THIS_SERIA_IMAGE_URL": str(seria_image) if seria_image else "",
|
"THIS_SERIA_IMAGE_URL": str(seria_image) if seria_image else "",
|
||||||
})
|
})
|
||||||
@@ -417,7 +412,7 @@ def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> d
|
|||||||
seria_to_geo.append({"LATITUDE": latitude,
|
seria_to_geo.append({"LATITUDE": latitude,
|
||||||
"LONGITUDE": longitude,
|
"LONGITUDE": longitude,
|
||||||
"ADDR_ID": count["id"],
|
"ADDR_ID": count["id"],
|
||||||
"ADDR_LAT": pytils.translit.slugify(count["sAddress"]),
|
"ADDR_LAT": sanitize_slug(count["sAddress"]),
|
||||||
"ADDR_RUS": count["sAddress"],
|
"ADDR_RUS": count["sAddress"],
|
||||||
"SER_ID": count["kSeria_Link__kRoot_id"]
|
"SER_ID": count["kSeria_Link__kRoot_id"]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ from oknardia.models import (
|
|||||||
SetKit,
|
SetKit,
|
||||||
Win_MountDim,
|
Win_MountDim,
|
||||||
)
|
)
|
||||||
import pytils
|
from web.add_func import sanitize_slug
|
||||||
|
|
||||||
|
|
||||||
# Namespace схемы sitemap.xml по стандарту sitemaps.org.
|
# Namespace схемы sitemap.xml по стандарту sitemaps.org.
|
||||||
SITEMAP_XMLNS = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
SITEMAP_XMLNS = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
@@ -138,7 +137,7 @@ class BuildingOffersSitemap(Sitemap):
|
|||||||
if not root_id:
|
if not root_id:
|
||||||
continue
|
continue
|
||||||
for apart_id in apartments_by_root.get(root_id, []):
|
for apart_id in apartments_by_root.get(root_id, []):
|
||||||
yield (building.id, apart_id, pytils.translit.slugify(building.sAddress))
|
yield (building.id, apart_id, sanitize_slug(building.sAddress))
|
||||||
|
|
||||||
def location(self, item: tuple[int, int, str]) -> str:
|
def location(self, item: tuple[int, int, str]) -> str:
|
||||||
build_id, apart_id, address_slug = item
|
build_id, apart_id, address_slug = item
|
||||||
@@ -147,7 +146,7 @@ class BuildingOffersSitemap(Sitemap):
|
|||||||
building = Building_Info.objects.select_related('kSeria_Link__kRoot').get(id=build_id)
|
building = Building_Info.objects.select_related('kSeria_Link__kRoot').get(id=build_id)
|
||||||
seria = building.kSeria_Link.kRoot
|
seria = building.kSeria_Link.kRoot
|
||||||
seria_id = seria.id
|
seria_id = seria.id
|
||||||
seria_slug = pytils.translit.slugify((seria.sName or "").strip()).lower()
|
seria_slug = sanitize_slug((seria.sName or ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
# fallback на старый роутинг, если что-то пошло не так
|
# fallback на старый роутинг, если что-то пошло не так
|
||||||
return f"/{build_id}/{apart_id}/{address_slug}"
|
return f"/{build_id}/{apart_id}/{address_slug}"
|
||||||
@@ -250,7 +249,7 @@ class BlogPostSitemap(Sitemap):
|
|||||||
).only("id", "sPostHeader", "dPostDataModify")
|
).only("id", "sPostHeader", "dPostDataModify")
|
||||||
|
|
||||||
def location(self, item: BlogPosts) -> str:
|
def location(self, item: BlogPosts) -> str:
|
||||||
return f"/blogpost/{item.id}/{pytils.translit.slugify(item.sPostHeader).lower()}"
|
return f"/blogpost/{item.id}/{sanitize_slug(item.sPostHeader)}"
|
||||||
|
|
||||||
def lastmod(self, item: BlogPosts) -> date | datetime | None:
|
def lastmod(self, item: BlogPosts) -> date | datetime | None:
|
||||||
return item.dPostDataModify
|
return item.dPostDataModify
|
||||||
@@ -270,7 +269,7 @@ class ProfileManufactureSitemap(Sitemap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def location(self, item: dict) -> str:
|
def location(self, item: dict) -> str:
|
||||||
manufacturer_slug = pytils.translit.slugify(item["sProfileManufacturer"]).lower()
|
manufacturer_slug = sanitize_slug(item["sProfileManufacturer"])
|
||||||
return f"/catalog/profile/{item['first_id']}-{manufacturer_slug}"
|
return f"/catalog/profile/{item['first_id']}-{manufacturer_slug}"
|
||||||
|
|
||||||
def lastmod(self, item: dict) -> date | datetime | None:
|
def lastmod(self, item: dict) -> date | datetime | None:
|
||||||
@@ -287,8 +286,8 @@ class ProfileModelSitemap(Sitemap):
|
|||||||
return PVCprofiles.objects.only("id", "sProfileManufacturer", "sProfileName", "dProfileModify")
|
return PVCprofiles.objects.only("id", "sProfileManufacturer", "sProfileName", "dProfileModify")
|
||||||
|
|
||||||
def location(self, item: PVCprofiles) -> str:
|
def location(self, item: PVCprofiles) -> str:
|
||||||
manufacturer_slug = pytils.translit.slugify(item.sProfileManufacturer).lower()
|
manufacturer_slug = sanitize_slug(item.sProfileManufacturer)
|
||||||
model_slug = pytils.translit.slugify(item.sProfileName).lower()
|
model_slug = sanitize_slug(item.sProfileName)
|
||||||
# Исторически канонический URL использует id модели и в сегменте manufacturer_id, и в segment model_id.
|
# Исторически канонический URL использует id модели и в сегменте manufacturer_id, и в segment model_id.
|
||||||
return f"/catalog/profile/{item.id}-{manufacturer_slug}/{item.id}-{model_slug}"
|
return f"/catalog/profile/{item.id}-{manufacturer_slug}/{item.id}-{model_slug}"
|
||||||
|
|
||||||
@@ -308,7 +307,7 @@ class SeriaDetailSitemap(Sitemap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def location(self, item: Seria_Info) -> str:
|
def location(self, item: Seria_Info) -> str:
|
||||||
return f"/catalog/seria/{pytils.translit.slugify(item.sName).lower()}/all{item.id}"
|
return f"/catalog/seria/{sanitize_slug(item.sName)}/all{item.id}"
|
||||||
|
|
||||||
def lastmod(self, item: Seria_Info) -> date | datetime | None:
|
def lastmod(self, item: Seria_Info) -> date | datetime | None:
|
||||||
return item.dSeriaInfoModify
|
return item.dSeriaInfoModify
|
||||||
@@ -329,7 +328,7 @@ class CompanyDetailSitemap(Sitemap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def location(self, item: MerchantBrand) -> str:
|
def location(self, item: MerchantBrand) -> str:
|
||||||
return f"/catalog/company/{item.id}-{pytils.translit.slugify(item.sMerchantName).lower()}"
|
return f"/catalog/company/{item.id}-{sanitize_slug(item.sMerchantName)}"
|
||||||
|
|
||||||
def lastmod(self, item: MerchantBrand) -> date | datetime | None:
|
def lastmod(self, item: MerchantBrand) -> date | datetime | None:
|
||||||
return getattr(item, "last_offer_modify", None) or getattr(item, "last_office_modify", None)
|
return getattr(item, "last_offer_modify", None) or getattr(item, "last_office_modify", None)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytils
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
@@ -11,6 +10,7 @@ from django.test import RequestFactory
|
|||||||
|
|
||||||
from oknardia.models import Seria_Info
|
from oknardia.models import Seria_Info
|
||||||
from web import catalog_series
|
from web import catalog_series
|
||||||
|
from web.add_func import sanitize_slug
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -76,7 +76,7 @@ class Command(BaseCommand):
|
|||||||
if target_file.exists():
|
if target_file.exists():
|
||||||
target_file.unlink()
|
target_file.unlink()
|
||||||
|
|
||||||
slug = pytils.translit.slugify(seria.sName)
|
slug = sanitize_slug(seria.sName)
|
||||||
request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}")
|
request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}")
|
||||||
|
|
||||||
# В команде принудительно включаем «production-mode» для вьюхи,
|
# В команде принудительно включаем «production-mode» для вьюхи,
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ from oknardia.models import (
|
|||||||
)
|
)
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list, 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 get_rating_set_for_stars, get_flaps_for_big_pictures, get_flaps_for_mini_pictures, \
|
||||||
get_geo_distance
|
get_geo_distance, sanitize_slug
|
||||||
import django.utils.dateformat
|
import django.utils.dateformat
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
@@ -26,11 +26,6 @@ from types import SimpleNamespace
|
|||||||
import pytils
|
import pytils
|
||||||
|
|
||||||
|
|
||||||
def _slugify_lower(value: str | None) -> str:
|
|
||||||
"""Транслитерирует строку в slug и всегда приводит к нижнему регистру."""
|
|
||||||
return pytils.translit.slugify((value or "").strip()).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def _one_win_price_canonical_path(win_width_mm: int | str, win_height_mm: int | str, win_id: int | str) -> str:
|
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)}/"
|
return f"/catalog/standard_opening/price-{int(win_width_mm)}x{int(win_height_mm)}mm-tip{int(win_id)}/"
|
||||||
@@ -271,9 +266,9 @@ def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_long
|
|||||||
'GLAZING_TONING': offer.sGlazingToning,
|
'GLAZING_TONING': offer.sGlazingToning,
|
||||||
'PVC_ID': offer.pwc_id,
|
'PVC_ID': offer.pwc_id,
|
||||||
'PVC_NAME': offer.sProfileName,
|
'PVC_NAME': offer.sProfileName,
|
||||||
'PVC_NAME_T': _slugify_lower(offer.sProfileName),
|
'PVC_NAME_T': sanitize_slug(offer.sProfileName),
|
||||||
'PVC_MANUFACTURER': offer.sProfileManufacturer,
|
'PVC_MANUFACTURER': offer.sProfileManufacturer,
|
||||||
'PVC_MANUFACTURER_T': _slugify_lower(offer.sProfileManufacturer),
|
'PVC_MANUFACTURER_T': sanitize_slug(offer.sProfileManufacturer),
|
||||||
'PVC_SEAL': offer.sProfileSealDescription,
|
'PVC_SEAL': offer.sProfileSealDescription,
|
||||||
'SETS_CLIMATE_CONTROL': offer.sSetClimateControl,
|
'SETS_CLIMATE_CONTROL': offer.sSetClimateControl,
|
||||||
'SETS_SILL': offer.sSetSill,
|
'SETS_SILL': offer.sSetSill,
|
||||||
@@ -530,9 +525,9 @@ def report_price_frame(apartment_id: int, mount_dim_per_offer: int, address_long
|
|||||||
'GLAZING_TONING': i2.sGlazingToning,
|
'GLAZING_TONING': i2.sGlazingToning,
|
||||||
'PVC_ID': i2.pwc_id,
|
'PVC_ID': i2.pwc_id,
|
||||||
'PVC_NAME': i2.sProfileName,
|
'PVC_NAME': i2.sProfileName,
|
||||||
'PVC_NAME_T': _slugify_lower(i2.sProfileName),
|
'PVC_NAME_T': sanitize_slug(i2.sProfileName),
|
||||||
'PVC_MANUFACTURER': i2.sProfileManufacturer,
|
'PVC_MANUFACTURER': i2.sProfileManufacturer,
|
||||||
'PVC_MANUFACTURER_T': _slugify_lower(i2.sProfileManufacturer),
|
'PVC_MANUFACTURER_T': sanitize_slug(i2.sProfileManufacturer),
|
||||||
'PVC_SEAL': i2.sProfileSealDescription,
|
'PVC_SEAL': i2.sProfileSealDescription,
|
||||||
'SETS_CLIMATE_CONTROL': i2.sSetClimateControl,
|
'SETS_CLIMATE_CONTROL': i2.sSetClimateControl,
|
||||||
'SETS_SILL': i2.sSetSill,
|
'SETS_SILL': i2.sSetSill,
|
||||||
@@ -710,7 +705,7 @@ def report_one_win_price(request: HttpRequest,
|
|||||||
list_seria_for_win.append(SimpleNamespace(
|
list_seria_for_win.append(SimpleNamespace(
|
||||||
id=seria_item['kApartment__kSeria__id'],
|
id=seria_item['kApartment__kSeria__id'],
|
||||||
sName=seria_name,
|
sName=seria_name,
|
||||||
sNameLat=_slugify_lower(seria_name),
|
sNameLat=sanitize_slug(seria_name),
|
||||||
num_variation_of_apartment=pytils.numeral.sum_string(
|
num_variation_of_apartment=pytils.numeral.sum_string(
|
||||||
seria_item['num_variation_of_apartment'],
|
seria_item['num_variation_of_apartment'],
|
||||||
pytils.numeral.MALE,
|
pytils.numeral.MALE,
|
||||||
@@ -793,7 +788,7 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
|
|||||||
|
|
||||||
# если кто-то нахимичит ID квартиры не для этого дома, то сделаем так, что он будет от этого дома!
|
# если кто-то нахимичит ID квартиры не для этого дома, то сделаем так, что он будет от этого дома!
|
||||||
apart_inside = any(ap.id == apart_id for ap in list_apart)
|
apart_inside = any(ap.id == apart_id for ap in list_apart)
|
||||||
address_slug = _slugify_lower(building.sAddress)
|
address_slug = sanitize_slug(building.sAddress)
|
||||||
if not apart_inside or slug != address_slug:
|
if not apart_inside or slug != address_slug:
|
||||||
# Переадресация 302, если с apart_id (ID-квартиры нахимичили) или slug-ом.
|
# Переадресация 302, если с apart_id (ID-квартиры нахимичили) или slug-ом.
|
||||||
# Нужно для склейки парных URL в поисковиках
|
# Нужно для склейки парных URL в поисковиках
|
||||||
@@ -867,7 +862,7 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
|
|||||||
|
|
||||||
# узнаем базовую серию дома
|
# узнаем базовую серию дома
|
||||||
q_base_seria = building.kSeria_Link.kRoot
|
q_base_seria = building.kSeria_Link.kRoot
|
||||||
base_seria_slug = _slugify_lower(q_base_seria.sName)
|
base_seria_slug = sanitize_slug(q_base_seria.sName)
|
||||||
to_template.update({'BASE_SERIA': q_base_seria.sName,
|
to_template.update({'BASE_SERIA': q_base_seria.sName,
|
||||||
'BASE_SERIA_LAT': base_seria_slug,
|
'BASE_SERIA_LAT': base_seria_slug,
|
||||||
'BASE_SERIA_ID': q_base_seria.id})
|
'BASE_SERIA_ID': q_base_seria.id})
|
||||||
@@ -1015,8 +1010,8 @@ def report_price_new(request, seria_id, seria_slug, apart_id, address_id, addres
|
|||||||
except Exception:
|
except Exception:
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
# Проверяем slug'и, если не совпадает — делаем 301 на канонический URL (новый формат)
|
# Проверяем slug'и, если не совпадает — делаем 301 на канонический URL (новый формат)
|
||||||
seria_slug_real = pytils.translit.slugify((seria.sName or "").strip()).lower()
|
seria_slug_real = sanitize_slug((seria.sName or "").strip()).lower()
|
||||||
address_slug_real = pytils.translit.slugify((building.sAddress or "").strip()).lower()
|
address_slug_real = sanitize_slug((building.sAddress or "").strip()).lower()
|
||||||
if seria_slug != seria_slug_real or address_slug != address_slug_real:
|
if seria_slug != seria_slug_real or address_slug != address_slug_real:
|
||||||
# Новый формат: /price/seriaID<seria_id>--<seria_slug>/appartAD<apart_id>/addressID<address_id>--<address_slug>/
|
# Новый формат: /price/seriaID<seria_id>--<seria_slug>/appartAD<apart_id>/addressID<address_id>--<address_slug>/
|
||||||
return redirect(f"/price/seriaID{seria_id}--{seria_slug_real}/appartID{apart_id}/addressID{address_id}--{address_slug_real}/", permanent=True)
|
return redirect(f"/price/seriaID{seria_id}--{seria_slug_real}/appartID{apart_id}/addressID{address_id}--{address_slug_real}/", permanent=True)
|
||||||
@@ -1037,10 +1032,7 @@ def report_price_legacy_redirect(request, build_id, apart_id, slug):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
import pytils
|
import pytils
|
||||||
seria_slug = pytils.translit.slugify((seria.sName or "").strip()).lower()
|
seria_slug = sanitize_slug((seria.sName or "").strip()).lower()
|
||||||
address_slug = pytils.translit.slugify((building.sAddress or "").strip()).lower()
|
address_slug = sanitize_slug((building.sAddress or "").strip()).lower()
|
||||||
# Новый формат: /price/seriaID<seria_id>--<seria_slug>/appartID<apart_id>/addressID<build_id>--<address_slug>/
|
# Новый формат: /price/seriaID<seria_id>--<seria_slug>/appartID<apart_id>/addressID<build_id>--<address_slug>/
|
||||||
return redirect(f"/price/seriaID{seria.id}--{seria_slug}/appartID{apart_id}/addressID{build_id}--{address_slug}/", permanent=True)
|
return redirect(f"/price/seriaID{seria.id}--{seria_slug}/appartID{apart_id}/addressID{build_id}--{address_slug}/", permanent=True)
|
||||||
seria_slug = pytils.translit.slugify((seria.sName or "").strip()).lower()
|
|
||||||
address_slug = pytils.translit.slugify((building.sAddress or "").strip()).lower()
|
|
||||||
return redirect(f"/price/seriaID{seria.id}--{seria_slug}/appartID{apart_id}/addressID{build_id}--{address_slug}/", permanent=True)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils import timezone
|
|||||||
from django.db.models import F, Q, ExpressionWrapper, BooleanField, Max, Count, Avg
|
from django.db.models import F, Q, ExpressionWrapper, BooleanField, Max, Count, Avg
|
||||||
from oknardia.models import LogVisitPriceReport, SetKit
|
from oknardia.models import LogVisitPriceReport, SetKit
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from web.add_func import normalize, get_rating_set_for_stars, sum_through
|
from web.add_func import normalize, get_rating_set_for_stars, sum_through, sanitize_slug
|
||||||
# from time import time
|
# from time import time
|
||||||
import django.utils.dateformat
|
import django.utils.dateformat
|
||||||
import time
|
import time
|
||||||
@@ -290,7 +290,7 @@ def compare_offers(request: HttpRequest, to_compare: str = "1,2") -> HttpRespons
|
|||||||
"MERCHANT": i.sMerchantName,
|
"MERCHANT": i.sMerchantName,
|
||||||
"MERCHANT_ID": i.MERCHANT_ID,
|
"MERCHANT_ID": i.MERCHANT_ID,
|
||||||
"IS_COMMERCIAL": i.bCommercial,
|
"IS_COMMERCIAL": i.bCommercial,
|
||||||
"MERCHANT_T": pytils.translit.slugify(i.sMerchantName),
|
"MERCHANT_T": sanitize_slug(i.sMerchantName),
|
||||||
"MERCHANT_URL": i.sMerchantMainURL,
|
"MERCHANT_URL": i.sMerchantMainURL,
|
||||||
"MERCHANT_URL_SHOT": re.sub(r"(?:^https?://|/$|www\.)", "", i.sMerchantMainURL),
|
"MERCHANT_URL_SHOT": re.sub(r"(?:^https?://|/$|www\.)", "", i.sMerchantMainURL),
|
||||||
"SET_NAME": i.sSetName,
|
"SET_NAME": i.sSetName,
|
||||||
@@ -300,9 +300,9 @@ def compare_offers(request: HttpRequest, to_compare: str = "1,2") -> HttpRespons
|
|||||||
"RATING_SET_COLOR": rating_set_color,
|
"RATING_SET_COLOR": rating_set_color,
|
||||||
"PROFILE_ID": i.PROFILE_ID,
|
"PROFILE_ID": i.PROFILE_ID,
|
||||||
"PROFILE_NAME": i.sProfileName,
|
"PROFILE_NAME": i.sProfileName,
|
||||||
"PROFILE_NAME_T": pytils.translit.slugify(i.sProfileName),
|
"PROFILE_NAME_T": sanitize_slug(i.sProfileName),
|
||||||
"PROFILE_MANUFACTURER": i.sProfileManufacturer,
|
"PROFILE_MANUFACTURER": i.sProfileManufacturer,
|
||||||
"PROFILE_MANUFACTURER_T": pytils.translit.slugify(i.sProfileManufacturer),
|
"PROFILE_MANUFACTURER_T": sanitize_slug(i.sProfileManufacturer),
|
||||||
"PROFILE_NUM_COLOR": i.sProfileColor,
|
"PROFILE_NUM_COLOR": i.sProfileColor,
|
||||||
"PROFILE_NUM_CAMERAS": i.iProfileCameras, # Число камер рамы/створки
|
"PROFILE_NUM_CAMERAS": i.iProfileCameras, # Число камер рамы/створки
|
||||||
"PROFILE_NUM_CAMERAS_COLOR": _color_hi(profile_num_cameras, min_cameras, max_cameras, threshold=1),
|
"PROFILE_NUM_CAMERAS_COLOR": _color_hi(profile_num_cameras, min_cameras, max_cameras, threshold=1),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from django.shortcuts import render, redirect
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from oknardia.models import PVCprofiles
|
from oknardia.models import PVCprofiles
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from web.add_func import normalize, get_rating_set_for_stars
|
from web.add_func import normalize, get_rating_set_for_stars, sanitize_slug
|
||||||
from time import time
|
from time import time
|
||||||
import json
|
import json
|
||||||
import pytils
|
import pytils
|
||||||
@@ -73,9 +73,9 @@ def profiles_rating(request: HttpRequest) -> HttpResponse:
|
|||||||
"ID": profile.id,
|
"ID": profile.id,
|
||||||
"R_REAL": rating_real,
|
"R_REAL": rating_real,
|
||||||
"BRAND": profile.sProfileManufacturer,
|
"BRAND": profile.sProfileManufacturer,
|
||||||
"BRAND_URL": pytils.translit.slugify(profile.sProfileManufacturer),
|
"BRAND_URL": sanitize_slug(profile.sProfileManufacturer),
|
||||||
"NAME": profile.sProfileName,
|
"NAME": profile.sProfileName,
|
||||||
"NAME_URL": pytils.translit.slugify(profile.sProfileName),
|
"NAME_URL": sanitize_slug(profile.sProfileName),
|
||||||
"K_ARR": k_arr,
|
"K_ARR": k_arr,
|
||||||
"RATING_STAR": get_rating_set_for_stars(profile.fProfileRating),
|
"RATING_STAR": get_rating_set_for_stars(profile.fProfileRating),
|
||||||
"RATING_N": profile.fProfileRating,
|
"RATING_N": profile.fProfileRating,
|
||||||
|
|||||||
@@ -6,12 +6,8 @@ from django.db.models import ExpressionWrapper, FloatField, F, Count
|
|||||||
from django.db.models.functions import Abs
|
from django.db.models.functions import Abs
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from oknardia.models import Seria_Info, Building_Info, Apartment_Type
|
from oknardia.models import Seria_Info, Building_Info, Apartment_Type
|
||||||
from web.add_func import get_yandex_geocode_by_address, get_geo_distance
|
from web.add_func import get_yandex_geocode_by_address, get_geo_distance, sanitize_slug
|
||||||
import json
|
|
||||||
import datetime
|
|
||||||
import time
|
import time
|
||||||
import pytils
|
|
||||||
|
|
||||||
# from django.core.context_processors import csrf
|
# from django.core.context_processors import csrf
|
||||||
|
|
||||||
|
|
||||||
@@ -240,9 +236,9 @@ def get_address(request: HttpRequest) -> HttpResponse:
|
|||||||
to_template.update({
|
to_template.update({
|
||||||
'SERIA_BASE': q1.sName if q1 else "",
|
'SERIA_BASE': q1.sName if q1 else "",
|
||||||
'BASE_SERIA_ID': seria_root.id if seria_root else "",
|
'BASE_SERIA_ID': seria_root.id if seria_root else "",
|
||||||
'BASE_SERIA_LAT': pytils.translit.slugify((seria_root.sName or "").strip()).lower() if seria_root else "",
|
'BASE_SERIA_LAT': sanitize_slug((seria_root.sName or "").strip()) if seria_root else "",
|
||||||
'addr': addr,
|
'addr': addr,
|
||||||
'addr_T': pytils.translit.slugify(addr),
|
'addr_T': sanitize_slug(addr),
|
||||||
'ticks': float(time.perf_counter() - time_start),
|
'ticks': float(time.perf_counter() - time_start),
|
||||||
})
|
})
|
||||||
return render(request, "popup/popup_show_apartment_variants.html", to_template)
|
return render(request, "popup/popup_show_apartment_variants.html", to_template)
|
||||||
|
|||||||
Reference in New Issue
Block a user