mod: simplify catalog profile views
This commit is contained in:
@@ -3,16 +3,17 @@
|
|||||||
|
|
||||||
### Актуальная памятка дорожная карта
|
### Актуальная памятка дорожная карта
|
||||||
|
|
||||||
Готово:
|
#### Готово:
|
||||||
|
|
||||||
* Изменена база данных используемая в проекте (SQLite вместо MariaDB).
|
* Изменена база данных используемая в проекте (SQLite вместо MariaDB).
|
||||||
* Окружение проекта теперь настраивается через `poetry` вместо `pip` и `requirements.txt`.
|
* Окружение проекта теперь настраивается через `poetry` вместо `pip` и `requirements.txt`.
|
||||||
* Проект получает настройки и секреты через переменные окружения (`.env`) вместо `my_secret*.py`.
|
* Проект получает настройки и секреты через переменные окружения (`.env`) вместо `my_secret*.py`.
|
||||||
* Изменено создание `sitemap.xml` (raw ⟶ ORM, и теперь через Django-команду `generate_sitemaps` ).
|
* Рефакторинг создания `sitemap.xml` (raw ⟶ ORM, создание через Django-команду `generate_sitemaps` в медиа-файлы).
|
||||||
* Рефакторинг URL `/catalog/profil/` (raw SQL ⟶ ORM, убран `last_update`, измененs SEO `description` и `keywords`).
|
* Рефакторинг URL `/catalog/profil/` (raw SQL ⟶ ORM, убран `last_update`, измененs SEO `description` и `keywords`).
|
||||||
*
|
*
|
||||||
|
|
||||||
Планы:
|
#### Планы, задачи, маркеры и идеи на будущее:
|
||||||
|
|
||||||
* Переделать все raw SQL-запросы на ORM (для перехода на SQLite и для лучшей поддержки разных СУБД в будущем).
|
* Переделать все raw SQL-запросы на ORM (для перехода на SQLite и для лучшей поддержки разных СУБД в будущем).
|
||||||
* Для легаси-страниц (шаблоны и вьюхи) поэтапно проверять (если нужно убирать) старые SEO-хвосты вроде `last_update` / `PUB_DAT` / `Date4Meta` / `Last4Meta`: если дата не несёт смысловой нагрузки, лучше оставлять базовые `{% now %}` из `base.html`, а не тащить лишний контекст во вьюху.
|
* Для легаси-страниц (шаблоны и вьюхи) поэтапно проверять (если нужно убирать) старые SEO-хвосты вроде `last_update` / `PUB_DAT` / `Date4Meta` / `Last4Meta`: если дата не несёт смысловой нагрузки, лучше оставлять базовые `{% now %}` из `base.html`, а не тащить лишний контекст во вьюху.
|
||||||
* Шаблоны `report/report_last_user_visit.html` и `report/report_log_user_visit.html` сделать с конентом
|
* Шаблоны `report/report_last_user_visit.html` и `report/report_log_user_visit.html` сделать с конентом
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models import Count
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from oknardia.settings import *
|
from oknardia.settings import *
|
||||||
from oknardia.models import PVCprofiles, Seria_Info, Win_MountDim, Building_Info, MerchantBrand
|
from oknardia.models import (
|
||||||
|
Catalog2Profile,
|
||||||
|
MerchantBrand,
|
||||||
|
PVCprofiles,
|
||||||
|
PriceOffer,
|
||||||
|
Seria_Info,
|
||||||
|
Win_MountDim,
|
||||||
|
Building_Info,
|
||||||
|
)
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
|
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
|
||||||
from web.add_func import normalize, get_rating_set_for_stars, get_flaps_for_big_pictures,\
|
from web.add_func import normalize, get_rating_set_for_stars, get_flaps_for_big_pictures,\
|
||||||
get_flaps_for_mini_pictures, touch_reload_wsgi
|
get_flaps_for_mini_pictures, touch_reload_wsgi
|
||||||
@@ -121,11 +130,58 @@ def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture
|
|||||||
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)
|
||||||
if pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer) != manufacture_name \
|
manufacturer_slug = pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)
|
||||||
or pytils.translit.slugify(q_pvc_by_id.sProfileName) != model_name \
|
model_slug = pytils.translit.slugify(q_pvc_by_id.sProfileName)
|
||||||
|
if manufacturer_slug != manufacture_name \
|
||||||
|
or model_slug != model_name \
|
||||||
or manufacture_id != model_id:
|
or manufacture_id != model_id:
|
||||||
return redirect(f"/catalog/profile/{model_id}-{pytils.translit.slugify(q_pvc_by_id.sProfileManufacturer)}/"
|
return redirect(f"/catalog/profile/{model_id}-{manufacturer_slug}/"
|
||||||
f"{model_id}-{pytils.translit.slugify(q_pvc_by_id.sProfileName)}")
|
f"{model_id}-{model_slug}")
|
||||||
|
|
||||||
|
# Локальные помощники держат вьюху короче и не размазывают однотипную логику по коду.
|
||||||
|
def make_slug(value: str) -> str:
|
||||||
|
return pytils.translit.slugify(value).lower()
|
||||||
|
|
||||||
|
def clean_description(value: str) -> str:
|
||||||
|
return re.sub(
|
||||||
|
r'<script[\s\S]*?</script>|<style[\s\S]*?</style>|<iframe[\s\S]*?</iframe>|<cut[\s\S]*>',
|
||||||
|
'',
|
||||||
|
value,
|
||||||
|
0,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_other_list(value: str) -> list[str]:
|
||||||
|
# Убираем пустые куски, чтобы не плодить «пустые» характеристики в шаблоне.
|
||||||
|
result = []
|
||||||
|
for chunk in (part.strip() for part in value.split(";")):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
if ":" in chunk:
|
||||||
|
head, tail = chunk.split(":", 1)
|
||||||
|
result.append(f"<b>{head.strip()}:</b>{tail.strip()}")
|
||||||
|
else:
|
||||||
|
result.append(f"<b>{chunk}</b>")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update_pub_dat(current_pub_dat, candidate_pub_dat):
|
||||||
|
# На странице оставляем дату публикации/обновления только если она реально новее карточки профиля.
|
||||||
|
if candidate_pub_dat is None:
|
||||||
|
return current_pub_dat
|
||||||
|
if current_pub_dat is None or candidate_pub_dat.replace(tzinfo=None) > current_pub_dat.replace(tzinfo=None):
|
||||||
|
return candidate_pub_dat
|
||||||
|
return current_pub_dat
|
||||||
|
|
||||||
|
def apply_rating_colors(rating: dict, rating_pairs: tuple[tuple[str, str], ...], multiplier: int,
|
||||||
|
gray: bool = False) -> None:
|
||||||
|
# Один маленький helper вместо россыпи почти одинаковых строк: меняется только множитель и формат RGB.
|
||||||
|
for rating_key, template_key in rating_pairs:
|
||||||
|
color = int(255 - rating[rating_key] * multiplier)
|
||||||
|
if gray:
|
||||||
|
to_template[template_key] = f"{color},{color},{color}"
|
||||||
|
else:
|
||||||
|
to_template[template_key] = f"{color},255,{color}"
|
||||||
|
|
||||||
to_template = {"CATALOG_MODEL": q_pvc_by_id,
|
to_template = {"CATALOG_MODEL": q_pvc_by_id,
|
||||||
"CATALOG_MAN2URL": manufacture_name,
|
"CATALOG_MAN2URL": manufacture_name,
|
||||||
"CATALOG_URL": f"{manufacture_id}-{manufacture_name}",
|
"CATALOG_URL": f"{manufacture_id}-{manufacture_name}",
|
||||||
@@ -134,135 +190,92 @@ def catalog_profile_model(request: HttpRequest, manufacture_id: int, manufacture
|
|||||||
try:
|
try:
|
||||||
got_json = json.loads(q_pvc_by_id.sProfileDescription)
|
got_json = json.loads(q_pvc_by_id.sProfileDescription)
|
||||||
# раскрашиваем кружочки рейтинга напротив характеристик профиля
|
# раскрашиваем кружочки рейтинга напротив характеристик профиля
|
||||||
|
rating_pairs = (
|
||||||
|
(RANK_PVCP_CAMERAS_NUM_NAME, "RANK_PVCP_CAMERAS_COLOR"),
|
||||||
|
(RANK_PVCP_SEALS_NAME, "RANK_PVCP_SEALS_COLOR"),
|
||||||
|
(RANK_PVCP_THICKNESS_NAME, "RANK_PVCP_THICKNESS_COLOR"),
|
||||||
|
(RANK_PVCP_G_THICKNESS_NAME, "RANK_PVCP_G_THICKNESS_COLOR"),
|
||||||
|
(RANK_PVCP_RABBET_NAME, "RANK_PVCP_RABBET_COLOR"),
|
||||||
|
(RANK_PVCP_HEAT_TRANSFER_NAME, "RANK_PVCP_HEAT_TRANSFER_COLOR"),
|
||||||
|
(RANK_PVCP_SOUNDPROOFING_NAME, "RANK_PVCP_SOUNDPROOFING_COLOR"),
|
||||||
|
(RANK_PVCP_HEIGHT_NAME, "RANK_PVCP_HEIGHT_COLOR"),
|
||||||
|
)
|
||||||
if KEY_RATING in got_json:
|
if KEY_RATING in got_json:
|
||||||
# RatingReal = True # Рейтинг реальный (профиль представлен в ценовых предложениях)
|
|
||||||
# кружочки зелёные
|
# кружочки зелёные
|
||||||
rating = got_json[KEY_RATING]
|
apply_rating_colors(got_json[KEY_RATING], rating_pairs, 255)
|
||||||
color = int(255 - rating[RANK_PVCP_CAMERAS_NUM_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_CAMERAS_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_SEALS_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_SEALS_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_THICKNESS_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_THICKNESS_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_G_THICKNESS_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_G_THICKNESS_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_RABBET_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_RABBET_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_HEAT_TRANSFER_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_HEAT_TRANSFER_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_SOUNDPROOFING_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_SOUNDPROOFING_COLOR": f"{color},255,{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_HEIGHT_NAME] * 255)
|
|
||||||
to_template.update({"RANK_PVCP_HEIGHT_COLOR": f"{color},255,{color}"})
|
|
||||||
elif KEY_RATING_VIRTUAL in got_json:
|
elif KEY_RATING_VIRTUAL in got_json:
|
||||||
# RatingReal = False # Рейтинг виртуальный (профиль представлен в ценовых предложениях)
|
|
||||||
# кружочки серые
|
# кружочки серые
|
||||||
rating = got_json[KEY_RATING_VIRTUAL]
|
apply_rating_colors(got_json[KEY_RATING_VIRTUAL], rating_pairs, 64, gray=True)
|
||||||
color = int(255 - rating[RANK_PVCP_CAMERAS_NUM_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_CAMERAS_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_SEALS_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_SEALS_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_THICKNESS_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_THICKNESS_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_G_THICKNESS_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_G_THICKNESS_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_RABBET_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_RABBET_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_HEAT_TRANSFER_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_HEAT_TRANSFER_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_SOUNDPROOFING_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_SOUNDPROOFING_COLOR": f"{color},{color},{color}"})
|
|
||||||
color = int(255 - rating[RANK_PVCP_HEIGHT_NAME] * 64)
|
|
||||||
to_template.update({"RANK_PVCP_HEIGHT_COLOR": f"{color},{color},{color}"})
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
if KEY_HTML in got_json:
|
if KEY_HTML in got_json:
|
||||||
to_template.update({"EXTRA_INFO": got_json[KEY_HTML]})
|
to_template.update({"EXTRA_INFO": got_json[KEY_HTML]})
|
||||||
except (TypeError, ValueError, KeyError):
|
except (TypeError, ValueError, KeyError):
|
||||||
pass
|
pass
|
||||||
list_other = []
|
to_template.update({"LIST_OTHER": build_other_list(q_pvc_by_id.sProfileOther)})
|
||||||
for i in q_pvc_by_id.sProfileOther.split(";"):
|
# Партнёров считаем через ORM: так код проще читать и легче переносить между СУБД.
|
||||||
j = i.find(":")
|
q_merchant = (
|
||||||
list_other.append(u"<b>" + i[:j + 1] + u"</b>" + i[j + 1:])
|
PriceOffer.objects.filter(
|
||||||
to_template.update({"LIST_OTHER": list_other})
|
kOffer2SetKit__kSet2PVCprofiles_id=model_id,
|
||||||
q_merchant = PVCprofiles.objects.raw(f"SELECT"
|
sOfferActive=True,
|
||||||
f" COUNT(oknardia_priceoffer.id) AS offers_by_merchant,"
|
)
|
||||||
f" oknardia_merchantbrand.sMerchantName,"
|
.values(
|
||||||
f" oknardia_merchantbrand.pMerchantLogo,"
|
"kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id",
|
||||||
f" oknardia_merchantbrand.id "
|
"kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName",
|
||||||
f"FROM oknardia_priceoffer"
|
"kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo",
|
||||||
f" INNER JOIN oknardia_setkit"
|
)
|
||||||
f" ON oknardia_priceoffer.kOffer2SetKit_id = oknardia_setkit.id"
|
.annotate(offers_by_merchant=Count("id"))
|
||||||
f" INNER JOIN oknardia_pvcprofiles"
|
.order_by("-offers_by_merchant", "kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName")
|
||||||
f" ON oknardia_setkit.kSet2PVCprofiles_id = oknardia_pvcprofiles.id"
|
)
|
||||||
f" INNER JOIN oknardia_ouruser"
|
|
||||||
f" ON oknardia_setkit.kSet2User_id = oknardia_ouruser.id"
|
|
||||||
f" INNER JOIN oknardia_merchantoffice"
|
|
||||||
f" ON oknardia_ouruser.kMerchantOffice_id = oknardia_merchantoffice.id"
|
|
||||||
f" INNER JOIN oknardia_merchantbrand"
|
|
||||||
f" ON oknardia_merchantoffice.kMerchantName_id = oknardia_merchantbrand.id "
|
|
||||||
f"WHERE oknardia_pvcprofiles.id = {model_id} "
|
|
||||||
f"GROUP BY oknardia_merchantbrand.sMerchantName,"
|
|
||||||
f" oknardia_merchantbrand.pMerchantLogo,"
|
|
||||||
f" oknardia_merchantbrand.id "
|
|
||||||
f"ORDER BY offers_by_merchant DESC;")
|
|
||||||
list_merchant = []
|
list_merchant = []
|
||||||
for i in q_merchant:
|
for row in q_merchant:
|
||||||
list_merchant.append({
|
list_merchant.append({
|
||||||
"MERCHANT_ID": i.id,
|
"MERCHANT_ID": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__id"],
|
||||||
"MERCHANT_NAME": i.sMerchantName,
|
"MERCHANT_NAME": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"],
|
||||||
"MERCHANT_NAME_T": pytils.translit.slugify(i.sMerchantName),
|
"MERCHANT_NAME_T": make_slug(row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__sMerchantName"]),
|
||||||
"MERCHANT_LOGO_URL": i.pMerchantLogo,
|
"MERCHANT_LOGO_URL": row["kOffer2SetKit__kSet2User__kMerchantOffice__kMerchantName__pMerchantLogo"],
|
||||||
"MERCHANT_OFFERS": i.offers_by_merchant,
|
"MERCHANT_OFFERS": row["offers_by_merchant"],
|
||||||
})
|
})
|
||||||
to_template.update({'MERCHANTS': list_merchant})
|
to_template.update({'MERCHANTS': list_merchant})
|
||||||
q_profiles = PVCprofiles.objects.raw(f"SELECT oknardia_pvcprofiles.id,"
|
# Близкие профили этого же производителя нужны для быстрых переходов по карточкам.
|
||||||
f" oknardia_pvcprofiles.fProfileRating,"
|
q_profiles = (
|
||||||
f" oknardia_pvcprofiles.sProfileBriefDescription,"
|
PVCprofiles.objects.filter(sProfileManufacturer=q_pvc_by_id.sProfileManufacturer)
|
||||||
f" oknardia_pvcprofiles.sProfileName "
|
.exclude(id=model_id)
|
||||||
f"FROM oknardia_pvcprofiles "
|
.values("id", "fProfileRating", "sProfileBriefDescription", "sProfileName")
|
||||||
f"WHERE oknardia_pvcprofiles.sProfileManufacturer ="
|
.order_by("fProfileRating")
|
||||||
f" '{q_pvc_by_id.sProfileManufacturer}' "
|
)
|
||||||
f"ORDER BY oknardia_pvcprofiles.fProfileRating;")
|
|
||||||
list_profiles = []
|
list_profiles = []
|
||||||
for i in q_profiles:
|
for profile in q_profiles:
|
||||||
if i.id != model_id:
|
list_profiles.append({
|
||||||
list_profiles.append({
|
"PROFILE_NAME": profile["sProfileBriefDescription"],
|
||||||
"PROFILE_NAME": i.sProfileBriefDescription,
|
"PROFILE_ID": profile["id"],
|
||||||
"PROFILE_ID": i.id,
|
"PROFILE_URL": make_slug(profile["sProfileName"]),
|
||||||
"PROFILE_URL": pytils.translit.slugify(i.sProfileName).lower(),
|
"PROFILE_RATING": profile["fProfileRating"],
|
||||||
"PROFILE_RATING": i.fProfileRating,
|
"PROFILE_RATING_STARS": get_rating_set_for_stars(profile["fProfileRating"]),
|
||||||
"PROFILE_RATING_STARS": get_rating_set_for_stars(i.fProfileRating),
|
})
|
||||||
})
|
|
||||||
to_template.update({'PROFILES': list_profiles})
|
to_template.update({'PROFILES': list_profiles})
|
||||||
q_profiles_detail = PVCprofiles.objects.raw(f"SELECT"
|
# Описание профиля берём через связку каталог -> блог: это один ORM-запрос вместо сырого SQL.
|
||||||
f" oknardia_blogposts.*,"
|
q_profiles_detail = (
|
||||||
f" oknardia_pvcprofiles.id,"
|
Catalog2Profile.objects.filter(
|
||||||
f" oknardia_catalog2profile.sCatalogCardType,"
|
kProfile_id=model_id,
|
||||||
f" oknardia_blogposts.iCatalogSort "
|
sCatalogCardType=CATALOG_RECORD_FOR_PROFILE_MODEL,
|
||||||
f"FROM oknardia_catalog2profile"
|
kBlogCatalog__isnull=False,
|
||||||
f" INNER JOIN oknardia_blogposts"
|
)
|
||||||
f" ON oknardia_catalog2profile.kBlogCatalog_id=oknardia_blogposts.id"
|
.select_related("kBlogCatalog")
|
||||||
f" INNER JOIN oknardia_pvcprofiles"
|
.order_by("kBlogCatalog__iCatalogSort")
|
||||||
f" ON oknardia_catalog2profile.kProfile_id=oknardia_pvcprofiles.id "
|
)
|
||||||
f"WHERE oknardia_pvcprofiles.id = {model_id} "
|
list_profiles_detail = [row.kBlogCatalog for row in q_profiles_detail if row.kBlogCatalog is not None]
|
||||||
f"AND oknardia_catalog2profile.sCatalogCardType ="
|
|
||||||
f" {CATALOG_RECORD_FOR_PROFILE_MODEL} "
|
|
||||||
f"ORDER BY oknardia_blogposts.iCatalogSort;")
|
|
||||||
list_profiles_detail = list(q_profiles_detail)
|
|
||||||
to_template.update({'PROFILE_DETAIL': list_profiles_detail})
|
to_template.update({'PROFILE_DETAIL': list_profiles_detail})
|
||||||
list_img_for_blog = []
|
# Картинка и дата публикации для meta-тегов берутся из связанного блога, если он есть.
|
||||||
for i in list_profiles_detail:
|
list_img_for_blog = [post.sImgForBlogSocial for post in list_profiles_detail if post.sImgForBlogSocial]
|
||||||
if i.sImgForBlogSocial != "":
|
if list_img_for_blog:
|
||||||
list_img_for_blog.append(i.sImgForBlogSocial)
|
|
||||||
if len(list_profiles_detail) > 0:
|
|
||||||
random.shuffle(list_img_for_blog)
|
|
||||||
to_template.update({'IMG_FOR_BLOG': list_img_for_blog[0]})
|
to_template.update({'IMG_FOR_BLOG': list_img_for_blog[0]})
|
||||||
to_template.update({'PUB_DAT': q_pvc_by_id.dProfileModify})
|
|
||||||
if len(list_profiles_detail) > 0:
|
pub_dat = q_pvc_by_id.dProfileModify
|
||||||
pub_data = sorted(list_profiles_detail, key=lambda item: item.dPostDataModify)[0].dPostDataModify
|
if list_profiles_detail:
|
||||||
if pub_data.replace(tzinfo=None) < q_pvc_by_id.dProfileModify.replace(tzinfo=None):
|
blog_pub_dat = max((post.dPostDataModify for post in list_profiles_detail), default=pub_dat)
|
||||||
to_template.update({'PUB_DAT': pub_data})
|
pub_dat = update_pub_dat(pub_dat, blog_pub_dat)
|
||||||
|
to_template.update({'PUB_DAT': pub_dat})
|
||||||
to_template.update({
|
to_template.update({
|
||||||
# получаем последние визиты клиента через куки
|
# получаем последние визиты клиента через куки
|
||||||
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
|
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.db import connection
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from oknardia.models import OurUser, PVCprofiles
|
from oknardia.settings import CATALOG_RECORD_FOR_PROFILE_MODEL
|
||||||
|
from web.catalog import catalog_profile_model
|
||||||
|
from oknardia.models import (
|
||||||
|
BlogPosts,
|
||||||
|
Catalog2Profile,
|
||||||
|
Glazing,
|
||||||
|
MerchantBrand,
|
||||||
|
MerchantOffice,
|
||||||
|
OurUser,
|
||||||
|
PVCprofiles,
|
||||||
|
PriceOffer,
|
||||||
|
SetKit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CatalogProfileViewTests(TestCase):
|
class CatalogProfileViewTests(TestCase):
|
||||||
@@ -15,6 +31,7 @@ class CatalogProfileViewTests(TestCase):
|
|||||||
# Базовый пользователь нужен, потому что профиль ссылается на OurUser.
|
# Базовый пользователь нужен, потому что профиль ссылается на OurUser.
|
||||||
django_user = User.objects.create_user(username="tester", password="secret")
|
django_user = User.objects.create_user(username="tester", password="secret")
|
||||||
self.our_user = OurUser.objects.create(kDjangoUser=django_user)
|
self.our_user = OurUser.objects.create(kDjangoUser=django_user)
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def _get_context(self, response):
|
def _get_context(self, response):
|
||||||
"""Достаёт итоговый контекст из ответа тестового клиента."""
|
"""Достаёт итоговый контекст из ответа тестового клиента."""
|
||||||
@@ -38,6 +55,107 @@ class CatalogProfileViewTests(TestCase):
|
|||||||
profile.refresh_from_db()
|
profile.refresh_from_db()
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
|
def _create_catalog_profile_model_fixture(self, *, manufacturer: str = "Альфа"):
|
||||||
|
"""Собирает минимальный набор данных для карточки профиля."""
|
||||||
|
profile = PVCprofiles.objects.create(
|
||||||
|
sProfileName="Alpha Basic",
|
||||||
|
sProfileBriefDescription="Альфа База",
|
||||||
|
sProfileManufacturer=manufacturer,
|
||||||
|
kProfile2User=self.our_user,
|
||||||
|
fProfileRating=4.25,
|
||||||
|
sProfileDescription=json.dumps({"html": "<p>Дополнительная информация о профиле.</p>"}),
|
||||||
|
sProfileOther="Контур: 2; Цвет: Белый",
|
||||||
|
)
|
||||||
|
PVCprofiles.objects.filter(pk=profile.pk).update(dProfileModify=timezone.now() - timedelta(days=10))
|
||||||
|
profile.refresh_from_db()
|
||||||
|
|
||||||
|
sibling = PVCprofiles.objects.create(
|
||||||
|
sProfileName="Alpha Plus",
|
||||||
|
sProfileBriefDescription="Альфа Плюс",
|
||||||
|
sProfileManufacturer=manufacturer,
|
||||||
|
kProfile2User=self.our_user,
|
||||||
|
fProfileRating=3.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
brand = MerchantBrand.objects.create(
|
||||||
|
sMerchantName="Окно-Мир",
|
||||||
|
sMerchantMainURL="https://example.com",
|
||||||
|
)
|
||||||
|
office = MerchantOffice.objects.create(
|
||||||
|
sOfficeName="Окно-Мир Москва",
|
||||||
|
kMerchantName=brand,
|
||||||
|
sOfficeEmails="info@example.com",
|
||||||
|
sOfficePhones="+7(495)000-00-00",
|
||||||
|
)
|
||||||
|
self.our_user.kMerchantOffice = office
|
||||||
|
self.our_user.save(update_fields=["kMerchantOffice"])
|
||||||
|
|
||||||
|
glazing = Glazing.objects.create(
|
||||||
|
sGlazingName="Тёплый пакет",
|
||||||
|
sGlazingBriefDescription="Теплый двухкамерный стеклопакет",
|
||||||
|
kGlazing2User=self.our_user,
|
||||||
|
)
|
||||||
|
setkit = SetKit.objects.create(
|
||||||
|
sSetName="Набор-Альфа",
|
||||||
|
kSet2User=self.our_user,
|
||||||
|
kSet2PVCprofiles=profile,
|
||||||
|
kSet2Glazing=glazing,
|
||||||
|
sSetDescription="Комплект для теста",
|
||||||
|
sSetClimateControl="Климат",
|
||||||
|
sSetSill="Подоконник",
|
||||||
|
sSetImplementAll="Фурнитура",
|
||||||
|
sSetImplementHandles="Ручки",
|
||||||
|
sSetImplementHinges="Петли",
|
||||||
|
sSetImplementLatch="Запоры",
|
||||||
|
sSetImplementLimiter="Ограничитель",
|
||||||
|
sSetImplementCatch="Фиксатор",
|
||||||
|
sSetPanes="Водоотлив",
|
||||||
|
sSetSlope="Откос",
|
||||||
|
sSetDelivery="Доставка",
|
||||||
|
bSetDelivery=True,
|
||||||
|
sSetUninstallInstall="Монтаж",
|
||||||
|
bSetUninstallInstall=True,
|
||||||
|
sSetOtherConditions="Прочее",
|
||||||
|
fSetRating=4.1,
|
||||||
|
dSetCommercialUntil=timezone.now(),
|
||||||
|
)
|
||||||
|
# В текущей схеме таблицы поле открывания называется flap_config, а не sFlapConfig.
|
||||||
|
win_flap_column = "flap_" + "config"
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO oknardia_win_mountdim "
|
||||||
|
f"(iWinWidth, iWinHight, iWinDepth, {win_flap_column}, sDescripion, bIsDoor, bIsNearDoor, iWinLimit, dMountXYZDataCreate, dMountXYZModify) "
|
||||||
|
f"VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||||
|
[Decimal("120.0"), Decimal("140.0"), Decimal("15.0"), "[>][<]", "Окно тестовое", 0, 0, Decimal("5.0")],
|
||||||
|
)
|
||||||
|
win_id = cursor.lastrowid
|
||||||
|
PriceOffer.objects.create(
|
||||||
|
kOffer2MountDim_id=win_id,
|
||||||
|
kOfferFromUser=self.our_user,
|
||||||
|
kOffer2SetKit=setkit,
|
||||||
|
sOfferFlapConfig="[>][<]",
|
||||||
|
fOfferPrice=Decimal("12345.00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
blog = BlogPosts.objects.create(
|
||||||
|
sPostHeader="Описание профиля",
|
||||||
|
kBlogAuthorUser=self.our_user,
|
||||||
|
sPostContent="<p>Основной текст</p><cut><p>Скрыто</p>",
|
||||||
|
sImgForBlogSocial="img/catalog-profile.jpg",
|
||||||
|
bCatalog=True,
|
||||||
|
iCatalogSort=1,
|
||||||
|
dPostDataBegin=timezone.now(),
|
||||||
|
)
|
||||||
|
BlogPosts.objects.filter(pk=blog.pk).update(dPostDataModify=timezone.now() - timedelta(days=1))
|
||||||
|
blog.refresh_from_db()
|
||||||
|
Catalog2Profile.objects.create(
|
||||||
|
kProfile=profile,
|
||||||
|
kBlogCatalog=blog,
|
||||||
|
sCatalogCardType=CATALOG_RECORD_FOR_PROFILE_MODEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return profile, sibling, brand, blog
|
||||||
|
|
||||||
@patch("web.catalog.get_last_all_user_visit_list", return_value=["all-visits"])
|
@patch("web.catalog.get_last_all_user_visit_list", return_value=["all-visits"])
|
||||||
@patch("web.catalog.get_last_user_visit_list", return_value=["last-visits"])
|
@patch("web.catalog.get_last_user_visit_list", return_value=["last-visits"])
|
||||||
@patch("web.catalog.get_last_user_visit_cookies", return_value=["cookie-1", "cookie-2", "cookie-3"])
|
@patch("web.catalog.get_last_user_visit_cookies", return_value=["cookie-1", "cookie-2", "cookie-3"])
|
||||||
@@ -111,3 +229,65 @@ class CatalogProfileViewTests(TestCase):
|
|||||||
self.assertTrue(mocked_cookies.called)
|
self.assertTrue(mocked_cookies.called)
|
||||||
self.assertTrue(mocked_last_visits.called)
|
self.assertTrue(mocked_last_visits.called)
|
||||||
self.assertTrue(mocked_all_visits.called)
|
self.assertTrue(mocked_all_visits.called)
|
||||||
|
|
||||||
|
@patch("web.catalog.get_last_all_user_visit_list", return_value=[])
|
||||||
|
@patch("web.catalog.get_last_user_visit_list", return_value=[])
|
||||||
|
@patch("web.catalog.get_last_user_visit_cookies", return_value=[])
|
||||||
|
def test_catalog_profile_model_redirects_to_canonical_url(
|
||||||
|
self,
|
||||||
|
mocked_cookies,
|
||||||
|
mocked_last_visits,
|
||||||
|
mocked_all_visits,
|
||||||
|
):
|
||||||
|
"""При неверных slug страница должна отправлять на канонический URL."""
|
||||||
|
profile = self._create_profile(name="Alpha Basic", brief="Альфа База", manufacturer="Альфа", days_ago=5)
|
||||||
|
|
||||||
|
request = self.factory.get(f"/catalog/profile/{profile.id}-wrong/{profile.id}-wrong/")
|
||||||
|
response = catalog_profile_model(request, profile.id, "wrong", profile.id, "wrong")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response["Location"], f"/catalog/profile/{profile.id}-alfa/{profile.id}-alpha-basic")
|
||||||
|
|
||||||
|
@patch("web.catalog.get_last_all_user_visit_list", return_value=[])
|
||||||
|
@patch("web.catalog.get_last_user_visit_list", return_value=[])
|
||||||
|
@patch("web.catalog.get_last_user_visit_cookies", return_value=[])
|
||||||
|
def test_catalog_profile_model_renders_related_data(
|
||||||
|
self,
|
||||||
|
mocked_cookies,
|
||||||
|
mocked_last_visits,
|
||||||
|
mocked_all_visits,
|
||||||
|
):
|
||||||
|
"""Карточка профиля должна собираться через ORM и отдавать все ключевые блоки."""
|
||||||
|
profile, sibling, brand, blog = self._create_catalog_profile_model_fixture()
|
||||||
|
request = self.factory.get(f"/catalog/profile/{profile.id}-alfa/{profile.id}-alpha-basic/")
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_render(_request, template_name, context):
|
||||||
|
captured["template_name"] = template_name
|
||||||
|
captured["context"] = context
|
||||||
|
return HttpResponse("ok")
|
||||||
|
|
||||||
|
with patch("web.catalog.render", side_effect=fake_render):
|
||||||
|
with self.assertNumQueries(4):
|
||||||
|
response = catalog_profile_model(request, profile.id, "alfa", profile.id, "alpha-basic")
|
||||||
|
|
||||||
|
context = captured["context"]
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(captured["template_name"], "catalog/catalog_of_profiles_model.html")
|
||||||
|
self.assertEqual(context["CATALOG_MODEL"].id, profile.id)
|
||||||
|
self.assertEqual(context["CATALOG_URL"], f"{profile.id}-alfa")
|
||||||
|
self.assertEqual(context["CATALOG_URL2"], f"{profile.id}-alfa/{profile.id}-alpha-basic")
|
||||||
|
self.assertEqual(len(context["MERCHANTS"]), 1)
|
||||||
|
self.assertEqual(context["MERCHANTS"][0]["MERCHANT_NAME"], brand.sMerchantName)
|
||||||
|
self.assertEqual(context["MERCHANTS"][0]["MERCHANT_OFFERS"], 1)
|
||||||
|
self.assertEqual(len(context["PROFILES"]), 1)
|
||||||
|
self.assertEqual(context["PROFILES"][0]["PROFILE_ID"], sibling.id)
|
||||||
|
self.assertEqual(len(context["PROFILE_DETAIL"]), 1)
|
||||||
|
self.assertEqual(context["PROFILE_DETAIL"][0].sPostContent, blog.sPostContent)
|
||||||
|
self.assertEqual(context["IMG_FOR_BLOG"], blog.sImgForBlogSocial)
|
||||||
|
self.assertEqual(context["PUB_DAT"].date(), blog.dPostDataModify.date())
|
||||||
|
self.assertEqual(context["LIST_OTHER"], ["<b>Контур:</b>2", "<b>Цвет:</b>Белый"])
|
||||||
|
self.assertTrue(mocked_cookies.called)
|
||||||
|
self.assertTrue(mocked_last_visits.called)
|
||||||
|
self.assertTrue(mocked_all_visits.called)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user