This commit is contained in:
Untone 2024-11-01 09:50:19 +03:00
parent bcac627345
commit 5a9a02d3a4
11 changed files with 377 additions and 305 deletions

View File

@ -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

23
docs/load_shouts.md Normal file
View File

@ -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` - фильтрует публикации, по которым пользователь проголосовал, по умолчанию не применяется

View File

@ -1,5 +0,0 @@
## Reader resolvers
### load_shouts_by
Запрашиваемые поля: `stat`, `authors`, `topics` влияют на количество подзапросов.

View File

@ -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"

View File

@ -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,

View File

@ -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")

240
resolvers/feed.py Normal file
View File

@ -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 []

View File

@ -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 []

View File

@ -13,6 +13,12 @@ enum ReactionSort {
dislike
}
enum ShoutsOrderBy {
last_reacted_at
rating
comments_count
}
enum ReactionKind {
# collabs

View File

@ -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
}

View File

@ -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!