Files
core/services/viewed.py

426 lines
19 KiB
Python
Raw Normal View History

2024-08-07 13:25:48 +03:00
import asyncio
2024-01-25 22:41:27 +03:00
import os
import time
2025-08-17 16:33:54 +03:00
from datetime import UTC, datetime, timedelta
from pathlib import Path
2025-08-17 16:33:54 +03:00
from typing import ClassVar
2025-03-20 11:55:21 +03:00
2024-08-07 13:37:08 +03:00
# ga
2024-01-28 16:26:40 +03:00
from google.analytics.data_v1beta import BetaAnalyticsDataClient
2025-02-11 12:00:35 +03:00
from google.analytics.data_v1beta.types import (
DateRange,
Dimension,
Metric,
RunReportRequest,
)
2024-12-12 02:03:19 +03:00
from google.analytics.data_v1beta.types import Filter as GAFilter
2022-11-22 01:23:16 +03:00
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации - **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)` ### 🧪 Тестирование - **Исправление тестов** - адаптация к новой структуре моделей - **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py` - **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев - **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями - **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода ### 🔧 Рефакторинг - **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру - **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль - **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры - **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей - **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки ### 🔧 Авторизация с cookies - **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization - **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно - **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token` - **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession` - **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author` - **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами - **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession` - **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации ### 📝 Документация - **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа - Обновлена документация RBAC - Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
from orm.author import Author
2024-01-23 16:04:38 +03:00
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
2025-08-17 17:56:31 +03:00
from storage.db import local_session
from storage.redis import redis
2024-08-07 13:37:50 +03:00
from utils.logger import root_logger as logger
2024-01-23 16:04:38 +03:00
2024-08-07 13:37:08 +03:00
GOOGLE_KEYFILE_PATH = os.environ.get("GOOGLE_KEYFILE_PATH", "/dump/google-service.json")
GOOGLE_PROPERTY_ID = os.environ.get("GOOGLE_PROPERTY_ID", "")
2024-01-28 15:54:38 +03:00
2024-01-13 15:44:56 +03:00
2022-11-18 20:54:37 +03:00
class ViewedStorage:
2025-04-14 19:53:14 +03:00
"""
Класс для хранения и доступа к данным о просмотрах.
Использует Redis в качестве основного хранилища и Google Analytics для сбора новых данных.
"""
2025-04-15 20:16:01 +03:00
2024-08-07 13:37:08 +03:00
lock = asyncio.Lock()
views_by_shout: ClassVar[dict] = {}
shouts_by_topic: ClassVar[dict] = {}
shouts_by_author: ClassVar[dict] = {}
2024-08-07 13:37:08 +03:00
views = None
2024-01-23 16:04:38 +03:00
period = 60 * 60 # каждый час
2025-08-17 16:33:54 +03:00
analytics_client: BetaAnalyticsDataClient | None = None
2024-08-07 13:37:08 +03:00
auth_result = None
2024-11-02 04:44:07 +03:00
running = False
2025-04-14 19:53:14 +03:00
redis_views_key = None
last_update_timestamp = 0
2025-08-17 16:33:54 +03:00
start_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
_background_task: asyncio.Task | None = None
2022-11-18 20:54:37 +03:00
2022-11-20 10:48:40 +03:00
@staticmethod
async def init() -> None:
2025-04-14 19:53:14 +03:00
"""Подключение к клиенту Google Analytics и загрузка данных о просмотрах из Redis"""
2022-11-22 10:29:54 +03:00
self = ViewedStorage
2024-08-07 13:37:08 +03:00
async with self.lock:
2025-04-14 19:53:14 +03:00
# Загрузка предварительно подсчитанных просмотров из Redis
await self.load_views_from_redis()
2024-08-07 13:37:08 +03:00
os.environ.setdefault("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_KEYFILE_PATH)
if GOOGLE_KEYFILE_PATH and Path(GOOGLE_KEYFILE_PATH).is_file():
2024-08-07 13:37:08 +03:00
# Using a default constructor instructs the client to use the credentials
# specified in GOOGLE_APPLICATION_CREDENTIALS environment variable.
self.analytics_client = BetaAnalyticsDataClient()
2024-10-23 11:29:44 +03:00
logger.info(" * Google Analytics credentials accepted")
2024-08-07 13:37:08 +03:00
# Запуск фоновой задачи
task = asyncio.create_task(self.worker())
# Store reference to prevent garbage collection
self._background_task = task
2024-08-07 13:37:08 +03:00
else:
2024-10-15 11:12:09 +03:00
logger.warning(" * please, add Google Analytics credentials file")
2024-11-02 04:44:07 +03:00
self.running = False
2022-11-20 10:48:40 +03:00
2024-01-22 19:17:39 +03:00
@staticmethod
async def load_views_from_redis() -> None:
2025-04-14 19:53:14 +03:00
"""Загрузка предварительно подсчитанных просмотров из Redis"""
2024-01-22 19:17:39 +03:00
self = ViewedStorage
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
# Подключаемся к Redis если соединение не установлено
2025-08-01 11:09:01 +03:00
try:
if not await redis.ping():
await redis.connect()
except Exception as e:
logger.warning(f"Redis connection check failed: {e}")
# Try to connect anyway
2025-04-14 19:53:14 +03:00
await redis.connect()
2025-04-15 20:16:01 +03:00
2025-05-25 23:21:53 +03:00
# Логируем настройки Redis соединения
2025-05-26 13:31:25 +03:00
logger.info("* Redis connected")
2025-05-25 23:21:53 +03:00
2025-04-14 19:53:14 +03:00
# Получаем список всех ключей migrated_views_* и находим самый последний
keys = await redis.execute("KEYS", "migrated_views_*")
2025-08-01 11:09:01 +03:00
if keys is None:
keys = []
logger.warning("Redis KEYS command returned None, treating as empty list")
logger.info("Raw Redis result for 'KEYS migrated_views_*': %d", len(keys))
2025-05-29 12:37:39 +03:00
2025-05-25 23:21:53 +03:00
# Декодируем байтовые строки, если есть
if keys and isinstance(keys[0], bytes):
2025-05-29 12:37:39 +03:00
keys = [k.decode("utf-8") for k in keys]
logger.info("Decoded keys: %s", keys)
2025-05-29 12:37:39 +03:00
2025-04-14 19:53:14 +03:00
if not keys:
2025-08-27 16:37:34 +03:00
logger.info(" * No migrated_views keys found in Redis - views will be 0")
2025-04-14 19:53:14 +03:00
return
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
# Фильтруем только ключи timestamp формата (исключаем migrated_views_slugs)
timestamp_keys = [k for k in keys if k != "migrated_views_slugs"]
logger.info("Timestamp keys after filtering: %s", timestamp_keys)
2025-05-29 12:37:39 +03:00
2025-04-14 19:53:14 +03:00
if not timestamp_keys:
2025-08-27 16:37:34 +03:00
logger.info(" * No migrated_views timestamp keys found in Redis - views will be 0")
2025-04-14 19:53:14 +03:00
return
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
# Сортируем по времени создания (в названии ключа) и берем последний
timestamp_keys.sort()
latest_key = timestamp_keys[-1]
self.redis_views_key = latest_key
logger.info("Selected latest key: %s", latest_key)
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
# Получаем метку времени создания для установки start_date
timestamp = await redis.execute("HGET", latest_key, "_timestamp")
if timestamp:
self.last_update_timestamp = int(timestamp)
2025-08-17 16:33:54 +03:00
timestamp_dt = datetime.fromtimestamp(int(timestamp), tz=UTC)
2025-04-14 19:53:14 +03:00
self.start_date = timestamp_dt.strftime("%Y-%m-%d")
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
# Если данные сегодняшние, считаем их актуальными
2025-08-17 16:33:54 +03:00
now_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
2024-10-15 11:12:09 +03:00
if now_date == self.start_date:
2025-04-14 19:53:14 +03:00
logger.info(" * Views data is up to date!")
2024-03-12 15:57:46 +03:00
else:
logger.warning("Views data is from %s, may need update", self.start_date)
2025-04-15 20:16:01 +03:00
2025-08-27 16:37:34 +03:00
# 🔎 ЗАГРУЖАЕМ ДАННЫЕ из Redis в views_by_shout
logger.info("🔍 Loading views data from Redis key: %s", latest_key)
# Получаем все данные из hash
views_data = await redis.execute("HGETALL", latest_key)
if views_data and len(views_data) > 0:
# Преобразуем список [key1, value1, key2, value2] в словарь
views_dict = {}
try:
# Проверяем что views_data это словарь или список
if isinstance(views_data, dict):
# Если это уже словарь
for key, value in views_data.items():
key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
value_str = value.decode("utf-8") if isinstance(value, bytes) else str(value)
if not key_str.startswith("_"):
try:
views_dict[key_str] = int(value_str)
except (ValueError, TypeError):
logger.warning(f"🔍 Invalid views value for {key_str}: {value_str}")
elif isinstance(views_data, list | tuple):
# Если это список [key1, value1, key2, value2]
for i in range(0, len(views_data), 2):
if i + 1 < len(views_data):
key = (
views_data[i].decode("utf-8")
if isinstance(views_data[i], bytes)
else str(views_data[i])
)
value = (
views_data[i + 1].decode("utf-8")
if isinstance(views_data[i + 1], bytes)
else str(views_data[i + 1])
)
# Пропускаем служебные ключи
if not key.startswith("_"):
try:
views_dict[key] = int(value)
except (ValueError, TypeError):
logger.warning(f"🔍 Invalid views value for {key}: {value}")
else:
logger.warning(f"🔍 Unexpected Redis data format: {type(views_data)}")
# Загружаем данные в класс
self.views_by_shout.update(views_dict)
logger.info("🔍 Loaded %d shouts with views from Redis", len(views_dict))
# Показываем образцы загруженных данных только если есть данные
if views_dict:
sample_items = list(views_dict.items())[:3]
logger.info("🔍 Sample loaded data: %s", sample_items)
else:
logger.debug("🔍 No valid views data found in Redis hash - views will be 0")
except Exception as e:
logger.warning(f"🔍 Error parsing Redis views data: {e} - views will be 0")
else:
logger.debug("🔍 Redis hash is empty for key: %s - views will be 0", latest_key)
2025-04-14 19:53:14 +03:00
# Выводим информацию о количестве загруженных записей
total_entries = await redis.execute("HGET", latest_key, "_total")
if total_entries:
logger.info("%s shouts with views loaded from Redis key: %s", total_entries, latest_key)
2024-01-22 19:17:39 +03:00
logger.info("Found migrated_views keys: %s", keys)
2025-05-25 23:21:53 +03:00
2024-08-07 13:37:08 +03:00
# noinspection PyTypeChecker
2022-11-22 01:23:16 +03:00
@staticmethod
async def update_pages() -> None:
2025-04-14 19:53:14 +03:00
"""Запрос всех страниц от Google Analytics, отсортированных по количеству просмотров"""
2022-11-22 01:23:16 +03:00
self = ViewedStorage
2024-10-15 11:12:09 +03:00
logger.info(" ⎧ views update from Google Analytics ---")
2024-11-02 04:44:07 +03:00
if self.running:
2024-01-23 16:04:38 +03:00
try:
start = time.time()
2024-08-07 13:37:08 +03:00
async with self.lock:
if self.analytics_client:
request = RunReportRequest(
property=f"properties/{GOOGLE_PROPERTY_ID}",
dimensions=[Dimension(name="pagePath")],
metrics=[Metric(name="screenPageViews")],
date_ranges=[DateRange(start_date=self.start_date, end_date="today")],
)
response = self.analytics_client.run_report(request)
if response and isinstance(response.rows, list):
slugs = set()
for row in response.rows:
print(
row.dimension_values[0].value,
row.metric_values[0].value,
)
# Извлечение путей страниц из ответа Google Analytics
if isinstance(row.dimension_values, list):
page_path = row.dimension_values[0].value
slug = page_path.split("discours.io/")[-1]
2024-12-12 02:03:19 +03:00
fresh_views = int(row.metric_values[0].value)
2024-08-07 13:37:08 +03:00
# Обновление данных в хранилище
self.views_by_shout[slug] = self.views_by_shout.get(slug, 0)
2024-12-12 02:03:19 +03:00
self.views_by_shout[slug] += fresh_views
2024-08-07 13:37:08 +03:00
self.update_topics(slug)
# Запись путей страниц для логирования
slugs.add(slug)
logger.info("collected pages: %d", len(slugs))
2024-08-07 13:37:08 +03:00
end = time.time()
logger.info("views update time: %.2fs", end - start)
except (ConnectionError, TimeoutError, ValueError) as error:
2024-01-28 12:03:41 +03:00
logger.error(error)
2024-11-02 04:44:07 +03:00
self.running = False
2022-11-18 20:54:37 +03:00
2022-11-19 14:35:34 +03:00
@staticmethod
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
def get_shout(shout_slug: str = "", shout_id: int = 0) -> int:
2025-04-14 19:53:14 +03:00
"""
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
🔎 Синхронное получение метрики просмотров shout по slug или id из кеша.
Использует кешированные данные из views_by_shout (in-memory кеш).
Для обновления данных используется асинхронный фоновый процесс.
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
Args:
shout_slug: Slug публикации
shout_id: ID публикации
2025-04-15 20:16:01 +03:00
2025-04-14 19:53:14 +03:00
Returns:
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
int: Количество просмотров из кеша
2025-04-14 19:53:14 +03:00
"""
2022-11-19 14:35:34 +03:00
self = ViewedStorage
2025-04-15 20:16:01 +03:00
2025-08-27 16:37:34 +03:00
# 🔍 DEBUG: Логируем только если кеш пустой и это первый запрос
cache_size = len(self.views_by_shout)
if cache_size == 0 and shout_slug:
logger.debug(f"🔍 ViewedStorage cache is empty for slug '{shout_slug}'")
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
# 🔎 Используем только in-memory кеш для быстрого доступа
if shout_slug:
2025-08-27 16:37:34 +03:00
views = self.views_by_shout.get(shout_slug, 0)
if views > 0:
logger.debug(f"🔍 Found {views} views for slug '{shout_slug}'")
return views
2025-04-15 20:16:01 +03:00
2025-08-27 15:22:18 +03:00
# 🔎 Для ID ищем slug в БД и затем получаем views_count
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
if shout_id:
2025-08-27 15:22:18 +03:00
try:
with local_session() as session:
from orm.shout import Shout
2025-08-27 16:37:34 +03:00
2025-08-27 15:22:18 +03:00
shout = session.query(Shout).where(Shout.id == shout_id).first()
if shout and shout.slug:
2025-08-27 16:37:34 +03:00
views = self.views_by_shout.get(shout.slug, 0)
logger.debug(f"🔍 Found slug '{shout.slug}' for id {shout_id}, views: {views}")
return views
logger.debug(f"🔍 No shout found with id {shout_id} or missing slug")
2025-08-27 15:22:18 +03:00
except Exception as e:
logger.warning(f"Failed to get shout slug for id {shout_id}: {e}")
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
return 0
2025-04-15 20:16:01 +03:00
2025-08-27 16:37:34 +03:00
logger.debug("🔍 get_shout called without slug or id")
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
return 0
2023-11-03 13:10:22 +03:00
@staticmethod
async def get_shout_media(shout_slug: str) -> dict[str, int]:
2024-08-07 13:30:41 +03:00
"""Получение метрики воспроизведения shout по slug."""
2023-11-03 13:10:22 +03:00
self = ViewedStorage
2024-12-12 02:03:19 +03:00
# TODO: get media plays from Google Analytics
2024-08-07 13:30:41 +03:00
return self.views_by_shout.get(shout_slug, 0)
2022-11-19 14:35:34 +03:00
2022-11-21 08:18:50 +03:00
@staticmethod
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
def get_topic(topic_slug: str) -> int:
2024-08-07 13:30:41 +03:00
"""Получение суммарного значения просмотров темы."""
2022-11-21 08:18:50 +03:00
self = ViewedStorage
2025-04-14 19:53:14 +03:00
views_count = 0
for shout_slug in self.shouts_by_topic.get(topic_slug, []):
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
views_count += self.get_shout(shout_slug=shout_slug)
2025-04-14 19:53:14 +03:00
return views_count
2022-11-21 08:18:50 +03:00
2024-01-22 18:42:45 +03:00
@staticmethod
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
def get_author(author_slug: str) -> int:
2024-08-07 13:30:41 +03:00
"""Получение суммарного значения просмотров автора."""
2024-01-22 18:42:45 +03:00
self = ViewedStorage
2025-04-14 19:53:14 +03:00
views_count = 0
for shout_slug in self.shouts_by_author.get(author_slug, []):
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
views_count += self.get_shout(shout_slug=shout_slug)
2025-04-14 19:53:14 +03:00
return views_count
2024-01-22 18:42:45 +03:00
2022-11-22 16:58:55 +03:00
@staticmethod
def update_topics(shout_slug: str) -> None:
2024-08-07 13:37:08 +03:00
"""Обновление счетчиков темы по slug shout"""
2022-11-22 16:58:55 +03:00
self = ViewedStorage
2023-11-22 21:23:15 +03:00
with local_session() as session:
2025-04-14 19:53:14 +03:00
# Определение вспомогательной функции для избежания повторения кода
def update_groups(dictionary: dict, key: str, value: str) -> None:
dictionary[key] = list({*dictionary.get(key, []), value})
2024-01-22 21:20:17 +03:00
2024-01-23 16:04:38 +03:00
# Обновление тем и авторов с использованием вспомогательной функции
2024-11-02 04:44:07 +03:00
for [_st, topic] in (
2025-05-29 12:37:39 +03:00
session.query(ShoutTopic, Topic).join(Topic).join(Shout).where(Shout.slug == shout_slug).all()
2024-01-25 22:41:27 +03:00
):
2024-01-22 21:20:17 +03:00
update_groups(self.shouts_by_topic, topic.slug, shout_slug)
2024-11-02 04:44:07 +03:00
for [_st, author] in (
2025-05-29 12:37:39 +03:00
session.query(ShoutAuthor, Author).join(Author).join(Shout).where(Shout.slug == shout_slug).all()
2024-01-25 22:41:27 +03:00
):
2024-01-22 21:20:17 +03:00
update_groups(self.shouts_by_author, author.slug, shout_slug)
2024-01-22 18:42:45 +03:00
2024-11-02 04:44:07 +03:00
@staticmethod
async def stop() -> None:
2024-11-02 04:44:07 +03:00
"""Остановка фоновой задачи"""
self = ViewedStorage
async with self.lock:
self.running = False
logger.info("ViewedStorage worker was stopped.")
2023-11-03 13:10:22 +03:00
@staticmethod
async def worker() -> None:
2024-08-07 13:37:08 +03:00
"""Асинхронная задача обновления"""
2022-11-22 01:23:16 +03:00
failed = 0
2022-11-22 10:29:54 +03:00
self = ViewedStorage
2023-10-06 01:45:32 +03:00
2024-11-02 04:44:07 +03:00
while self.running:
2024-11-02 04:28:16 +03:00
try:
await self.update_pages()
failed = 0
except (ConnectionError, TimeoutError, ValueError) as exc:
2024-11-02 04:28:16 +03:00
failed += 1
logger.debug(exc)
logger.info("update failed #%d, wait 10 secs", failed)
2024-11-02 04:28:16 +03:00
if failed > 3:
logger.info(" - views update failed, not trying anymore")
2024-11-02 04:44:07 +03:00
self.running = False
2024-11-02 04:28:16 +03:00
break
if failed == 0:
2025-08-17 16:33:54 +03:00
when = datetime.now(UTC) + timedelta(seconds=self.period)
2024-11-02 04:28:16 +03:00
t = format(when.astimezone().isoformat())
logger.info(" ⎩ next update: %s", t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
2024-11-02 04:28:16 +03:00
await asyncio.sleep(self.period)
else:
await asyncio.sleep(10)
logger.info(" - try to update views again")
2024-12-12 02:03:19 +03:00
@staticmethod
async def update_slug_views(slug: str) -> int:
"""
Получает fresh статистику просмотров для указанного slug.
Args:
slug: Идентификатор страницы
Returns:
int: Количество просмотров
"""
self = ViewedStorage
if not self.analytics_client:
logger.warning("Google Analytics client not initialized")
return 0
try:
# Создаем фильтр для точного совпадения конца URL
request = RunReportRequest(
property=f"properties/{GOOGLE_PROPERTY_ID}",
date_ranges=[DateRange(start_date=self.start_date, end_date="today")],
dimensions=[Dimension(name="pagePath")],
dimension_filter=GAFilter(
field_name="pagePath",
string_filter=GAFilter.StringFilter(
value=f".*/{slug}$", # Используем регулярное выражение для точного совпадения конца URL
match_type=GAFilter.StringFilter.MatchType.FULL_REGEXP,
case_sensitive=False, # Включаем чувствительность к регистру для точности
),
),
metrics=[Metric(name="screenPageViews")],
)
response = self.analytics_client.run_report(request)
if not response.rows:
return 0
views = int(response.rows[0].metric_values[0].value)
except (ConnectionError, ValueError, AttributeError):
logger.exception("Google Analytics API Error")
return 0
else:
2024-12-12 02:03:19 +03:00
# Кэшируем результат
self.views_by_shout[slug] = views
return views