mod: унифицированная slug-офикация

This commit is contained in:
2026-05-10 23:34:00 +03:00
parent 1b2666f3d7
commit dc379fa8da
13 changed files with 176 additions and 143 deletions

View File

@@ -3,42 +3,117 @@ __author__ = 'Sergei Erjemin'
from PIL import Image, ImageDraw
from oknardia.settings import *
from pytils.translit import slugify
import os
import math
import re
import html
import urllib3
import xml.dom.minidom
def safe_html_spec_symbols(s: str) -> str:
""" Очистка строки от HTML-разметки типографа
""" Очистка строки от HTML-разметки и получение чистого текста.
:param s: str -- строка которую надо очистить
:return: str: str -- очищенная строка
Функция удаляет HTML-теги, содержимое исключённых тегов (script, style, object, embed, applet,
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
result = s.replace('­', '­')
result = result.replace('<span class="laquo">', '')
result = result.replace('<span style="margin-right:0.44em;">', '')
result = result.replace('<span style="margin-left:-0.44em;">', '')
result = result.replace('<span class="raquo">', '')
result = result.replace('<span class="point">', '')
result = result.replace('<span class="thinsp">', ' ')
result = result.replace('<span class="ensp">', '')
result = result.replace('</span>', '')
result = result.replace('&nbsp;', ' ')
result = result.replace('&laquo;', '«')
result = result.replace('&raquo;', '»')
result = result.replace('&hellip;', '')
result = result.replace('<nobr>', '')
result = result.replace('</nobr>', '')
result = result.replace('&mdash;', '')
result = result.replace('&#8470;', '')
result = result.replace('<br />', ' ')
result = result.replace('<br>', ' ')
# Шаг 1: Удаляем содержимое "опасных" и невидимых тегов
# Опасные: script, object, embed, applet, iframe, svg, canvas
# Техническое содержимое: style, code, kbd, pre, var, samp, output, noscript
# Формы: form, input, button, textarea, select
# Служебные: meta, link, base, title, head, body, track, source, picture
# Используем флаг IGNORECASE и DOTALL для работы с многострочным контентом
result = re.sub(
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>',
'',
s,
flags=re.IGNORECASE | re.DOTALL
)
# Удаляем самозакрывающиеся теги (что-то типа <input/>, <embed/>, и т.д.)
result = re.sub(
r'<(input|embed|meta|link|base|track|source|img)(?:\s[^>]*)?/>',
'',
result,
flags=re.IGNORECASE
)
# Шаг 2: Удаляем все остальные HTML-теги (в т.ч. самозакрывающиеся)
result = re.sub(r'<[^>]+>', '', result)
# Шаг 3: Заменяем HTML-мнемоники на Unicode-символы (включая числовые и именованные)
# html.unescape() обрабатывает: &nbsp;, &lt;, &#8470;, &#x20AC; и т.д.
result = html.unescape(result)
# Шаг 4: Очищаем множественные пробелы (в т.ч. табуляцию и переводы строк)
result = re.sub(r'\s+', ' ', result)
# Шаг 5: Убираем пробелы в начале и конце строки
result = result.strip()
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(' Тест &mdash; 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):
# return translit(re.sub(
# r'<[\s\S]*?>', '', re.sub(r'&[\S]*?;', '-', RusString)

View File

@@ -6,10 +6,9 @@ from django.core.exceptions import ObjectDoesNotExist
from oknardia.models import BlogPosts
from oknardia.settings import *
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
import re
import pytils
from oknardia.settings import *
@@ -88,7 +87,7 @@ def blog_list_posts(request: HttpRequest, page: str = "0") -> HttpResponse:
'PUB_DAT': post.dPostDataBegin,
'HEADER': 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,
'USER_STATUS': post.kBlogAuthorUser.get_sUserStatus_display(),
'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,
'PUB_MODIFY': q.dPostDataModify,
'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_AVATAR': q.kBlogAuthorUser.sUserAvatarImg,
'USER_TITLE': q.kBlogAuthorUser.sUserJobTitle,
'USER_FROM_ID_OFFICE': q.kBlogAuthorUser.kMerchantOffice,
'CONTENT': re.sub(r'<cut[\s\S]*?>', '', q.sPostContent, 0, re.IGNORECASE)})
to_template.update({'TIZER': safe_html_spec_symbols(
re.sub('<script[\s\S]*?</script>|<style[\s\S]*?</style>|<iframe[\s\S]*?</iframe>',
'', to_template["CONTENT"], 0, re.IGNORECASE))})
content = to_template.get('CONTENT', '')
to_template.update({'TIZER': sanitize_slug(str(content))})
# получаем следующую по дате запись
try:
q1 = BlogPosts.objects.filter(dPostDataBegin__gt=q.dPostDataBegin, dPostDataBegin__lt=timezone.now(),
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})
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
to_template.update({'FORW_DISABLE': True})
@@ -181,7 +179,7 @@ def blog_post(request: HttpRequest, post_id: str = "0", page_back: str = None) -
try:
q1 = BlogPosts.objects.filter(dPostDataBegin__lt=q.dPostDataBegin, bPublished=True,
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})
except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist):
to_template.update({'BACK_DISABLE': True})

