From 4038c5dbf5a5ed095273413279d657bcdea1ef7e Mon Sep 17 00:00:00 2001 From: Untone Date: Thu, 2 Oct 2025 01:16:14 +0300 Subject: [PATCH] docs-restruct --- CHANGELOG.md | 10 +++ resolvers/follower.py | 138 +++++++++++++++++++++++++++++++++++++++++- resolvers/reaction.py | 61 +++++++++++++------ 3 files changed, 190 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdd9543..a66f81cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.9.29] - 2025-10-01 + +### πŸ”§ Fixed +- **Π€ΠΈΡ‡Π΅Ρ€Π΅Π½ΠΈΠ΅ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° Π»ΠΎΠ³ΠΈΠΊΠ° автоматичСского фичСрСния/расфичСрСния + - Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°ΡŽΡ‚ΡΡ всС ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ (LIKE, ACCEPT, PROOF), Π° Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ LIKE + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ подсчСт Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ Π² `check_to_unfeature`: ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ POSITIVE + NEGATIVE вмСсто Ρ‚ΠΎΠ»ΡŒΠΊΠΎ RATING_REACTIONS + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° явная ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° `reply_to.is_(None)` для ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² + - **РСвалидация кСша**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° рСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΉ, Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΈ Ρ‚Π΅ΠΌ ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ `featured_at` + - Π£Π»ΡƒΡ‡ΡˆΠ΅Π½ΠΎ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ процСсса фичСрСния + ## [0.9.28] - 2025-09-28 ### πŸͺ CRITICAL Cross-Origin Auth diff --git a/resolvers/follower.py b/resolvers/follower.py index c0918866..9cafb693 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) @@ -38,6 +66,36 @@ def get_entity_field_name(entity_type: str) -> str: async def follow( _: 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 +109,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 +123,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), @@ -207,11 +268,81 @@ async def follow( async def unfollow( _: 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 +350,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 +364,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), diff --git a/resolvers/reaction.py b/resolvers/reaction.py index cae61bf8..862fe2a9 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -143,27 +143,29 @@ def is_featured_author(session: Session, author_id: int) -> bool: def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool: """ - Make a shout featured if it receives more than 4 votes from authors. + Make a shout featured if it receives more than 4 votes from featured authors. :param session: Database session. :param approver_id: Approver author ID. :param reaction: Reaction object. :return: True if shout should be featured, else False. """ - is_positive_kind = reaction.get("kind") == ReactionKind.LIKE.value + # πŸ”§ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π»ΡŽΠ±ΡƒΡŽ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ Ρ€Π΅Π°ΠΊΡ†ΠΈΡŽ (LIKE, ACCEPT, PROOF), Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ LIKE + is_positive_kind = reaction.get("kind") in POSITIVE_REACTIONS if not reaction.get("reply_to") and is_positive_kind: # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Π½Π΅ содСрТит Π»ΠΈ пост Π±ΠΎΠ»Π΅Π΅ 20% Π΄ΠΈΠ·Π»Π°ΠΉΠΊΠΎΠ² # Если Π΄Π°, Ρ‚ΠΎ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ featured нСзависимо ΠΎΡ‚ количСства Π»Π°ΠΉΠΊΠΎΠ² if check_to_unfeature(session, reaction): return False - # Π‘ΠΎΠ±ΠΈΡ€Π°Π΅ΠΌ всСх Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², ΠΏΠΎΡΡ‚Π°Π²ΠΈΠ²ΡˆΠΈΡ… Π»Π°ΠΉΠΊ + # Π‘ΠΎΠ±ΠΈΡ€Π°Π΅ΠΌ всСх Π°Π²Ρ‚ΠΎΡ€ΠΎΠ², ΠΏΠΎΡΡ‚Π°Π²ΠΈΠ²ΡˆΠΈΡ… ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ Ρ€Π΅Π°ΠΊΡ†ΠΈΡŽ author_approvers = set() reacted_readers = ( session.query(Reaction.created_by) .where( Reaction.shout == reaction.get("shout"), Reaction.kind.in_(POSITIVE_REACTIONS), + Reaction.reply_to.is_(None), # Π½Π΅ рСакция Π½Π° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΈ (LIKE, DISLIKE) физичСски ΡƒΠ΄Π°Π»ΡΡŽΡ‚ΡΡ, поэтому Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ deleted_at Π½Π΅ Π½ΡƒΠΆΠ΅Π½ ) .distinct() @@ -189,7 +191,7 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool def check_to_unfeature(session: Session, reaction: dict) -> bool: """ Unfeature a shout if: - 1. Less than 5 positive votes, OR + 1. Less than 5 positive votes from featured authors, OR 2. 20% or more of reactions are negative. :param session: Database session. @@ -199,18 +201,8 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool: if not reaction.get("reply_to"): shout_id = reaction.get("shout") - # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΡΠΎΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΠ΅ Π΄ΠΈΠ·Π»Π°ΠΉΠΊΠΎΠ², Π΄Π°ΠΆΠ΅ Ссли тСкущая рСакция Π½Π΅ Π΄ΠΈΠ·Π»Π°ΠΉΠΊ - total_reactions = ( - session.query(Reaction) - .where( - Reaction.shout == shout_id, - Reaction.reply_to.is_(None), - Reaction.kind.in_(RATING_REACTIONS), - # Π Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΈ физичСски ΡƒΠ΄Π°Π»ΡΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ, поэтому Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ deleted_at Π½Π΅ Π½ΡƒΠΆΠ΅Π½ - ) - .count() - ) - + # πŸ”§ Π‘Ρ‡ΠΈΡ‚Π°Π΅ΠΌ всС Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ΠΎΠ²Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ (ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ + ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅) + # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ POSITIVE_REACTIONS + NEGATIVE_REACTIONS вмСсто Ρ‚ΠΎΠ»ΡŒΠΊΠΎ RATING_REACTIONS positive_reactions = ( session.query(Reaction) .where( @@ -233,9 +225,13 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool: .count() ) + total_reactions = positive_reactions + negative_reactions + # УсловиС 1: МСньшС 5 голосов "Π·Π°" if positive_reactions < 5: - logger.debug(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id}: {positive_reactions} Π»Π°ΠΉΠΊΠΎΠ² (мСньшС 5) - Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ unfeatured") + logger.debug( + f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id}: {positive_reactions} ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ (мСньшС 5) - Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ unfeatured" + ) return True # УсловиС 2: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, ΡΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‚ Π»ΠΈ ΠΎΡ‚Ρ€ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ 20% ΠΈΠ»ΠΈ Π±ΠΎΠ»Π΅Π΅ ΠΎΡ‚ всСх Ρ€Π΅Π°ΠΊΡ†ΠΈΠΉ @@ -256,6 +252,8 @@ async def set_featured(session: Session, shout_id: int) -> None: :param session: Database session. :param shout_id: Shout ID. """ + from cache.revalidator import revalidation_manager + s = session.query(Shout).where(Shout.id == shout_id).first() if s: current_time = int(time.time()) @@ -267,6 +265,17 @@ async def set_featured(session: Session, shout_id: int) -> None: session.add(s) session.commit() + # πŸ”§ РСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ связанных сущностСй + revalidation_manager.mark_for_revalidation(shout_id, "shouts") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for author in s.authors: + revalidation_manager.mark_for_revalidation(author.id, "authors") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Ρ‚Π΅ΠΌΡ‹ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for topic in s.topics: + revalidation_manager.mark_for_revalidation(topic.id, "topics") + + logger.info(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id} ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»Π° статус featured, кСш ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½ для Ρ€Π΅Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ") + def set_unfeatured(session: Session, shout_id: int) -> None: """ @@ -275,9 +284,27 @@ def set_unfeatured(session: Session, shout_id: int) -> None: :param session: Database session. :param shout_id: Shout ID. """ + from cache.revalidator import revalidation_manager + + # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ для доступа ΠΊ Π°Π²Ρ‚ΠΎΡ€Π°ΠΌ ΠΈ Ρ‚Π΅ΠΌΠ°ΠΌ + shout = session.query(Shout).where(Shout.id == shout_id).first() + if not shout: + return + session.query(Shout).where(Shout.id == shout_id).update({"featured_at": None}) session.commit() + # πŸ”§ РСвалидация кСша ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ связанных сущностСй + revalidation_manager.mark_for_revalidation(shout_id, "shouts") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΎΠ² ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for author in shout.authors: + revalidation_manager.mark_for_revalidation(author.id, "authors") + # Π Π΅Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅ΠΌ Ρ‚Π΅ΠΌΡ‹ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ + for topic in shout.topics: + revalidation_manager.mark_for_revalidation(topic.id, "topics") + + logger.info(f"ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ {shout_id} потСряла статус featured, кСш ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½ для Ρ€Π΅Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ") + async def _create_reaction(session: Session, shout_id: int, is_author: bool, author_id: int, reaction: dict) -> dict: """