diff --git a/resolvers/follower.py b/resolvers/follower.py index c0918866..f06b002f 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -25,8 +25,36 @@ from utils.logger import root_logger as logger def get_entity_field_name(entity_type: str) -> str: - """Возвращает имя поля для связи с сущностью в модели подписчика""" - entity_field_mapping = {"author": "following", "topic": "topic", "community": "community", "shout": "shout"} + """ + Возвращает имя поля для связи с сущностью в модели подписчика. + + Эта функция используется для определения правильного поля в моделях подписчиков + (AuthorFollower, TopicFollower, CommunityFollower, ShoutReactionsFollower) при создании + или проверке подписки. + + Args: + entity_type: Тип сущности в нижнем регистре ('author', 'topic', 'community', 'shout') + + Returns: + str: Имя поля в модели подписчика ('following', 'topic', 'community', 'shout') + + Raises: + ValueError: Если передан неизвестный тип сущности + + Examples: + >>> get_entity_field_name('author') + 'following' + >>> get_entity_field_name('topic') + 'topic' + >>> get_entity_field_name('invalid') + ValueError: Unknown entity_type: invalid + """ + entity_field_mapping = { + "author": "following", # AuthorFollower.following -> Author + "topic": "topic", # TopicFollower.topic -> Topic + "community": "community", # CommunityFollower.community -> Community + "shout": "shout" # ShoutReactionsFollower.shout -> Shout + } if entity_type not in entity_field_mapping: msg = f"Unknown entity_type: {entity_type}" raise ValueError(msg) @@ -36,8 +64,42 @@ def get_entity_field_name(entity_type: str) -> str: @mutation.field("follow") @login_required async def follow( - _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None + _: None, + info: GraphQLResolveInfo, + what: str, + slug: str = "", + entity_id: int | None = None ) -> dict[str, Any]: + """ + GraphQL мутация для создания подписки на автора, тему, сообщество или публикацию. + + Эта функция обрабатывает все типы подписок в системе, включая: + - Подписку на автора (AUTHOR) + - Подписку на тему (TOPIC) + - Подписку на сообщество (COMMUNITY) + - Подписку на публикацию (SHOUT) + + Args: + _: None - Стандартный параметр GraphQL (не используется) + info: GraphQLResolveInfo - Контекст GraphQL запроса, содержит информацию об авторизованном пользователе + what: str - Тип сущности для подписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT') + slug: str - Slug сущности (например, 'author-slug' или 'topic-slug') + entity_id: int | None - ID сущности (альтернатива slug) + + Returns: + dict[str, Any] - Результат операции: + { + "success": bool, # Успешность операции + "error": str | None, # Текст ошибки если есть + "authors": Author[], # Обновленные авторы (для кеширования) + "topics": Topic[], # Обновленные темы (для кеширования) + "entity_id": int | None # ID созданной подписки + } + + Raises: + ValueError: При передаче некорректных параметров + DatabaseError: При проблемах с базой данных + """ logger.debug("Начало выполнения функции 'follow'") viewer_id = info.context.get("author", {}).get("id") follower_dict = info.context.get("author") or {} @@ -51,7 +113,9 @@ async def follow( await redis.execute("DEL", f"author:id:{viewer_id}") logger.debug(f"Инвалидирован кеш подписок follower'а: {cache_key_pattern}") + # Проверка авторизации пользователя if not viewer_id: + logger.warning("Попытка подписаться без авторизации") return {"error": "Access denied"} logger.debug(f"follower: {follower_dict}") @@ -63,6 +127,7 @@ async def follow( follower_id = follower_dict.get("id") logger.debug(f"follower_id: {follower_id}") + # Маппинг типов сущностей на их классы и методы кеширования entity_classes = { "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), @@ -205,13 +270,87 @@ async def follow( @mutation.field("unfollow") @login_required async def unfollow( - _: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None + _: None, + info: GraphQLResolveInfo, + what: str, + slug: str = "", + entity_id: int | None = None ) -> dict[str, Any]: + """ + GraphQL мутация для отмены подписки на автора, тему, сообщество или публикацию. + + Эта функция обрабатывает отмену всех типов подписок в системе, включая: + - Отписку от автора (AUTHOR) + - Отписку от темы (TOPIC) + - Отписку от сообщества (COMMUNITY) + - Отписку от публикации (SHOUT) + + Процесс отмены подписки: + 1. Проверка авторизации пользователя + 2. Поиск существующей подписки в базе данных + 3. Удаление подписки если она найдена + 4. Инвалидация кеша для обновления данных + 5. Отправка уведомлений об отписке + + Args: + _: None - Стандартный параметр GraphQL (не используется) + info: GraphQLResolveInfo - Контекст GraphQL запроса, содержит информацию об авторизованном пользователе + what: str - Тип сущности для отписки ('AUTHOR', 'TOPIC', 'COMMUNITY', 'SHOUT') + slug: str - Slug сущности (например, 'author-slug' или 'topic-slug') + entity_id: int | None - ID сущности (альтернатива slug) + + Returns: + dict[str, Any] - Результат операции: + { + "success": bool, # Успешность операции + "error": str | None, # Текст ошибки если есть + "authors": Author[], # Обновленные авторы (для кеширования) + "topics": Topic[], # Обновленные темы (для кеширования) + } + + Raises: + ValueError: При передаче некорректных параметров + DatabaseError: При проблемах с базой данных + + Examples: + # Отписка от автора + mutation { + unfollow(what: "AUTHOR", slug: "author-slug") { + success + error + } + } + + # Отписка от темы + mutation { + unfollow(what: "TOPIC", slug: "topic-slug") { + success + error + } + } + + # Отписка от сообщества + mutation { + unfollow(what: "COMMUNITY", slug: "community-slug") { + success + error + } + } + + # Отписка от публикации + mutation { + unfollow(what: "SHOUT", entity_id: 123) { + success + error + } + } + """ logger.debug("Начало выполнения функции 'unfollow'") viewer_id = info.context.get("author", {}).get("id") follower_dict = info.context.get("author") or {} # ✅ КРИТИЧНО: Инвалидируем кеш В САМОМ НАЧАЛЕ, если пользователь авторизован + # чтобы предотвратить чтение старых данных при последующей перезагрузке if viewer_id: entity_type = what.lower() cache_key_pattern = f"author:follows-{entity_type}s:{viewer_id}" @@ -219,7 +358,9 @@ async def unfollow( await redis.execute("DEL", f"author:id:{viewer_id}") logger.debug(f"Инвалидирован кеш подписок В НАЧАЛЕ операции unfollow: {cache_key_pattern}") + # Проверка авторизации пользователя if not viewer_id: + logger.warning("Попытка отписаться без авторизации") return {"error": "Access denied"} logger.debug(f"follower: {follower_dict}") @@ -231,6 +372,7 @@ async def unfollow( follower_id = follower_dict.get("id") logger.debug(f"follower_id: {follower_id}") + # Маппинг типов сущностей на их классы и методы кеширования entity_classes = { "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),