Files
core/resolvers/feed.py

180 lines
7.4 KiB
Python
Raw Permalink Normal View History

2025-07-31 18:55:59 +03:00
from typing import Any
from graphql import GraphQLResolveInfo
2025-07-31 18:55:59 +03:00
from sqlalchemy import Select, and_, select
2024-11-01 09:50:19 +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, AuthorFollower
2024-11-01 09:50:19 +03:00
from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic
2024-11-01 11:09:16 +03:00
from orm.topic import Topic, TopicFollower
2025-02-11 12:00:35 +03:00
from resolvers.reader import (
apply_options,
get_shouts_with_links,
has_field,
query_with_stat,
)
2024-11-01 09:50:19 +03:00
from services.auth import login_required
2025-08-17 17:56:31 +03:00
from storage.db import local_session
from storage.schema import query
2024-11-01 09:50:19 +03:00
from utils.logger import root_logger as logger
2024-11-01 15:06:21 +03:00
2024-11-01 09:50:19 +03:00
@query.field("load_shouts_coauthored")
@login_required
async def load_shouts_coauthored(_: None, info: GraphQLResolveInfo, options: dict) -> list[Shout]:
2024-11-01 09:50:19 +03:00
"""
Загрузка публикаций, написанных в соавторстве с пользователем.
:param info: Информаци о контексте GraphQL.
:param options: Опции фильтрации и сортировки.
:return: Список публикаций в соавтостве.
"""
author_id = info.context.get("author", {}).get("id")
if not author_id:
return []
2024-11-01 13:50:47 +03:00
q = query_with_stat(info)
2025-07-31 18:55:59 +03:00
q = q.where(Shout.authors.any(id=author_id))
2024-11-01 13:50:47 +03:00
q, limit, offset = apply_options(q, options)
2024-11-01 09:50:19 +03:00
return get_shouts_with_links(info, q, limit, offset=offset)
@query.field("load_shouts_discussed")
@login_required
async def load_shouts_discussed(_: None, info: GraphQLResolveInfo, options: dict) -> list[Shout]:
2024-11-01 09:50:19 +03:00
"""
Загрузка публикаций, которые обсуждались пользователем.
:param info: Информация о контексте GraphQL.
:param options: Опции фильтрации и сортировки.
:return: Список публикаций, обсужденых пользователем.
"""
author_id = info.context.get("author", {}).get("id")
if not author_id:
return []
2024-11-01 13:50:47 +03:00
q = query_with_stat(info)
options["filters"]["commented"] = True
2024-11-01 09:50:19 +03:00
q, limit, offset = apply_options(q, options, author_id)
return get_shouts_with_links(info, q, limit, offset=offset)
2025-07-31 18:55:59 +03:00
def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict[str, Any]) -> list[Shout]:
2024-11-01 09:50:19 +03:00
"""
2024-11-01 11:29:41 +03:00
Загружает публикации, на которые подписан автор.
2024-11-01 09:50:19 +03:00
2024-11-01 13:50:47 +03:00
- по авторам
- по темам
- по реакциям
2024-11-01 11:29:41 +03:00
:param info: Информация о контексте GraphQL.
:param follower_id: Идентификатор автора.
2024-11-01 10:04:32 +03:00
:param options: Опции фильтрации и сортировки.
2024-11-01 09:50:19 +03:00
:return: Список публикаций.
"""
2024-11-01 13:50:47 +03:00
q = query_with_stat(info)
[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
reader_followed_authors: Select = select(AuthorFollower.following).where(AuthorFollower.follower == follower_id)
2025-07-31 18:55:59 +03:00
reader_followed_topics: Select = select(TopicFollower.topic).where(TopicFollower.follower == follower_id)
reader_followed_shouts: Select = select(ShoutReactionsFollower.shout).where(
ShoutReactionsFollower.follower == follower_id
)
2024-11-01 13:50:47 +03:00
followed_subquery = (
select(Shout.id)
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
.join(ShoutTopic, ShoutTopic.shout == Shout.id)
.where(
ShoutAuthor.author.in_(reader_followed_authors)
| ShoutTopic.topic.in_(reader_followed_topics)
| Shout.id.in_(reader_followed_shouts)
)
2024-11-01 14:00:19 +03:00
.scalar_subquery()
2024-11-01 11:29:41 +03:00
)
2025-07-31 18:55:59 +03:00
q = q.where(Shout.id.in_(followed_subquery))
2024-11-01 13:50:47 +03:00
q, limit, offset = apply_options(q, options)
return get_shouts_with_links(info, q, limit, offset=offset)
2024-11-01 09:50:19 +03:00
2024-11-01 11:29:41 +03:00
@query.field("load_shouts_followed_by")
async def load_shouts_followed_by(_: None, info: GraphQLResolveInfo, slug: str, options: dict) -> list[Shout]:
2024-11-01 09:50:19 +03:00
"""
2024-11-01 11:29:41 +03:00
Загружает публикации, на которые подписан автор по slug.
2024-11-01 09:50:19 +03:00
:param info: Информация о контексте GraphQL.
2024-11-01 11:29:41 +03:00
:param slug: Slug автора.
2024-11-01 10:04:32 +03:00
:param options: Опции фильтрации и сортировки.
2024-11-01 09:50:19 +03:00
:return: Список публикаций.
"""
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.slug == slug).first()
2024-11-01 09:50:19 +03:00
if author:
2024-11-01 11:29:41 +03:00
follower_id = author.dict()["id"]
return shouts_by_follower(info, follower_id, options)
2024-11-01 09:50:19 +03:00
return []
2024-11-01 11:29:41 +03:00
@query.field("load_shouts_feed")
@login_required
async def load_shouts_feed(_: None, info: GraphQLResolveInfo, options: dict) -> list[Shout]:
2024-11-01 09:50:19 +03:00
"""
2024-11-01 11:29:41 +03:00
Загружает публикации, на которые подписан авторизованный пользователь.
2024-11-01 09:50:19 +03:00
:param info: Информация о контексте GraphQL.
2024-11-01 10:04:32 +03:00
:param options: Опции фильтрации и сортировки.
2024-11-01 09:50:19 +03:00
:return: Список публикаций.
"""
2024-11-01 11:29:41 +03:00
author_id = info.context.get("author", {}).get("id")
return shouts_by_follower(info, author_id, options) if author_id else []
2024-11-01 11:09:16 +03:00
@query.field("load_shouts_authored_by")
2025-07-31 18:55:59 +03:00
async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str, options: dict[str, Any]) -> list[Shout]:
2024-11-01 11:09:16 +03:00
"""
Загружает публикации, написанные автором по slug.
2024-11-01 14:00:19 +03:00
:param info: Информация о контексте GraphQL.
:param slug: Slug автора.
:param options: Опции фильтрации и сортировки.
:return: Список публикаций.
2024-11-01 11:09:16 +03:00
"""
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.slug == slug).first()
2024-11-01 11:09:16 +03:00
if author:
try:
author_id: int = author.dict()["id"]
2025-07-31 18:55:59 +03:00
q: Select = (
2024-11-01 13:50:47 +03:00
query_with_stat(info)
2024-11-01 11:29:41 +03:00
if has_field(info, "stat")
2025-07-31 18:55:59 +03:00
else select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
2024-11-01 11:29:41 +03:00
)
2025-07-31 18:55:59 +03:00
q = q.where(Shout.authors.any(id=author_id))
2024-11-01 11:09:16 +03:00
q, limit, offset = apply_options(q, options, author_id)
return get_shouts_with_links(info, q, limit, offset=offset)
2024-11-01 11:09:16 +03:00
except Exception as error:
logger.debug(error)
return []
@query.field("load_shouts_with_topic")
2025-07-31 18:55:59 +03:00
async def load_shouts_with_topic(_: None, info: GraphQLResolveInfo, slug: str, options: dict[str, Any]) -> list[Shout]:
2024-11-01 11:09:16 +03:00
"""
Загружает публикации, связанные с темой по slug.
2024-11-01 14:00:19 +03:00
:param info: Информация о контексте GraphQL.
:param slug: Slug темы.
:param options: Опции фильтрации и сортировки.
:return: Список публикаций.
2024-11-01 11:09:16 +03:00
"""
with local_session() as session:
2025-07-31 18:55:59 +03:00
topic = session.query(Topic).where(Topic.slug == slug).first()
2024-11-01 11:09:16 +03:00
if topic:
try:
topic_id: int = topic.dict()["id"]
2025-07-31 18:55:59 +03:00
q: Select = (
2024-11-01 13:50:47 +03:00
query_with_stat(info)
2024-11-01 11:29:41 +03:00
if has_field(info, "stat")
2025-07-31 18:55:59 +03:00
else select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
2024-11-01 11:29:41 +03:00
)
2025-07-31 18:55:59 +03:00
q = q.where(Shout.topics.any(id=topic_id))
2024-11-01 11:29:41 +03:00
q, limit, offset = apply_options(q, options)
return get_shouts_with_links(info, q, limit, offset=offset)
2024-11-01 11:09:16 +03:00
except Exception as error:
logger.debug(error)
return []