diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 87% rename from CHANGELOG.txt rename to CHANGELOG.md index 8349814d..03625997 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,21 +1,30 @@ +[0.4.6] +- optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions` +- `load_shouts_bookmarked` resolver fixed +- `refactored with `resolvers/feed.py` +- model updates: + - `ShoutsOrderBy` enum added + - `Shout.main_topic` from `ShoutTopic.main` type output + - `Shout.created_by` as `Author` type output + [0.4.5] -- bookmark_shout mutation resolver added +- `bookmark_shout` mutation resolver added - load_bookmarked_shouts resolver fix - community stats in orm -- get_communities_by_author resolver added -- get_communities_all resolver fix +- `get_communities_by_author` resolver added +- `get_communities_all` resolver fix - reaction filter by kinds -- reaction sort enum added -- community follower roles enum added -- invite status enum added -- topic parents ids added +- `ReactionSort` enum added +- `CommunityFollowerRole` enum added +- `InviteStatus` enum added +- `Topic.parents` ids added - community CUDL resolvers added -- get_shout resolver accepts slug or shout_id +- `get_shout` resolver accepts slug or shout_id [0.4.4] - followers_stat removed for shout - sqlite3 support added -- rating_stat and commented_stat fix +- `rating_stat` and `commented_stat` fixes [0.4.3] - cache reimplemented diff --git a/docs/load_shouts.md b/docs/load_shouts.md new file mode 100644 index 00000000..07bfd272 --- /dev/null +++ b/docs/load_shouts.md @@ -0,0 +1,23 @@ +## Reader resolvers + +### load_shouts_by + +Если graphql запрос содержит ожидаемые поля `stat`, `authors` или `topics`, то будут выполнены дополнительные подзапросы. + +#### Параметры + +- `filters` - словарь с фильтрами + - `featured` - фильтрует публикации, одобренные для показа на главной, по умолчанию не применяется + - `topics` - список идентификаторов тем, по умолчанию не применяется + - `authors` - список идентификаторов авторов, по умолчанию не применяется + - `after` - unixtime после которого будут выбраны публикации, по умолчанию не применяется + - `layouts` - список идентификаторов форматов, по умолчанию не применяется +- `order_by` может быть `rating`, `comments_count`, `last_reacted_at`. По умолчанию применяется сортировка по `published_at` +- `order_by_desc` определяет порядок сортировки, по умолчанию применяется `desc` +- `offset` и `limit` определяют смещение и ограничение, по умолчанию `0` и `10` + +### load_shouts_feed, load_shouts_followed, load_shouts_followed_by, load_shouts_discussed, load_shouts_reacted + +Параметры аналогичны `load_shouts_by`, но применяются дополнительные фильтры: + +- `reacted` - фильтрует публикации, по которым пользователь проголосовал, по умолчанию не применяется diff --git a/docs/reader.md b/docs/reader.md deleted file mode 100644 index e06e45d6..00000000 --- a/docs/reader.md +++ /dev/null @@ -1,5 +0,0 @@ -## Reader resolvers - -### load_shouts_by - -Запрашиваемые поля: `stat`, `authors`, `topics` влияют на количество подзапросов. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a9d67e99..2c1bb231 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "0.4.4" +version = "0.4.6" description = "core module for discours.io" authors = ["discoursio devteam"] license = "MIT" diff --git a/resolvers/__init__.py b/resolvers/__init__.py index a466dcef..2d62f076 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -32,15 +32,17 @@ from resolvers.reaction import ( from resolvers.reader import ( get_shout, load_shouts_by, + load_shouts_random_top, + load_shouts_random_topic, + load_shouts_search, + load_shouts_unrated, +) +from resolvers.feed import ( load_shouts_coauthored, load_shouts_discussed, load_shouts_feed, load_shouts_followed, load_shouts_followed_by, - load_shouts_random_top, - load_shouts_random_topic, - load_shouts_search, - load_shouts_unrated, ) from resolvers.topic import ( get_topic, diff --git a/resolvers/bookmark.py b/resolvers/bookmark.py index 2a728387..c50c36f6 100644 --- a/resolvers/bookmark.py +++ b/resolvers/bookmark.py @@ -1,15 +1,20 @@ +from operator import and_ from graphql import GraphQLError -from sqlalchemy import delete, insert +from sqlalchemy import delete, insert, select from orm.author import AuthorBookmark from orm.shout import Shout +from resolvers.feed import apply_options +from resolvers.reader import get_shouts_with_links, has_field, query_with_stat +from services.auth import login_required from services.common_result import CommonResult from services.db import local_session from services.schema import mutation, query @query.field("load_shouts_bookmarked") -def load_shouts_bookmarked(_, info, limit=50, offset=0): +@login_required +def load_shouts_bookmarked(_, info, options): """ Load bookmarked shouts for the authenticated user. @@ -24,10 +29,17 @@ def load_shouts_bookmarked(_, info, limit=50, offset=0): author_id = author_dict.get("id") if not author_id: raise GraphQLError("User not authenticated") - result = [] - with local_session() as db: - result = db.query(AuthorBookmark).where(AuthorBookmark.author == author_id).offset(offset).limit(limit).all() - return result + + q = query_with_stat() if has_field(info, "stat") else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + q = q.join(AuthorBookmark) + q = q.filter( + and_( + Shout.id == AuthorBookmark.shout, + AuthorBookmark.author == author_id, + ) + ) + q, limit, offset = apply_options(q, options, author_id) + return get_shouts_with_links(info, q, limit, offset) @mutation.field("toggle_bookmark_shout") diff --git a/resolvers/feed.py b/resolvers/feed.py new file mode 100644 index 00000000..209dea4a --- /dev/null +++ b/resolvers/feed.py @@ -0,0 +1,240 @@ +from typing import List + +from sqlalchemy import and_, desc, select, text, union +from sqlalchemy.orm import joinedload + +from orm.author import Author, AuthorFollower +from orm.reaction import Reaction +from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic +from orm.topic import TopicFollower +from resolvers.reader import apply_filters, apply_sorting, get_shouts_with_links, has_field, query_with_stat +from services.auth import login_required +from services.db import local_session +from services.schema import query +from utils.logger import root_logger as logger + + +def apply_options(q, options, author_id: int): + filters = options.get("filters") + if isinstance(filters, dict): + q = apply_filters(q, filters) + if "reacted" in filters: + reacted = filters.get("reacted") + q = q.join(Reaction, Reaction.shout == Shout.id) + if reacted: + q = q.filter(Reaction.created_by == author_id) + else: + q = q.filter(Reaction.created_by != author_id) + q = apply_sorting(q, options) + limit = options.get("limit", 10) + offset = options.get("offset", 0) + return q, limit, offset + + +def filter_followed(info, q): + """ + Фильтрация публикаций, основанная на подписках пользователя. + + :param info: Информация о контексте GraphQL. + :param q: Исходный запрос для публикаций. + :return: Фильтрованный запрос. + """ + user_id = info.context.get("user_id") + reader_id = info.context.get("author", {}).get("id") + if user_id and reader_id: + reader_followed_authors = select(AuthorFollower.author).where(AuthorFollower.follower == reader_id) + reader_followed_topics = select(TopicFollower.topic).where(TopicFollower.follower == reader_id) + reader_followed_shouts = select(ShoutReactionsFollower.shout).where( + ShoutReactionsFollower.follower == reader_id + ) + + 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) + ) + ) + q = q.filter(Shout.id.in_(subquery)) + return q, reader_id + + +@query.field("load_shouts_feed") +@login_required +async def load_shouts_feed(_, info, options): + """ + Загрузка ленты публикаций для авторизованного пользователя. + + :param info: Информация о контексте GraphQL. + :param options: Опции фильтрации и сортировки. + :return: Список публикаций для ленты. + """ + author_id = info.context.get("author_id") + if not author_id: + return [] + q = ( + query_with_stat() + if has_field(info, "stat") + else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + ) + + q, limit, offset = apply_options(q, options, author_id) + return get_shouts_with_links(info, q, limit, offset) + + +@query.field("load_shouts_coauthored") +@login_required +async def load_shouts_coauthored(_, info, options): + """ + Загрузка публикаций, написанных в соавторстве с пользователем. + + :param info: Информаци о контексте GraphQL. + :param options: Опции фильтрации и сортировки. + :return: Список публикаций в соавтостве. + """ + author_id = info.context.get("author", {}).get("id") + if not author_id: + return [] + q = ( + query_with_stat() + if has_field(info, "stat") + else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + ) + q = q.filter(Shout.authors.any(id=author_id)) + + filters = options.get("filters") + if isinstance(filters, dict): + q = apply_filters(q, filters) + if filters.get("reacted"): + q = q.join( + Reaction, + and_( + Reaction.shout == Shout.id, + Reaction.created_by == author_id, + ), + ) + q = apply_sorting(q, options) + limit = options.get("limit", 10) + offset = options.get("offset", 0) + return get_shouts_with_links(info, q, limit, offset=offset) + + +@query.field("load_shouts_discussed") +@login_required +async def load_shouts_discussed(_, info, options): + """ + Загрузка публикаций, которые обсуждались пользователем. + + :param info: Информация о контексте GraphQL. + :param options: Опции фильтрации и сортировки. + :return: Список публикаций, обсужденых пользователем. + """ + author_id = info.context.get("author", {}).get("id") + if not author_id: + return [] + # Подзапрос для поиска идентификаторов публикаций, которые комментировал автор + reaction_subquery = ( + select(Reaction.shout) + .distinct() # Убедитесь, что получены уникальные идентификаторы публикаций + .filter(and_(Reaction.created_by == author_id, Reaction.body.is_not(None))) + .correlate(Shout) # Убедитесь, что подзапрос правильно связан с основным запросом + ) + q = ( + query_with_stat() + if has_field(info, "stat") + else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + ) + q = q.filter(Shout.id.in_(reaction_subquery)) + q, limit, offset = apply_options(q, options, author_id) + return get_shouts_with_links(info, q, limit, offset=offset) + + +# применяется сортировка публикаций по последней реакции +async def reacted_shouts_updates(info, follower_id: int, options) -> List[Shout]: + """ + Обновляет публикации, на которые подписан автор, с учетом реакций. + + :param follower_id: Идентификатор подписчика. + :param limit: Колиество пукликаций для загрузки. + :param offset: Смещение для пагинации. + :return: Список публикаций. + """ + shouts: List[Shout] = [] + with local_session() as session: + author = session.query(Author).filter(Author.id == follower_id).first() + if author: + # Публикации, где подписчик является автором + q1 = ( + query_with_stat() + if has_field(info, "stat") + else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + ) + q1 = q1.filter(Shout.authors.any(id=follower_id)) + + # Публикации, на которые подписчик реагировал + q2 = ( + query_with_stat() + if has_field(info, "stat") + else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) + ) + q2 = q2.options(joinedload(Shout.reactions)) + q2 = q2.filter(Reaction.created_by == follower_id) + + # Сортировка публикаций по полю `last_reacted_at` + combined_query = union(q1, q2).order_by(desc(text("last_reacted_at"))) + + # извлечение ожидаемой структуры данных + q, limit, offset = apply_options(combined_query, options, follower_id) + shouts = get_shouts_with_links(info, q, limit, offset=offset) + + return shouts + + +@query.field("load_shouts_followed") +@login_required +async def load_shouts_followed(_, info, options) -> List[Shout]: + """ + Загружает публикации, на которые подписан пользователь. + + :param info: Информация о контексте GraphQL. + :param limit: Количество публикаций для загрузки. + :param offset: Смещение для пагинации. + :return: Список публикаций. + """ + user_id = info.context["user_id"] + with local_session() as session: + author = session.query(Author).filter(Author.user == user_id).first() + if author: + try: + author_id: int = author.dict()["id"] + shouts = await reacted_shouts_updates(info, author_id, options) + return shouts + except Exception as error: + logger.debug(error) + return [] + + +@query.field("load_shouts_followed_by") +async def load_shouts_followed_by(_, info, slug: str, options) -> List[Shout]: + """ + Загружает публикации, на которые подписан автор по slug. + + :param info: Информация о контексте GraphQL. + :param slug: Slug автора. + :param limit: Количество публикаций для загрузки. + :param offset: Смещение для пагинации. + :return: Список публикаций. + """ + with local_session() as session: + author = session.query(Author).filter(Author.slug == slug).first() + if author: + try: + author_id: int = author.dict()["id"] + shouts = await reacted_shouts_updates(info, author_id, options) + return shouts + except Exception as error: + logger.debug(error) + return [] diff --git a/resolvers/reader.py b/resolvers/reader.py index 623523a0..92b4d5e7 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -1,25 +1,13 @@ import json -from typing import List -from sqlalchemy.orm import aliased, joinedload -from sqlalchemy.sql import union -from sqlalchemy.sql.expression import ( - and_, - asc, - case, - desc, - func, - nulls_last, - select, - text, -) +from sqlalchemy.orm import aliased +from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select -from orm.author import Author, AuthorFollower +from orm.author import Author from orm.reaction import Reaction, ReactionKind -from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic -from orm.topic import Topic, TopicFollower +from orm.shout import Shout, ShoutAuthor, ShoutTopic +from orm.topic import Topic from resolvers.topic import get_topics_random -from services.auth import login_required from services.db import local_session from services.schema import query from services.search import search_text @@ -37,9 +25,6 @@ def has_field(info, fieldname: str) -> bool: def query_with_stat(): - """ - Оптимизированный базовый запрос - """ # Оптимизированный подзапрос статистики stats_subquery = ( select( @@ -225,65 +210,21 @@ def get_shouts_with_links(info, q, limit=20, offset=0, author_id=None): return list(shouts_data.values()) -def filter_my(info, session, q): +def apply_filters(q, filters): """ - Фильтрация публикаций, основанная на подписках пользователя. - - :param info: Информация о контексте GraphQL. - :param session: Сессия базы данных. - :param q: Исходный запрос для публикаций. - :return: Фильтрованный запрос. - """ - user_id = info.context.get("user_id") - reader_id = info.context.get("author", {}).get("id") - if user_id and reader_id: - reader_followed_authors = select(AuthorFollower.author).where(AuthorFollower.follower == reader_id) - reader_followed_topics = select(TopicFollower.topic).where(TopicFollower.follower == reader_id) - reader_followed_shouts = select(ShoutReactionsFollower.shout).where( - ShoutReactionsFollower.follower == reader_id - ) - - 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) - ) - ) - q = q.filter(Shout.id.in_(subquery)) - return q, reader_id - - -def apply_filters(q, filters, author_id=None): - """ - Применение фильтров к запросу. + Применение общих фильтров к запросу. :param q: Исходный запрос. :param filters: Словарь фильтров. - :param author_id: Идентификатор автора (опционально). :return: Запрос с примененными фильтрами. """ if isinstance(filters, dict): - if filters.get("reacted"): - q = q.join( - Reaction, - and_( - Reaction.shout == Shout.id, - Reaction.created_by == author_id, - ), - ) - if "featured" in filters: featured_filter = filters.get("featured") if featured_filter: q = q.filter(Shout.featured_at.is_not(None)) else: q = q.filter(Shout.featured_at.is_(None)) - else: - pass by_layouts = filters.get("layouts") if by_layouts and isinstance(by_layouts, list): q = q.filter(Shout.layout.in_(by_layouts)) @@ -335,6 +276,23 @@ async def get_shout(_, info, slug="", shout_id=0): return None +def apply_sorting(q, options): + # Определение поля для сортировки + order_str = options.get("order_by") + + # Проверка, требуется ли сортировка по одному из статистических полей + if order_str in ["rating", "comments_count", "last_reacted_at"]: + # Сортировка по выбранному статистическому полю в указанном порядке + q = q.order_by(desc(order_str)) + query_order_by = desc(order_str) if options.get("order_by_desc", True) else asc(order_str) + # Применение сортировки с размещением NULL значений в конце + q = q.order_by(nulls_last(query_order_by)) + else: + q = q.order_by(Shout.published_at.desc()) + + return q + + @query.field("load_shouts_by") async def load_shouts_by(_, info, options): """ @@ -343,80 +301,38 @@ async def load_shouts_by(_, info, options): :param options: Опции фильтрации и сортировки. :return: Список публикаций, удовлетворяющих критериям. """ - # Базовый запрос + # Базовый запрос: если запрашиваются статистические данные, используем специальный запрос с статистикой q = ( query_with_stat() if has_field(info, "stat") else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) ) - # Применение фильтров - filters = options.get("filters", {}) - q = apply_filters(q, filters) + filters = options.get("filters") + if isinstance(filters, dict): + q = apply_filters(q, filters) - # Сортировка - order_by = Shout.featured_at if filters.get("featured") else Shout.published_at - order_str = options.get("order_by") - if order_str in ["rating", "followers", "comments", "last_reacted_at"]: - q = q.order_by(desc(text(f"{order_str}_stat"))) - query_order_by = desc(order_by) if options.get("order_by_desc", True) else asc(order_by) - q = q.order_by(nulls_last(query_order_by)) - else: - q = q.order_by(Shout.published_at.desc()) + q = apply_sorting(q, options) - # Ограничение и смещение + # Установка лимита и смещения для пагинации offset = options.get("offset", 0) limit = options.get("limit", 10) + # Передача сформированного запроса в метод получения публикаций с учетом сортировки и пагинации return get_shouts_with_links(info, q, limit, offset) -@query.field("load_shouts_feed") -@login_required -async def load_shouts_feed(_, info, options): - """ - Загрузка ленты публикаций для авторизованного пользователя. - - :param info: Информация о контексте GraphQL. - :param options: Опции фильтрации и сортировки. - :return: Список публикаций для ленты. - """ - with local_session() as session: - q = ( - query_with_stat() - if has_field(info, "stat") - else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) - - # Применение фильтров - filters = options.get("filters", {}) - if filters: - q, reader_id = filter_my(info, session, q) - q = apply_filters(q, filters, reader_id) - - # Сортировка - order_by = options.get("order_by") - order_by = text(order_by) if order_by else Shout.featured_at if filters.get("featured") else Shout.published_at - query_order_by = desc(order_by) if options.get("order_by_desc", True) else asc(order_by) - q = q.order_by(nulls_last(query_order_by)) - - # Пагинация - offset = options.get("offset", 0) - limit = options.get("limit", 10) - - return get_shouts_with_links(info, q, limit, offset) - - @query.field("load_shouts_search") -async def load_shouts_search(_, info, text, limit=50, offset=0): +async def load_shouts_search(_, info, text, options): """ Поиск публикаций по тексту. :param text: Строка поиска. - :param limit: Максимальное количество результатов. - :param offset: Смещение для пагинации. + :param options: Опции фильтрации и сортировки. :return: Список публикаций, найденных по тексту. """ + limit = options.get("limit", 10) + offset = options.get("offset", 0) if isinstance(text, str) and len(text) > 2: results = await search_text(text, limit, offset) scores = {} @@ -434,6 +350,8 @@ async def load_shouts_search(_, info, text, limit=50, offset=0): else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) ) q = q.filter(Shout.id.in_(hits_ids)) + q = apply_filters(q, options) + q = apply_sorting(q, options) shouts = get_shouts_with_links(info, q, limit, offset) for shout in shouts: shout.score = scores[f"{shout.id}"] @@ -443,7 +361,7 @@ async def load_shouts_search(_, info, text, limit=50, offset=0): @query.field("load_shouts_unrated") -async def load_shouts_unrated(_, info, limit=5, offset=0): +async def load_shouts_unrated(_, info, options): """ Загрузка публикаций с менее чем 3 реакциями типа LIKE/DISLIKE """ @@ -465,7 +383,8 @@ async def load_shouts_unrated(_, info, limit=5, offset=0): # .order_by(desc(Shout.published_at)) .order_by(func.random()) ) - + limit = options.get("limit", 5) + offset = options.get("offset", 0) return get_shouts_with_links(info, q, limit, offset) @@ -480,11 +399,11 @@ async def load_shouts_random_top(_, info, options): """ aliased_reaction = aliased(Reaction) - subquery = ( - select(Shout.id).outerjoin(aliased_reaction).where(and_(Shout.deleted_at.is_(None), Shout.layout.is_not(None))) - ) + subquery = select(Shout.id).outerjoin(aliased_reaction).where(Shout.deleted_at.is_(None)) - subquery = apply_filters(subquery, options.get("filters", {})) + filters = options.get("filters") + if isinstance(filters, dict): + subquery = apply_filters(subquery, filters) subquery = subquery.group_by(Shout.id).order_by( desc( @@ -515,7 +434,7 @@ async def load_shouts_random_top(_, info, options): @query.field("load_shouts_random_topic") -async def load_shouts_random_topic(_, info, limit: int = 10): +async def load_shouts_random_topic(_, info, options): """ Загрузка случайной темы и связанных с ней публикаций. @@ -531,147 +450,12 @@ async def load_shouts_random_topic(_, info, limit: int = 10): else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) ) q = q.filter(Shout.topics.any(slug=topic.slug)) - q = q.order_by(desc(Shout.created_at)) - shouts = get_shouts_with_links(info, q, limit) + + q = apply_filters(q, options) + q = apply_sorting(q, options) + limit = options.get("limit", 10) + offset = options.get("offset", 0) + shouts = get_shouts_with_links(info, q, limit, offset) if shouts: return {"topic": topic, "shouts": shouts} return {"error": "failed to get random topic"} - - -@query.field("load_shouts_coauthored") -@login_required -async def load_shouts_coauthored(_, info, limit=50, offset=0): - """ - Загрузка публикаций, написанных в соавторстве с пользователем. - - :param info: Информаци о контексте GraphQL. - :param limit: Максимальное количество публикаций. - :param offset: Смещение для пагинации. - :return: Список публикаций в соавтостве. - """ - author_id = info.context.get("author", {}).get("id") - if not author_id: - return [] - q = ( - query_with_stat() - if has_field(info, "stat") - else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) - q = q.filter(Shout.authors.any(id=author_id)) - return get_shouts_with_links(info, q, limit, offset=offset) - - -@query.field("load_shouts_discussed") -@login_required -async def load_shouts_discussed(_, info, limit=50, offset=0): - """ - Загрузка публикаций, которые обсуждались пользователем. - - :param info: Информация о контексте GraphQL. - :param limit: Максимальное количество публикаций. - :param offset: Смещне для пагинации. - :return: Список публикаций, обсужденых пользователем. - """ - author_id = info.context.get("author", {}).get("id") - if not author_id: - return [] - # Подзапрос для поиска идентификаторов публикаций, которые комментировал автор - reaction_subquery = ( - select(Reaction.shout) - .distinct() # Убедитесь, что получены уникальные идентификаторы публикаций - .filter(and_(Reaction.created_by == author_id, Reaction.body.is_not(None))) - .correlate(Shout) # Убедитесь, что подзапрос правильно связан с основным запросом - ) - q = ( - query_with_stat() - if has_field(info, "stat") - else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) - q = q.filter(Shout.id.in_(reaction_subquery)) - return get_shouts_with_links(info, q, limit, offset=offset) - - -async def reacted_shouts_updates(info, follower_id: int, limit=50, offset=0) -> List[Shout]: - """ - Обновляет публикации, на которые подписан автор, с учетом реакци. - - :param follower_id: Идентификатор подписчика. - :param limit: Колиество пукликаций для загрузки. - :param offset: Смещение для пагинации. - :return: Список публикаций. - """ - shouts: List[Shout] = [] - with local_session() as session: - author = session.query(Author).filter(Author.id == follower_id).first() - if author: - # Публикации, где подписчик является автором - q1 = ( - query_with_stat() - if has_field(info, "stat") - else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) - q1 = q1.filter(Shout.authors.any(id=follower_id)) - - # Публикации, на которые подписчик реагировал - q2 = ( - query_with_stat() - if has_field(info, "stat") - else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) - ) - q2 = q2.options(joinedload(Shout.reactions)) - q2 = q2.filter(Reaction.created_by == follower_id) - - # Сортировка публикаций по полю `last_reacted_at` - combined_query = union(q1, q2).order_by(desc(text("last_reacted_at"))) - - # извлечение ожидаемой структуры данных - shouts = get_shouts_with_links(info, combined_query, limit, offset=offset) - - return shouts - - -@query.field("load_shouts_followed") -@login_required -async def load_shouts_followed(_, info, limit=50, offset=0) -> List[Shout]: - """ - Загружает публикации, на которые подписан пользователь. - - :param info: Информация о контексте GraphQL. - :param limit: Количество публикаций для загрузки. - :param offset: Смещение для пагинации. - :return: Список публикаций. - """ - user_id = info.context["user_id"] - with local_session() as session: - author = session.query(Author).filter(Author.user == user_id).first() - if author: - try: - author_id: int = author.dict()["id"] - shouts = await reacted_shouts_updates(info, author_id, limit, offset) - return shouts - except Exception as error: - logger.debug(error) - return [] - - -@query.field("load_shouts_followed_by") -async def load_shouts_followed_by(_, info, slug: str, limit=50, offset=0) -> List[Shout]: - """ - Загружает публикации, на которые подписан автор по slug. - - :param info: Информация о контексте GraphQL. - :param slug: Slug автора. - :param limit: Количество публикаций для загрузки. - :param offset: Смещение для пагинации. - :return: Список публикаций. - """ - with local_session() as session: - author = session.query(Author).filter(Author.slug == slug).first() - if author: - try: - author_id: int = author.dict()["id"] - shouts = await reacted_shouts_updates(info, author_id, limit, offset) - return shouts - except Exception as error: - logger.debug(error) - return [] diff --git a/schema/enum.graphql b/schema/enum.graphql index f4e472e9..310dc813 100644 --- a/schema/enum.graphql +++ b/schema/enum.graphql @@ -13,6 +13,12 @@ enum ReactionSort { dislike } +enum ShoutsOrderBy { + last_reacted_at + rating + comments_count +} + enum ReactionKind { # collabs diff --git a/schema/input.graphql b/schema/input.graphql index 34ae1399..4d79f76e 100644 --- a/schema/input.graphql +++ b/schema/input.graphql @@ -54,17 +54,16 @@ input LoadShoutsFilters { author: String layouts: [String] featured: Boolean - reacted: Boolean + reacted: Boolean # requires auth, used in load_shouts_feed after: Int } input LoadShoutsOptions { filters: LoadShoutsFilters - with_author_captions: Boolean limit: Int! random_limit: Int offset: Int - order_by: String + order_by: ShoutsOrderBy order_by_desc: Boolean } diff --git a/schema/query.graphql b/schema/query.graphql index b71f67c8..dbc0ac6b 100644 --- a/schema/query.graphql +++ b/schema/query.graphql @@ -24,21 +24,23 @@ type Query { # reaction load_reactions_by(by: ReactionBy!, limit: Int, offset: Int): [Reaction] + load_shout_comments(shout: Int!, limit: Int, offset: Int): [Reaction] + load_shout_ratings(shout: Int!, limit: Int, offset: Int): [Reaction] + load_comment_ratings(comment: Int!, limit: Int, offset: Int): [Reaction] # reader get_shout(slug: String, shout_id: Int): Shout load_shouts_by(options: LoadShoutsOptions): [Shout] - load_shout_comments(shout: Int!, limit: Int, offset: Int): [Reaction] - load_shout_ratings(shout: Int!, limit: Int, offset: Int): [Reaction] - load_comment_ratings(comment: Int!, limit: Int, offset: Int): [Reaction] - load_shouts_search(text: String!, limit: Int, offset: Int): [SearchResult] + load_shouts_search(text: String!, options: LoadShoutsOptions): [SearchResult] + load_shouts_bookmarked(options: LoadShoutsOptions): [Shout] + load_shouts_random_topic(options: LoadShoutsOptions): CommonResult! # { topic shouts } + + # feed load_shouts_feed(options: LoadShoutsOptions): [Shout] - load_shouts_unrated(limit: Int, offset: Int): [Shout] - load_shouts_coauthored(limit: Int, offset: Int): [Shout] - load_shouts_discussed(limit: Int, offset: Int): [Shout] - load_shouts_random_top(options: LoadShoutsOptions): [Shout] - load_shouts_random_topic(limit: Int!): CommonResult! # { topic shouts } - load_shouts_bookmarked(limit: Int, offset: Int): [Shout] + load_shouts_unrated(options: LoadShoutsOptions): [Shout] + load_shouts_coauthored(options: LoadShoutsOptions): [Shout] + load_shouts_discussed(options: LoadShoutsOptions): [Shout] + load_shouts_random_top(options: LoadShoutsOptions): [Shout] # random order, fixed filter, limit offset can be used # editor get_my_shout(shout_id: Int!): CommonResult!