View File

@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
import time
import pytils.translit
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
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
@@ -32,7 +31,7 @@ def catalog_sets(request: HttpRequest) -> HttpResponse:
Для каждого набора собирается dict с полями набора, профиля, стеклопакета и компании-установщика.
Цепочка FK: SetKit.kSet2User → OurUser.kMerchantOffice → MerchantOffice.kMerchantName (MerchantBrand).
Слаги URL формируются через pytils.translit.slugify.
Слаги URL формируются через sanitize_slug.
:param request: HttpRequest -- входящий http-запрос
:return response: HttpResponse -- исходящий http-ответ
@@ -69,15 +68,13 @@ def catalog_sets(request: HttpRequest) -> HttpResponse:
'glazing': glazing,
# компания-установщик
'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_logo': str(brand.pMerchantLogo) if brand and brand.pMerchantLogo else "",
'merchant_url': brand.sMerchantMainURL if brand else "",
# слаги для ссылок на профиль в каталоге профилей
'profile_manufacturer_slug': pytils.translit.slugify(
profile.sProfileManufacturer) if profile else "",
'profile_slug': pytils.translit.slugify(
profile.sProfileName) if profile else "",
'profile_manufacturer_slug': sanitize_slug(profile.sProfileManufacturer) if profile else "",
'profile_slug': sanitize_slug(profile.sProfileName) if profile else "",
})
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)
q_seria = Seria_Info.objects.get(id=seria_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):
return redirect("/catalog/seria")
return redirect("/catalog/seria")

View File

