diff --git a/resolvers/__init__.py b/resolvers/__init__.py index d5fb5939..b31e2a1b 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -25,7 +25,7 @@ from resolvers.notifier import ( notifications_seen_after, notifications_seen_thread, ) -from resolvers.rating import rate_author +from resolvers.rating import get_my_rates_comments, get_my_rates_shouts, rate_author from resolvers.reaction import ( create_reaction, delete_reaction, @@ -63,7 +63,6 @@ __all__ = [ "get_author_follows_authors", "get_authors_all", "load_authors_by", - "rate_author", "update_author", ## "search_authors", # community @@ -110,4 +109,8 @@ __all__ = [ "notifications_seen_thread", "notifications_seen_after", "notification_mark_seen", + # rating + "rate_author", + "get_my_rates_comments", + "get_my_rates_shouts", ] diff --git a/resolvers/rating.py b/resolvers/rating.py index 6a7e82f4..311a1cec 100644 --- a/resolvers/rating.py +++ b/resolvers/rating.py @@ -6,7 +6,71 @@ from orm.reaction import Reaction, ReactionKind from orm.shout import Shout from services.auth import login_required from services.db import local_session -from services.schema import mutation +from services.schema import mutation, query + + +@query.field("get_my_rates_comments") +@login_required +def get_my_rates_comments(info, comments: list[int], shout: int) -> list[dict]: + """ + Получение реакций пользователя на комментарии + """ + author_dict = info.context.get("author") if info.context else None + author_id = author_dict.get("id") if author_dict else None + if not author_id: + return {"error": "Author not found"} + + # Подзапрос для реакций текущего пользователя + rated_query = ( + select(Reaction.shout.label("shout_id"), Reaction.kind.label("my_rate")) + .where( + and_( + Reaction.shout == shout, + Reaction.reply_to.in_(comments), + Reaction.created_by == author_id, + Reaction.deleted_at.is_(None), + Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]), + ) + ) + .order_by(Reaction.shout, Reaction.created_at.desc()) + .distinct(Reaction.shout) + .subquery() + ) + with local_session() as session: + comments_result = session.execute(rated_query).all() + return [{"comment_id": row.shout_id, "my_rate": row.my_rate} for row in comments_result] + + +@query.field("get_my_rates_shouts") +@login_required +def get_my_rates_shouts(info, shouts): + """ + Получение реакций пользователя на публикации + """ + author_dict = info.context.get("author") if info.context else None + author_id = author_dict.get("id") if author_dict else None + if not author_id: + return {"error": "Author not found"} + + # Подзапрос для реакций текущего пользователя + rated_query = ( + select(Reaction.shout.label("shout_id"), Reaction.kind.label("my_rate")) + .where( + and_( + Reaction.shout.in_(shouts), + Reaction.reply_to.is_(None), + Reaction.created_by == author_id, + Reaction.deleted_at.is_(None), + Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]), + ) + ) + .order_by(Reaction.shout, Reaction.created_at.desc()) + .distinct(Reaction.shout) + .subquery() + ) + with local_session() as session: + shouts_result = session.execute(rated_query).all() + return [{"shout_id": row.shout_id, "my_rate": row.my_rate} for row in shouts_result] @mutation.field("rate_author") diff --git a/resolvers/reader.py b/resolvers/reader.py index a15c8529..14bc67e9 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -146,7 +146,7 @@ def query_with_stat(info): if has_field(info, "stat"): logger.info("Начало построения запроса статистики") - + # Подзапрос для статистики реакций stats_subquery = ( select( @@ -163,9 +163,7 @@ def query_with_stat(info): ) .filter(Reaction.reply_to.is_(None)) .label("rating"), - func.max(Reaction.created_at) - .filter(Reaction.reply_to.is_(None)) - .label("last_reacted_at"), + func.max(Reaction.created_at).filter(Reaction.reply_to.is_(None)).label("last_reacted_at"), ) .where(Reaction.deleted_at.is_(None)) .group_by(Reaction.shout) @@ -182,30 +180,8 @@ def query_with_stat(info): if author_id: logger.info(f"Построение подзапроса реакций пользователя с ID: {author_id}") - # Подзапрос для реакций текущего пользователя - user_reaction_subquery = ( - select( - Reaction.shout.label("shout_id"), - Reaction.kind.label("my_rate") - ) - .where( - and_( - Reaction.created_by == author_id, - Reaction.deleted_at.is_(None), - Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]), - Reaction.reply_to.is_(None), - ) - ) - .order_by(Reaction.shout, Reaction.created_at.desc()) - .distinct(Reaction.shout) - .subquery() - ) - logger.info("Подзапрос реакций пользователя построен") - logger.info("Соединение основного запроса с подзапросом статистики") q = q.outerjoin(stats_subquery, stats_subquery.c.shout == Shout.id) - logger.info("Соединение основного запроса с подзапросом реакций пользователя") - q = q.outerjoin(user_reaction_subquery, user_reaction_subquery.c.shout_id == Shout.id) logger.info("Добавление колонок статистики в основной запрос") q = q.add_columns( @@ -216,8 +192,6 @@ def query_with_stat(info): func.coalesce(stats_subquery.c.rating, 0), "last_reacted_at", stats_subquery.c.last_reacted_at, - "my_rate", - user_reaction_subquery.c.my_rate ).label("stat") ) logger.info("Колонки статистики добавлены") @@ -233,7 +207,7 @@ def query_with_stat(info): "last_reacted_at", stats_subquery.c.last_reacted_at, "my_rate", - None + None, ).label("stat") ) logger.info("Колонки статистики без my_rate добавлены") @@ -263,19 +237,19 @@ def get_shouts_with_links(info, q, limit=20, offset=0): for idx, row in enumerate(shouts_result): try: logger.debug(f"Обработка строки {idx}") - + shout = None if hasattr(row, "Shout"): shout = row.Shout else: - if not row == 'stat': + if not row == "stat": logger.warning(f"Строка {idx} не содержит атрибута 'Shout': {row}") continue if shout: shout_id = int(f"{shout.id}") shout_dict = shout.dict() - + if has_field(info, "created_by") and shout_dict.get("created_by"): main_author_id = shout_dict.get("created_by") a = session.query(Author).filter(Author.id == main_author_id).first() @@ -285,7 +259,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0): "slug": a.slug, "pic": a.pic, } - + if hasattr(row, "stat"): logger.debug(f"Строка {idx} - stat: {row.stat}") stat = {} @@ -299,7 +273,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0): shout_dict["stat"] = {**stat, "viewed": viewed, "commented": stat.get("comments_count", 0)} else: logger.warning(f"Строка {idx} не содержит атрибута 'stat'") - + if has_field(info, "main_topic") and hasattr(row, "main_topic"): shout_dict["main_topic"] = ( json.loads(row.main_topic) if isinstance(row.main_topic, str) else row.main_topic @@ -407,7 +381,6 @@ def apply_sorting(q, options): @query.field("load_shouts_by") -@login_accepted async def load_shouts_by(_, info: GraphQLResolveInfo, options): """ Загрузка публикаций с фильтрацией, сортировкой и пагинацией. @@ -426,7 +399,6 @@ async def load_shouts_by(_, info: GraphQLResolveInfo, options): @query.field("load_shouts_search") -@login_accepted async def load_shouts_search(_, info, text, options): """ Поиск публикаций по тексту. @@ -488,29 +460,17 @@ async def load_shouts_unrated(_, info, options): .scalar_subquery() ) - q = ( - select(Shout) - .where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) + q = select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) q = q.join(Author, Author.id == Shout.created_by) q = q.add_columns( - json_builder( - "id", Author.id, - "name", Author.name, - "slug", Author.slug, - "pic", Author.pic - ).label("main_author") + json_builder("id", Author.id, "name", Author.name, "slug", Author.slug, "pic", Author.pic).label("main_author") ) q = q.join(ShoutTopic, and_(ShoutTopic.shout == Shout.id, ShoutTopic.main.is_(True))) q = q.join(Topic, Topic.id == ShoutTopic.topic) - q = q.add_columns( - json_builder( - "id", Topic.id, "title", Topic.title, "slug", Topic.slug - ).label("main_topic") - ) + q = q.add_columns(json_builder("id", Topic.id, "title", Topic.title, "slug", Topic.slug).label("main_topic")) q = q.where(Shout.id.not_in(rated_shouts)) q = q.order_by(func.random()) - + limit = options.get("limit", 5) offset = options.get("offset", 0) return get_shouts_with_links(info, q, limit, offset) diff --git a/schema/query.graphql b/schema/query.graphql index ced67d31..69a1a582 100644 --- a/schema/query.graphql +++ b/schema/query.graphql @@ -32,6 +32,10 @@ type Query { load_shouts_search(text: String!, options: LoadShoutsOptions): [SearchResult] load_shouts_bookmarked(options: LoadShoutsOptions): [Shout] + # rating + get_my_rates_shouts(shouts: [Int!]!): [MyRateShout] + get_my_rates_comments(comments: [Int!]!, shout: Int!): [MyRateComment] + # public feeds load_shouts_with_topic(slug: String, options: LoadShoutsOptions): [Shout] # topic feed load_shouts_random_top(options: LoadShoutsOptions): [Shout] # random order, fixed filter, limit offset can be used diff --git a/schema/type.graphql b/schema/type.graphql index 0b80689e..8892f7b6 100644 --- a/schema/type.graphql +++ b/schema/type.graphql @@ -112,7 +112,6 @@ type Stat { viewed: Int # followed: Int last_reacted_at: Int - my_rate: ReactionKind } type CommunityStat { @@ -234,3 +233,15 @@ type NotificationsResult { total: Int! error: String } + +type MyRateShout { + shout_id: Int! + my_rate: ReactionKind +} + +type MyRateComment { + shout_id: Int + comment_id: Int! + my_rate: ReactionKind +} + diff --git a/services/auth.py b/services/auth.py index c64554d7..56d0f5cc 100644 --- a/services/auth.py +++ b/services/auth.py @@ -106,11 +106,11 @@ def login_accepted(f): async def decorated_function(*args, **kwargs): info = args[1] req = info.context.get("request") - + logger.debug("login_accepted: Проверка авторизации пользователя.") user_id, user_roles = await check_auth(req) logger.debug(f"login_accepted: user_id={user_id}, user_roles={user_roles}") - + if user_id and user_roles: logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}") info.context["user_id"] = user_id.strip() @@ -123,7 +123,9 @@ def login_accepted(f): # Предполагается, что `author` является объектом с атрибутом `id` info.context["author"] = author.dict() else: - logger.error(f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные.")# Используем базовую информацию об автор + logger.error( + f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные." + ) # Используем базовую информацию об автор else: logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.") info.context["user_id"] = None @@ -131,4 +133,5 @@ def login_accepted(f): info.context["author"] = None return await f(*args, **kwargs) + return decorated_function