mod: данные о последних визитах пользователя полностью перенесены с сервера на клиент (в JS). Отключен из контекста всех шаблонов LAST_VISIT и связанные с ним функции в вьюшках

This commit is contained in:
2026-05-09 21:21:20 +03:00
parent b3aa0ce3b3
commit 978a3ad02e
14 changed files with 193 additions and 143 deletions

View File

@@ -75,11 +75,7 @@
<button type="submit" class="btn btn-primary btn-add">Найти</button>
</span>
</div>
{% if LAST_VISIT %}<div><h5>Ваши последние просмотры:</h5>
<ul style="font-size:small">{% for ITEM in LAST_VISIT %}
<li><a href="{{ ITEM.LastURL }}">{{ ITEM.LastApart }} <small>({{ ITEM.LastAddress }})</small></a> <small style="font-size: xx-small;">{{ ITEM.Time }}</small></li>{% endfor %}
</ul>
</div>{% endif %}
{% include 'report/report_last_user_visit.html' with background_color="None" %}
</form>
<p></p>{% endwith %}

View File

@@ -107,7 +107,8 @@
<meta property="article:modified_time" content="{{ META_DATA_PUBLISH|date:'Y-m-d' }}" />
{% endblock %}
{% block Top_JS3%}<script type="text/javascript">
{% block Top_JS3%}<script type="text/javascript" src="{% static 'js/track_user_visit.js' %}"></script>
<script type="text/javascript">
function show_phone_num( id ){ // колапсатор для отображения контатной информации постафшика окон
$('#tel'+id).collapse('show');
$('#hid'+id).collapse('hide');
@@ -278,6 +279,13 @@ $(function () { // инициализация и обработка попове
<p id="shadow_buffer"></p>
</div>
{# Скрытый элемент для отслеживания визитов пользователя (передача данных в JS track_user_visit.js) #}
<div id="tracking-data"
data-current-url="{{ request.path }}"
data-address="{{ ADDRESS }}"
data-apart="{{ APART }}"
style="display: none;"></div>
{# модальное окно #}
<div class="modal fade bs-example-modal-sm" id="modal-exclamation" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm">

View File

@@ -1,8 +1,71 @@
<!--- Информация об адресах просмотренных текущим пользователем --->{% load filters %}
{% if LAST_VISIT and LAST_VISIT|length >= 1 %}<div class="col-xs-12">
<div class="col-md-11 col-xs-12 last_user_visit"><h5>Цены на окна просмотренные вами:</h5>
<ul>{% for ITEM in LAST_VISIT %}
<li><a href="{{ ITEM.LastURL }}">Цены на окна для серии {{ ITEM.LastApart }} <small>({{ ITEM.LastAddress }})</small></a> <small>{{ ITEM.Time }}</small></li>{% endfor %}
</ul>
<!-- Информация об адресах, просмотренных текущим пользователем (читается из браузерных кук) -->
<div class="col-xs-12">
<div class="col-md-11 col-xs-12{% if background_color != "None" %} last_user_visit{% endif %}" id="last_user_visit_container" style="display:none;">
<h5>Цены на окна просмотренные вами {{ background_color }}:</h5>
<ul id="last_visits_list"></ul>
</div>
</div>{% endif %}
</div>
<script type="text/javascript">
/**
* Отслеживание последних визитов пользователя из браузерных кук.
* Читает куку 'LastVisit', парсит JSON и выводит список ссылок на уже просмотренные ценовые отчёты.
*/
document.addEventListener('DOMContentLoaded', function() {
// Функция для получения значения куки по имени
function getCookieValue(name) {
try {
if (document.cookie) {
const cookies = document.cookie.split('; ');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=');
if (cookieName === name) {
return decodeURIComponent(cookieValue);
}
}
}
} catch (e) {
console.warn('Ошибка при чтении куки LastVisit:', e);
}
return null;
}
// Получаем куку с визитами
const cookieValue = getCookieValue('LastVisit');
if (cookieValue) {
try {
const visits = JSON.parse(cookieValue);
// Проверяем, есть ли визиты
if (visits && visits.length > 0) {
const listContainer = document.getElementById('last_visits_list');
const lastUserVisitContainer = document.getElementById('last_user_visit_container');
// Очищаем список перед заполнением
listContainer.innerHTML = '';
// При перезагрузке страницы текущий визит уже записан, поэтому пропускаем первый
const visitsToShow = visits.slice(1);
// Выводим предыдущие визиты (не текущий)
for (let i = 0; i < visitsToShow.length; i++) {
const item = visitsToShow[i];
const li = document.createElement('li');
// Форматируем текст ссылки: адрес (тип квартиры)
const linkText = `Цены на окна для серии ${item.LastApart} <small>(${item.LastAddress})</small>`;
li.innerHTML = `<a href="${item.LastURL}">${linkText}</a>`;
listContainer.appendChild(li);
}
// Если есть данные для отображения, показываем блок
if (visitsToShow.length > 0) {
lastUserVisitContainer.style.display = 'block';
}
}
} catch (e) {
console.warn('Ошибка при разборе JSON из кук LastVisit:', e);
}
}
});
</script>

View File

@@ -7,7 +7,7 @@ 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.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_list
def catalog_root(request: HttpRequest) -> HttpResponse:
@@ -21,7 +21,6 @@ def catalog_root(request: HttpRequest) -> HttpResponse:
time_start = time.perf_counter()
# получаем из cookies последние визиты клиента
to_template: dict[str, object] = {
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start)}
response = render(request, "catalog/catalog_root.html", to_template)
@@ -83,7 +82,6 @@ def catalog_sets(request: HttpRequest) -> HttpResponse:
to_template: dict[str, object] = {
'SET_LIST': kits,
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start),
}

View File

@@ -17,11 +17,7 @@ from oknardia.models import (
SetKit,
PriceOffer,
)
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_list
from web.add_func import get_rating_set_for_stars
import django.utils.dateformat
import time
@@ -179,7 +175,6 @@ def catalog_company(request: HttpRequest) -> HttpResponse:
Контекст шаблона:
- COMPANIES (list): Список компаний с статистикой
- LAST_VISIT (list): Последние визиты текущего пользователя
- LOG_VISIT (list): Последние визиты всех пользователей
Args:
@@ -200,9 +195,6 @@ def catalog_company(request: HttpRequest) -> HttpResponse:
# Получаем информацию о посещениях для персонализации
to_template: dict[str, object] = {
'COMPANIES': formatted_companies,
'LAST_VISIT': get_last_user_visit_list(
get_last_user_visit_cookies(request)[:3]
),
'LOG_VISIT': get_last_all_user_visit_list(),
}
@@ -469,7 +461,6 @@ def catalog_company_detail(
- SETS (list): Список оконных наборов с их полной информацией
- IMG_FOR_BLOG (str): Логотип компании
- LIST_NOT (list): Стандартные маркеры "пусто"
- LAST_VISIT (list): Последние визиты текущего пользователя
- LOG_VISIT (list): Последние визиты всех пользователей
- ticks (float): Время выполнения представления (в секундах)
@@ -518,9 +509,6 @@ def catalog_company_detail(
'META_KEYWORDS': company.sMerchantName,
'IMG_FOR_BLOG': company.pMerchantLogo,
'LIST_NOT': empty_values,
'LAST_VISIT': get_last_user_visit_list(
get_last_user_visit_cookies(request)[:3]
),
'LOG_VISIT': get_last_all_user_visit_list(),
}

View File

@@ -3,7 +3,7 @@ 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_cookies, 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_mini_pictures
import time
import pytils
@@ -20,8 +20,6 @@ def _make_slug(value: str) -> str:
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
to_template.update({
# получаем последние визиты клиента через куки
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
# получаем последние визиты всех посетителей из базы
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start),

View File

@@ -7,7 +7,7 @@ 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_cookies, 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
import time
import json
@@ -49,7 +49,6 @@ def _profile_row_to_dict(profile: dict) -> dict:
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
to_template.update({
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start),
})

View File

@@ -13,7 +13,7 @@ from oknardia.models import (
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_list
from web.add_func import get_flaps_for_big_pictures
import time
import os
@@ -29,7 +29,6 @@ def _make_slug(value: str) -> str:
def _append_visit_context(to_template: dict, request: HttpRequest, time_start: float) -> None:
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
to_template.update({
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start),
})

View File

@@ -14,7 +14,7 @@ from oknardia.models import (
MountDim2Apartment,
)
from oknardia.settings import *
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_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
import django.utils.dateformat
@@ -57,11 +57,8 @@ def _append_visit_context(
"""Дописывает в контекст стандартный хвост: визиты и время выполнения."""
if log_visit is None:
log_visit = get_last_all_user_visit_list()
if last_visit_cookie is None:
last_visit_cookie = get_last_user_visit_cookies(request)
to_template.update({
'LAST_VISIT': get_last_user_visit_list(last_visit_cookie[:3]),
'LOG_VISIT': log_visit,
'ticks': float(time.perf_counter() - time_start),
})
@@ -956,29 +953,14 @@ def report_price(request: HttpRequest, build_id: str = "22427", apart_id: str =
)
log_entry.save() # INSERT
# получаем последние визиты клиента через куки
last_visit = get_last_user_visit_cookies(request)
# Для блока LAST_VISIT показываем историю до текущего захода.
last_visit_for_context = list(last_visit)
# подготавливаем данные о текущем посещении для помещения в cookie
Item = {
"LastURL": new_url,
"LastAddress": to_template["ADDRESS"],
"LastApart": to_template["APART"],
"Time": time.perf_counter()}
last_visit.insert(0, Item) # Добавляем текущий Item в начало
last_visit = json.dumps(last_visit[:3]) # упаковываем json без пробелов (три записи)
# print u"сейчас запишем вот эту куку:", LastVisit
# Вызываем контекст без параметра last_visit_cookie (получит из кук автоматически)
_append_visit_context(
to_template=to_template,
request=request,
time_start=time_start,
log_visit=log_visit,
last_visit_cookie=last_visit_for_context,
)
response = render(request, "price/price_list.html", to_template)
response.set_cookie("LastVisit", last_visit, max_age=7862400) # ставим или перезаписываем куки (91 день)
return response
return render(request, "price/price_list.html", to_template)
def next_price_frame(request: HttpRequest, apart_id: str = "1", mount_dim_per_offer: str = "1",

View File

@@ -95,21 +95,6 @@ def _bounds(items: list, field: str, threshold=None) -> tuple[float, float]:
return min(vals), max(vals)
def get_last_user_visit_cookies(request: HttpRequest) -> list:
""" Служебная функция: проверяет есть ли куки о последних посещениях пользователя, и если есть возвращает их
:param request: HttpRequest -- входящий http-запрос
:return LastVisit: json -- загруженный json-объект из куки LastVisit
"""
if "LastVisit" in request.COOKIES:
try:
return json.loads(request.COOKIES["LastVisit"])
except (json.decoder.JSONDecodeError, TypeError, ValueError, KeyError, AttributeError):
return []
else:
return []
def get_last_user_visit_list(list_visit: list) -> list:
""" Служебная функция: получает список с посещенных страниц с ценовой выдачей (ListVisit), меняет в нем даты
на описание типа "три недели назад" и возвращает обратно.
@@ -417,10 +402,7 @@ def compare_offers(request: HttpRequest, to_compare: str = "1,2") -> HttpRespons
except SetKit.DoesNotExist:
pass
to_template.update({
# получаем последние визиты клиента через куки
'LAST_VISIT': get_last_user_visit_list(get_last_user_visit_cookies(request)[:3]),
# получаем последние визиты всех посетителей из базы
# id2log, log_visit = get_last_all_user_visit_list()
'LOG_VISIT': get_last_all_user_visit_list(),
'ticks': float(time.perf_counter() - time_start)
})

View File

@@ -158,9 +158,6 @@ class ReportOneWinPriceTests(TestCase):
sOfferActive=False,
)
@patch("web.prices.get_last_all_user_visit_list", return_value=[])
@patch("web.prices.get_last_user_visit_list", return_value=[])
@patch("web.prices.get_last_user_visit_cookies", return_value=[])
@patch("web.prices.get_flaps_for_mini_pictures", return_value="img/test-mini.png")
@patch(
"web.prices.get_flaps_for_big_pictures",
@@ -178,9 +175,6 @@ class ReportOneWinPriceTests(TestCase):
self,
mocked_big_pictures,
mocked_mini_pictures,
mocked_cookies,
mocked_last_visits,
mocked_all_visits,
):
"""Вьюха должна собирать тот же ключевой контекст, но уже без raw SQL."""
request = self.factory.get(
@@ -216,9 +210,6 @@ class ReportOneWinPriceTests(TestCase):
self.assertIn("META_DATA_PUBLISH", context)
self.assertTrue(mocked_big_pictures.called)
self.assertTrue(mocked_mini_pictures.called)
self.assertTrue(mocked_cookies.called)
self.assertTrue(mocked_last_visits.called)
self.assertTrue(mocked_all_visits.called)
def test_report_one_win_price_redirects_to_canonical_dimensions(self):
"""Если SEO-размеры в URL неверные, вьюха должна редиректить на канонический URL."""

View File

@@ -156,14 +156,8 @@ class CatalogProfileViewTests(TestCase):
return profile, sibling, brand, blog
@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_cookies", return_value=["cookie-1", "cookie-2", "cookie-3"])
def test_catalog_profile_handles_empty_catalog(
self,
mocked_cookies,
mocked_last_visits,
mocked_all_visits,
):
"""Пустой каталог не должен падать и должен отдавать ожидаемый контекст."""
with self.assertNumQueries(1):
@@ -174,20 +168,10 @@ class CatalogProfileViewTests(TestCase):
self.assertEqual(context["CATALOG_PROFILE_NUM"], "0 профилей")
self.assertEqual(context["CATALOG_MANUFACT_NUM"], 0)
self.assertEqual(context["CATALOG_PROFILE_MAN1_NAME2"], [])
self.assertEqual(context["LAST_VISIT"], ["last-visits"])
self.assertEqual(context["LOG_VISIT"], ["all-visits"])
self.assertTrue(mocked_cookies.called)
self.assertTrue(mocked_last_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_groups_and_sorts_profiles(
self,
mocked_cookies,
mocked_last_visits,
mocked_all_visits,
):
"""Каталог должен группировать профили по производителю и сохранять сортировку."""
self._create_profile(name="Alpha Basic", brief="Альфа База", manufacturer="Альфа", days_ago=5)
@@ -224,20 +208,9 @@ class CatalogProfileViewTests(TestCase):
# Проверяем итоговые счетчики и структуру контекста.
self.assertEqual(context["CATALOG_MANUFACT_NUM"], 2)
self.assertEqual(context["CATALOG_PROFILE_NUM"], "4 профиля")
self.assertEqual(context["LAST_VISIT"], [])
self.assertEqual(context["LOG_VISIT"], [])
self.assertTrue(mocked_cookies.called)
self.assertTrue(mocked_last_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)
@@ -248,14 +221,8 @@ class CatalogProfileViewTests(TestCase):
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()
@@ -287,7 +254,4 @@ class CatalogProfileViewTests(TestCase):
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)

View File

@@ -16,7 +16,7 @@ import pytils
def main_init(request: HttpRequest) -> HttpResponse:
""" Главная страница (статичная, только с проверками куков)
""" Главная страница (статичная, только с проверками кук)
:param request: входящий http-запрос
:return response: исходящий http-ответ
@@ -28,22 +28,6 @@ def main_init(request: HttpRequest) -> HttpResponse:
# стоят куки, и это не первый визит
num_viz = request.COOKIES["NumVisit"] # читаем число визитов
num_viz = int(num_viz) + 1 # увеличиваем порядковый номер визитов
# ПРОВЕРЯЧЕМ КУКИ ПРОСМОТРЕ ЦЕНОВЫХ ПРЕДЛОЖЕНИЙ
if "LastVisit" in request.COOKIES:
# стоят куки
last_visit = json.loads(request.COOKIES["LastVisit"])
last_visit2 = []
for i in last_visit:
last_visit2.append({
"Time": datetime.datetime.fromtimestamp(i["Time"]),
"LastURL": i["LastURL"],
"LastAddress": i["LastAddress"],
"LastApart": i["LastApart"]
})
to_template.update({'LAST_VISIT': last_visit2[:3]})
else:
to_template.update({'LAST_VISIT': None})
to_template.update({'META_DOCUMENT_STATE': u"Static"}) # Эта страничка статичная (в шаблон)
to_template.update({'NV': num_viz})
# to_template.update(csrf(request)) # токен, для метода POST и GET
response = render(request, "index.html", to_template)

View File

@@ -0,0 +1,98 @@
/**
* Логика записи визитов пользователя в cookies.
* Отслеживает последние посещения страниц с ценовой выдачей.
*
* Используемые данные из HTML:
* - data-current-url: текущий URL страницы
* - data-address: адрес здания
* - data-apart: тип квартиры
*
* Сохраняет в куку 'LastVisit' максимум 3 последних визита в формате JSON.
*/
function trackUserVisit(currentUrl, address, apart) {
// Функция для получения значения куки по имени
function getCookieValue(name) {
try {
if (document.cookie) {
const cookies = document.cookie.split('; ');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=');
if (cookieName === name) {
return decodeURIComponent(cookieValue);
}
}
}
} catch (e) {
console.warn('Ошибка при чтении куки:', e);
}
return null;
}
// Функция для установки куки с заданным сроком жизни
function setCookie(name, value, maxAge) {
try {
const cookieValue = encodeURIComponent(value);
let cookieString = `${name}=${cookieValue}; path=/`;
if (maxAge) {
cookieString += `; max-age=${maxAge}`;
}
document.cookie = cookieString;
} catch (e) {
console.warn('Ошибка при установке куки:', e);
}
}
// Получаем последние визиты из куки (если есть)
let lastVisits = [];
const cookieValue = getCookieValue('LastVisit');
if (cookieValue) {
try {
lastVisits = JSON.parse(cookieValue);
} catch (e) {
console.warn('Ошибка при разборе JSON из куки LastVisit:', e);
lastVisits = [];
}
}
// Создаём новый item посещения с текущей информацией
const newItem = {
LastURL: currentUrl,
LastAddress: address,
LastApart: apart,
Time: performance.now() // используем performance.now() как аналог time.perf_counter() в Python
};
// Добавляем новый item в начало списка
lastVisits.unshift(newItem);
// Оставляем максимум 3 последних записи
lastVisits = lastVisits.slice(0, 4);
// Упаковываем в JSON (JSON.stringify без пробелов для компактности)
const jsonData = JSON.stringify(lastVisits);
// Устанавливаем куки на 91 день (7862400 секунд)
setCookie('LastVisit', jsonData, 7862400);
}
/**
* Инициализация отслеживания при загрузке документа.
* Ищет элемент с атрибутами data-current-url, data-address, data-apart
* и вызывает trackUserVisit с полученными значениями.
*/
document.addEventListener('DOMContentLoaded', function() {
// Ищем элемент со встроенными данными (например, скрытый div в шаблоне)
const trackingElement = document.querySelector('[data-current-url]');
if (trackingElement) {
const currentUrl = trackingElement.getAttribute('data-current-url');
const address = trackingElement.getAttribute('data-address');
const apart = trackingElement.getAttribute('data-apart');
if (currentUrl && address && apart) {
trackUserVisit(currentUrl, address, apart);
}
}
});