@@ -17,8 +17,8 @@ from oknardia.models import (
SetKit,
PriceOffer,
)
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
from web.add_func import get_rating_set_for_stars
from web.report1 import get_last_all_user_visit_list
from web.add_func import get_rating_set_for_stars, sanitize_slug
import django.utils.dateformat
import time
import random
@@ -131,12 +131,10 @@ def _format_company_for_template(company_data: dict) -> dict:
dict: Отформатированные данные компании
"""
formatted = company_data.copy()
# Вычисляем звёзды на основе рейтинга
formatted['STARS'] = get_rating_set_for_stars(
formatted['RatingAVG']
)
# Применяем правильные формы множественного числа
formatted['NumSets'] = pytils.numeral.get_plural(
formatted['NumSets'],
@@ -146,7 +144,6 @@ def _format_company_for_template(company_data: dict) -> dict:
formatted['NumOffers'],
"вариант, варианта, вариантов"
)
# Конвертируем время последнего обновления в читаемый формат
if formatted['lastUpdate']:
timestamp = int(
@@ -158,12 +155,8 @@ def _format_company_for_template(company_data: dict) -> dict:
formatted['lastUpdate'] = pytils.dt.distance_of_time_in_words(
timestamp
)
# Генерируем slug из имени компании для URL
formatted['sMerchantMainURL'] = pytils.translit.slugify(
formatted['sMerchantName']
)
formatted['sMerchantMainURL'] = sanitize_slug(formatted['sMerchantName'])
return formatted
@@ -387,11 +380,11 @@ def _format_set_for_template(set_data: dict, empty_values: list) -> dict:
'iProfileCameras': profile.iProfileCameras,
'sProfileName': {
'NAME': profile.sProfileName,
'NAME_T': pytils.translit.slugify(profile.sProfileName)
'NAME_T': sanitize_slug(profile.sProfileName)
},
'sProfileManufacturer': {
'NAME': profile.sProfileManufacturer,
'NAME_T': pytils.translit.slugify(profile.sProfileManufacturer)
'NAME_T': sanitize_slug(profile.sProfileManufacturer)
},
'sProfileColor': profile.sProfileColor,
'sProfileSealDescription': profile.sProfileSealDescription,
@@ -482,7 +475,7 @@ def catalog_company_detail(
raise Http404("Компания не найдена")
# Проверяем что slug совпадает (для SEO и красивых URL)
actual_slug = pytils.translit.slugify(company.sMerchantName)
actual_slug = sanitize_slug(company.sMerchantName)
if actual_slug != company_name_slug:
return redirect(
f'/catalog/company/{company_id_int}-{actual_slug}'

View File

@@ -3,20 +3,14 @@ from django.db.models import F
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse
from oknardia.models import MountDim2Apartment
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_list
from web.add_func import get_flaps_for_mini_pictures
from web.report1 import get_last_all_user_visit_list
from web.add_func import get_flaps_for_mini_pictures, sanitize_slug
import time
import pytils
from typing import Any
from itertools import groupby
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:
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
to_template.update({
@@ -73,7 +67,7 @@ def standard_opening(request: HttpRequest) -> HttpResponse:
serias_for_opening = [
{
'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'],
}
for row in rows_for_opening

View File

@@ -7,8 +7,8 @@ from django.shortcuts import render, redirect
from django.http import HttpRequest, HttpResponse
from oknardia.settings import *
from oknardia.models import Catalog2Profile, PVCprofiles, PriceOffer
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
from web.report1 import get_last_all_user_visit_list
from web.add_func import normalize, get_rating_set_for_stars, sanitize_slug
import time
import json
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:
"""Преобразует ORM-строку с данными партнёра в словарь для шаблона."""
merchant_name = row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"]
return {
"MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"],
"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_OFFERS": row["offers_by_merchant"],
}
@@ -40,7 +34,7 @@ def _profile_row_to_dict(profile: dict) -> dict:
return {
"PROFILE_NAME": profile["sProfileBriefDescription"],
"PROFILE_ID": profile["id"],
"PROFILE_URL": make_slug(profile["sProfileName"]),
"PROFILE_URL": sanitize_slug(profile["sProfileName"]),
"PROFILE_RATING": 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({
"PROF_MAN_ID": profile["id"],
"PROF_MAN": profile["sProfileManufacturer"],
"PROF_MAN_T": make_slug(profile["sProfileManufacturer"]),
"PROF_MAN_T": sanitize_slug(profile["sProfileManufacturer"]),
"PROF_MAN_LIST": [{
"PROF_NAME_ID": profile["id"],
"PROF_NAME": profile["sProfileBriefDescription"],
"PROF_NAME_T": make_slug(profile["sProfileName"]),
"PROF_NAME_T": sanitize_slug(profile["sProfileName"]),
}]
})
else:
@@ -106,7 +100,7 @@ def catalog_profile(request: HttpRequest) -> HttpResponse:
list_profile_manufactures[-1]["PROF_MAN_LIST"].append({
"PROF_NAME_ID": profile["id"],
"PROF_NAME": profile["sProfileBriefDescription"],
"PROF_NAME_T": make_slug(profile["sProfileName"]),
"PROF_NAME_T": sanitize_slug(profile["sProfileName"]),
})
to_template.update({
@@ -128,17 +122,17 @@ def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture
:param request: HttpRequest -- входящий http-запрос
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
:param manufacture_name: название производителя (транслитерированное pytils.translit.slugify())
:param manufacture_name: название производителя (транслитерированное sanitize_slug())
:param model_id: id модели (марки) профиля
:param model_name: модель (марка) профиля (транслитерированное pytils.translit.slugify(sProfileName))
:param model_name: модель (марка) профиля (транслитерированное sanitize_slug(sProfileName))
:return response: HttpResponse -- исходящий http-ответ
"""
time_start = time.perf_counter()
manufacture_id = int(manufacture_id)
model_id = int(model_id)
q_pvc_by_id = PVCprofiles.objects.get(id=model_id)
manufacturer_slug = pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)
model_slug = pytils.translit.slugify(q_pvc_by_id.sProfileName)
manufacturer_slug = sanitize_slug(q_pvc_by_id.sProfileManufacturer)
model_slug = sanitize_slug(q_pvc_by_id.sProfileName)
if manufacturer_slug != manufacture_name \
or model_slug != model_name \
or manufacture_id != model_id:
@@ -268,21 +262,21 @@ def catalog_profile_manufacture(request: HttpRequest, manufacture_id: int, manuf
:param request: HttpRequest -- входящий http-запрос
:param manufacture_id: id профиля. Предполагается, что это первый id при сортировке по sProfileBriefDescription
:param manufacture_name: название производителя (транслитерированное pytils.translit.slugify())
:param manufacture_name: название производителя (транслитерированное sanitize_slug())
:return response: HttpResponse -- исходящий http-ответ
"""
time_start = time.perf_counter()
manufacture_id = int(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}-'
f'{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}')
f'{sanitize_slug(q_pvc_by_id.sProfileManufacturer)}')
else:
q_pvc_by_id = PVCprofiles.objects.order_by('id') \
.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer).first()
if q_pvc_by_id.id != manufacture_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,
'CATALOG_MAN2URL': manufacture_name,
'CATALOG_URL': f"{manufacture_id}-{manufacture_name}"}

View File

@@ -14,18 +14,13 @@ from oknardia.models import (
Building_Info,
)
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 os
import math
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:
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
to_template.update({
@@ -54,7 +49,7 @@ def catalog_seria(request: HttpRequest) -> HttpResponse:
'ID': row['id'],
'URL': row['sURL2IMG'],
'NAME': row['sName'],
'NAME_T': _make_slug(row['sName']),
'NAME_T': sanitize_slug(row['sName']),
}
for row in q_seria
]
@@ -87,8 +82,8 @@ def catalog_seria_info(
try:
seria_id = int(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):
return redirect(f"/catalog/seria/{pytils.translit.slugify(q_seria.sName)}/all{seria_id}")
if q_seria.id != q_seria.kRoot_id or seria_name_translit != sanitize_slug(q_seria.sName):
return redirect(f"/catalog/seria/{sanitize_slug(q_seria.sName)}/all{seria_id}")
except (ObjectDoesNotExist, ValueError):
return redirect("/catalog/")
@@ -295,7 +290,7 @@ def all_seria_nav(seria_id: int, q_seria) -> tuple[int, dict]:
one_seria = {
"SERIA_R": seria_name,
"ID2URL": seria_id_value,
"SERIA_L": pytils.translit.slugify(seria_name),
"SERIA_L": sanitize_slug(seria_name),
}
if seria_id_value == seria_id:
# Изображение серии: используется в 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,
# ID и slug серии нужны для canonical URL и JSON-LD в шаблоне
"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/)
"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,
"LONGITUDE": longitude,
"ADDR_ID": count["id"],
"ADDR_LAT": pytils.translit.slugify(count["sAddress"]),
"ADDR_LAT": sanitize_slug(count["sAddress"]),
"ADDR_RUS": count["sAddress"],
"SER_ID": count["kSeria_Link__kRoot_id"]
})

View File

@@ -36,8 +36,7 @@ from oknardia.models import (
SetKit,
Win_MountDim,
)
import pytils
from web.add_func import sanitize_slug
# Namespace схемы sitemap.xml по стандарту sitemaps.org.
SITEMAP_XMLNS = "http://www.sitemaps.org/schemas/sitemap/0.9"
@@ -138,7 +137,7 @@ class BuildingOffersSitemap(Sitemap):
if not root_id:
continue
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:
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)
seria = building.kSeria_Link.kRoot
seria_id = seria.id
seria_slug = pytils.translit.slugify((seria.sName or "").strip()).lower()
seria_slug = sanitize_slug((seria.sName or ""))
except Exception:
# fallback на старый роутинг, если что-то пошло не так
return f"/{build_id}/{apart_id}/{address_slug}"
@@ -250,7 +249,7 @@ class BlogPostSitemap(Sitemap):
).only("id", "sPostHeader", "dPostDataModify")
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:
return item.dPostDataModify
@@ -270,7 +269,7 @@ class ProfileManufactureSitemap(Sitemap):
)
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}"
def lastmod(self, item: dict) -> date | datetime | None:
@@ -287,8 +286,8 @@ class ProfileModelSitemap(Sitemap):
return PVCprofiles.objects.only("id", "sProfileManufacturer", "sProfileName", "dProfileModify")
def location(self, item: PVCprofiles) -> str:
manufacturer_slug = pytils.translit.slugify(item.sProfileManufacturer).lower()
model_slug = pytils.translit.slugify(item.sProfileName).lower()
manufacturer_slug = sanitize_slug(item.sProfileManufacturer)
model_slug = sanitize_slug(item.sProfileName)
# Исторически канонический URL использует id модели и в сегменте manufacturer_id, и в segment model_id.
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:
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:
return item.dSeriaInfoModify
@@ -329,7 +328,7 @@ class CompanyDetailSitemap(Sitemap):
)
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:
return getattr(item, "last_offer_modify", None) or getattr(item, "last_office_modify", None)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from pathlib import Path
import pytils
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db.models import F
@@ -11,6 +10,7 @@ from django.test import RequestFactory
from oknardia.models import Seria_Info
from web import catalog_series
from web.add_func import sanitize_slug
class Command(BaseCommand):
@@ -76,7 +76,7 @@ class Command(BaseCommand):
if target_file.exists():
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}")
# В команде принудительно включаем «production-mode» для вьюхи,

View File

@@ -15,8 +15,8 @@ from oknardia.models import (
)
from oknardia.settings import *
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, \
get_geo_distance
from web.add_func import get_rating_set_for_stars, get_flaps_for_big_pictures, get_flaps_for_mini_pictures, \
get_geo_distance, sanitize_slug
import django.utils.dateformat
import time
import os
@@ -26,11 +26,6 @@ from types import SimpleNamespace
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:
"""Возвращает канонический путь страницы цен для одного типового окна."""
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,
'PVC_ID': offer.pwc_id,
'PVC_NAME': offer.sProfileName,
'PVC_NAME_T': _slugify_lower(offer.sProfileName),
'PVC_NAME_T': sanitize_slug(offer.sProfileName),
'PVC_MANUFACTURER': offer.sProfileManufacturer,
'PVC_MANUFACTURER_T': _slugify_lower(offer.sProfileManufacturer),
'PVC_MANUFACTURER_T': sanitize_slug(offer.sProfileManufacturer),
'PVC_SEAL': offer.sProfileSealDescription,
'SETS_CLIMATE_CONTROL': offer.sSetClimateControl,
'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,
'PVC_ID': i2.pwc_id,
'PVC_NAME': i2.sProfileName,
'PVC_NAME_T': _slugify_lower(i2.sProfileName),
'PVC_NAME_T': sanitize_slug(i2.sProfileName),
'PVC_MANUFACTURER': i2.sProfileManufacturer,
'PVC_MANUFACTURER_T': _slugify_lower(i2.sProfileManufacturer),
'PVC_MANUFACTURER_T': sanitize_slug(i2.sProfileManufacturer),
'PVC_SEAL': i2.sProfileSealDescription,
'SETS_CLIMATE_CONTROL': i2.sSetClimateControl,
'SETS_SILL': i2.sSetSill,
@@ -710,7 +705,7 @@ def report_one_win_price(request: HttpRequest,
list_seria_for_win.append(SimpleNamespace(
id=seria_item['kApartment__kSeria__id'],
sName=seria_name,
sNameLat=_slugify_lower(seria_name),
sNameLat=sanitize_slug(seria_name),
num_variation_of_apartment=pytils.numeral.sum_string(
seria_item['num_variation_of_apartment'],
pytils.numeral.MALE,
@@ -793,7 +788,7 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
# если кто-то нахимичит ID квартиры не для этого дома, то сделаем так, что он будет от этого дома!
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:
# Переадресация 302, если с apart_id (ID-квартиры нахимичили) или slug-ом.
# Нужно для склейки парных URL в поисковиках
@@ -867,7 +862,7 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
# узнаем базовую серию дома
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,
'BASE_SERIA_LAT': base_seria_slug,
'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:
return redirect("/")
# Проверяем slug'и, если не совпадает — делаем 301 на канонический URL (новый формат)
seria_slug_real = pytils.translit.slugify((seria.sName or "").strip()).lower()
address_slug_real = pytils.translit.slugify((building.sAddress or "").strip()).lower()
seria_slug_real = sanitize_slug((seria.sName 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:
# Новый формат: /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)
@@ -1037,10 +1032,7 @@ def report_price_legacy_redirect(request, build_id, apart_id, slug):
except Exception:
return redirect("/")
import pytils
seria_slug = pytils.translit.slugify((seria.sName or "").strip()).lower()
address_slug = pytils.translit.slugify((building.sAddress or "").strip()).lower()
seria_slug = sanitize_slug((seria.sName 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>/
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)

View File

@@ -7,7 +7,7 @@ from django.utils import timezone
from django.db.models import F, Q, ExpressionWrapper, BooleanField, Max, Count, Avg
from oknardia.models import LogVisitPriceReport, SetKit
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
import django.utils.dateformat
import time
@@ -290,7 +290,7 @@ def compare_offers(request: HttpRequest, to_compare: str = "1,2") -> HttpRespons
"MERCHANT": i.sMerchantName,
"MERCHANT_ID": i.MERCHANT_ID,
"IS_COMMERCIAL": i.bCommercial,
"MERCHANT_T": pytils.translit.slugify(i.sMerchantName),
"MERCHANT_T": sanitize_slug(i.sMerchantName),
"MERCHANT_URL": i.sMerchantMainURL,
"MERCHANT_URL_SHOT": re.sub(r"(?:^https?://|/$|www\.)", "", i.sMerchantMainURL),
"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,
"PROFILE_ID": i.PROFILE_ID,
"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_T": pytils.translit.slugify(i.sProfileManufacturer),
"PROFILE_MANUFACTURER_T": sanitize_slug(i.sProfileManufacturer),
"PROFILE_NUM_COLOR": i.sProfileColor,
"PROFILE_NUM_CAMERAS": i.iProfileCameras, # Число камер рамы/створки
"PROFILE_NUM_CAMERAS_COLOR": _color_hi(profile_num_cameras, min_cameras, max_cameras, threshold=1),

View File

@@ -3,7 +3,7 @@ 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 web.add_func import normalize, get_rating_set_for_stars, sanitize_slug
from time import time
import json
import pytils
@@ -73,9 +73,9 @@ def profiles_rating(request: HttpRequest) -> HttpResponse:
"ID": profile.id,
"R_REAL": rating_real,
"BRAND": profile.sProfileManufacturer,
"BRAND_URL": pytils.translit.slugify(profile.sProfileManufacturer),
"BRAND_URL": sanitize_slug(profile.sProfileManufacturer),
"NAME": profile.sProfileName,
"NAME_URL": pytils.translit.slugify(profile.sProfileName),
"NAME_URL": sanitize_slug(profile.sProfileName),
"K_ARR": k_arr,
"RATING_STAR": get_rating_set_for_stars(profile.fProfileRating),
"RATING_N": profile.fProfileRating,

View File

@@ -6,12 +6,8 @@ from django.db.models import ExpressionWrapper, FloatField, F, Count
from django.db.models.functions import Abs
from smtplib import SMTPException
from oknardia.models import Seria_Info, Building_Info, Apartment_Type
from web.add_func import get_yandex_geocode_by_address, get_geo_distance
import json
import datetime
from web.add_func import get_yandex_geocode_by_address, get_geo_distance, sanitize_slug
import time
import pytils
# from django.core.context_processors import csrf
@@ -240,9 +236,9 @@ def get_address(request: HttpRequest) -> HttpResponse:
to_template.update({
'SERIA_BASE': q1.sName if q1 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_T': pytils.translit.slugify(addr),
'addr_T': sanitize_slug(addr),
'ticks': float(time.perf_counter() - time_start),
})
return render(request, "popup/popup_show_apartment_variants.html", to_template)