diff --git a/resolvers/author.py b/resolvers/author.py index 3b0223ae..fd01c2cc 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -10,7 +10,7 @@ from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic from resolvers.follower import query_follows -from resolvers.stat import get_authors_from_query, add_author_stat_columns +from resolvers.stat import add_stat_columns, get_with_stat, unpack_stat from services.auth import login_required from services.db import local_session from services.rediscache import redis @@ -20,7 +20,7 @@ from services.logger import root_logger as logger @mutation.field('update_author') @login_required -async def update_author(_, info, profile): +def update_author(_, info, profile): user_id = info.context['user_id'] with local_session() as session: author = session.query(Author).where(Author.user == user_id).first() @@ -32,7 +32,7 @@ async def update_author(_, info, profile): # TODO: caching query @query.field('get_authors_all') -async def get_authors_all(_, _info): +def get_authors_all(_, _info): authors = [] with local_session() as session: authors = session.query(Author).all() @@ -97,10 +97,8 @@ def count_author_shouts_rating(session, author_id) -> int: return shouts_likes - shouts_dislikes -async def load_author_with_stats(q): - q = add_author_stat_columns(q) - - result = await get_authors_from_query(q) +def load_author_with_stats(q): + result = get_with_stat(q, Author, AuthorFollower) if result: [author] = result @@ -140,7 +138,7 @@ async def load_author_with_stats(q): @query.field('get_author') -async def get_author(_, _info, slug='', author_id=None): +def get_author(_, _info, slug='', author_id=None): q = None if slug or author_id: if bool(slug): @@ -148,7 +146,7 @@ async def get_author(_, _info, slug='', author_id=None): if author_id: q = select(Author).where(Author.id == author_id) - return await load_author_with_stats(q) + return load_author_with_stats(q) async def get_author_by_user_id(user_id: str): @@ -162,7 +160,7 @@ async def get_author_by_user_id(user_id: str): logger.info(f'getting author id for {user_id}') q = select(Author).filter(Author.user == user_id) - author = await load_author_with_stats(q) + author = load_author_with_stats(q) if author: update_author(author) return author @@ -174,9 +172,8 @@ async def get_author_id(_, _info, user: str): @query.field('load_authors_by') -async def load_authors_by(_, _info, by, limit, offset): +def load_authors_by(_, _info, by, limit, offset): q = select(Author) - q = add_author_stat_columns(q) if by.get('slug'): q = q.filter(Author.slug.ilike(f"%{by['slug']}%")) elif by.get('name'): @@ -201,14 +198,14 @@ async def load_authors_by(_, _info, by, limit, offset): q = q.order_by(desc(f'{order}_stat')) q = q.limit(limit).offset(offset) - - authors = await get_authors_from_query(q) + q = q.group_by(Author.id) + authors = get_with_stat(q, Author, AuthorFollower) return authors @query.field('get_author_follows') -async def get_author_follows( +def get_author_follows( _, _info, slug='', user=None, author_id=None ) -> List[Author]: with local_session() as session: @@ -228,7 +225,7 @@ async def get_author_follows( @mutation.field('rate_author') @login_required -async def rate_author(_, info, rated_slug, value): +def rate_author(_, info, rated_slug, value): user_id = info.context['user_id'] with local_session() as session: @@ -262,7 +259,7 @@ async def rate_author(_, info, rated_slug, value): return {} -async def create_author(user_id: str, slug: str, name: str = ''): +def create_author(user_id: str, slug: str, name: str = ''): with local_session() as session: new_author = Author(user=user_id, slug=slug, name=name) session.add(new_author) @@ -271,15 +268,15 @@ async def create_author(user_id: str, slug: str, name: str = ''): @query.field('get_author_followers') -async def get_author_followers(_, _info, slug) -> List[Author]: - q = select(Author) - q = add_author_stat_columns(q) - +def get_author_followers(_, _info, slug) -> List[Author]: aliased_author = aliased(Author) + q = select(aliased_author) + q = ( q.join(AuthorFollower, AuthorFollower.follower == Author.id) .join(aliased_author, aliased_author.id == AuthorFollower.author) .where(aliased_author.slug == slug) ) - - return await get_authors_from_query(q) + q = add_stat_columns(q, aliased_author, AuthorFollower) + q = q.group_by(aliased_author.id) + return unpack_stat(q) diff --git a/resolvers/follower.py b/resolvers/follower.py index 0ccee9b7..0ec30162 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -14,7 +14,7 @@ from orm.shout import Shout, ShoutReactionsFollower from orm.topic import Topic, TopicFollower from resolvers.community import community_follow, community_unfollow from resolvers.topic import topic_follow, topic_unfollow -from resolvers.stat import add_topic_stat_columns, get_topics_from_query, add_author_stat_columns +from resolvers.stat import add_stat_columns, unpack_stat from services.auth import login_required from services.db import local_session from services.follows import DEFAULT_FOLLOWS @@ -110,55 +110,14 @@ def query_follows(user_id: str): .filter(TopicFollower.topic == Topic.id) ) - authors_query = add_author_stat_columns(authors_query) - topics_query = add_topic_stat_columns(topics_query) - authors = [ - { - 'id': author_id, - 'name': author.name, - 'slug': author.slug, - 'pic': author.pic, - 'bio': author.bio, - 'last_seen': author.last_seen or int(time.time()), - 'stat': { - 'shouts': shouts_stat, - 'followers': followers_stat, - 'followings': followings_stat, # TODO: rename to authors to - # TODO: use graphql to reserve universal type Stat { authors shouts followers views comments } - }, - } - for [ - author, - shouts_stat, - followers_stat, - followings_stat, - ] in session.execute(authors_query) - ] - - topics = [ - { - 'id': topic.id, - 'title': topic.title, - 'slug': topic.slug, - 'body': topic.body, - 'stat': { - 'shouts': shouts_stat, - 'authors': authors_stat, - 'followers': followers_stat, - }, - } - for [ - topic, - shouts_stat, - authors_stat, - followers_stat, - ] in session.execute(topics_query) - ] + authors_query = add_stat_columns(authors_query, aliased_author, AuthorFollower) + authors = unpack_stat(authors_query) + topics_query = add_stat_columns(topics_query, aliased_author, TopicFollower) + authors = unpack_stat(topics_query) return { 'topics': topics, 'authors': authors, - # Include other results (e.g., shouts) if needed 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], } @@ -260,17 +219,17 @@ def author_unfollow(follower_id, slug): @query.field('get_topic_followers') -async def get_topic_followers(_, _info, slug: str, topic_id: int) -> List[Author]: +def get_topic_followers(_, _info, slug: str, topic_id: int) -> List[Author]: q = select(Author) - q = add_topic_stat_columns(q) - q = ( q.join(TopicFollower, TopicFollower.follower == Author.id) .join(Topic, Topic.id == TopicFollower.topic) .filter(or_(Topic.slug == slug, Topic.id == topic_id)) ) + q = add_stat_columns(q, Author, TopicFollower) + q = q.group_by(Author.id) - return await get_topics_from_query(q) + return unpack_stat(q) @query.field('get_shout_followers') diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 1167d053..920fefcd 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -19,7 +19,7 @@ from services.viewed import ViewedStorage from services.logger import root_logger as logger -def add_stat_columns(q, aliased_reaction): +def add_reaction_stat_columns(q, aliased_reaction): q = q.outerjoin(aliased_reaction).add_columns( func.sum(aliased_reaction.id).label('reacted_stat'), func.sum( @@ -229,7 +229,7 @@ async def update_reaction(_, info, reaction): with local_session() as session: reaction_query = select(Reaction).filter(Reaction.id == int(rid)) aliased_reaction = aliased(Reaction) - reaction_query = add_stat_columns(reaction_query, aliased_reaction) + reaction_query = add_reaction_stat_columns(reaction_query, aliased_reaction) reaction_query = reaction_query.group_by(Reaction.id) try: @@ -358,7 +358,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): # calculate counters aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) # filter q = apply_reaction_filters(by, q) @@ -425,7 +425,7 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S .outerjoin(Author, Shout.authors.any(id=follower_id)) .options(joinedload(Shout.reactions), joinedload(Shout.authors)) ) - q1 = add_stat_columns(q1, aliased(Reaction)) + q1 = add_reaction_stat_columns(q1, aliased(Reaction)) q1 = q1.filter(Author.id == follower_id).group_by(Shout.id) # Shouts where follower reacted @@ -436,7 +436,7 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S .filter(Reaction.created_by == follower_id) .group_by(Shout.id) ) - q2 = add_stat_columns(q2, aliased(Reaction)) + q2 = add_reaction_stat_columns(q2, aliased(Reaction)) # Sort shouts by the `last_comment` field combined_query = ( diff --git a/resolvers/reader.py b/resolvers/reader.py index 985e37f0..7bb4ac24 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -7,7 +7,7 @@ from orm.author import Author, AuthorFollower from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic, TopicFollower -from resolvers.reaction import add_stat_columns +from resolvers.reaction import add_reaction_stat_columns from resolvers.topic import get_random_topic from services.auth import login_required from services.db import local_session @@ -46,7 +46,7 @@ async def get_shout(_, _info, slug=None, shout_id=None): with local_session() as session: q = select(Shout).options(joinedload(Shout.authors), joinedload(Shout.topics)) aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) if slug is not None: q = q.filter(Shout.slug == slug) @@ -133,7 +133,7 @@ async def load_shouts_by(_, _info, options): # stats aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) # filters filters = options.get('filters', {}) @@ -269,7 +269,7 @@ async def load_shouts_feed(_, info, options): ) aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) filters = options.get('filters', {}) q = apply_filters(q, filters, reader.id) @@ -366,7 +366,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0): q = q.having(func.count(distinct(Reaction.id)) <= 4) aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) q = q.group_by(Shout.id).order_by(func.random()).limit(limit).offset(offset) user_id = info.context.get('user_id') @@ -450,7 +450,7 @@ async def load_shouts_random_top(_, _info, options): .where(Shout.id.in_(subquery)) ) aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) limit = options.get('limit', 10) q = q.group_by(Shout.id).order_by(func.random()).limit(limit) @@ -486,7 +486,7 @@ def fetch_shouts_by_topic(topic, limit): ) aliased_reaction = aliased(Reaction) - q = add_stat_columns(q, aliased_reaction) + q = add_reaction_stat_columns(q, aliased_reaction) q = q.group_by(Shout.id).order_by(desc(Shout.created_at)).limit(limit) diff --git a/resolvers/stat.py b/resolvers/stat.py index 1bcf1f9f..94dddd46 100644 --- a/resolvers/stat.py +++ b/resolvers/stat.py @@ -1,81 +1,36 @@ from sqlalchemy import func, distinct -from sqlalchemy.orm import aliased -from orm.author import Author, AuthorFollower -from orm.shout import ShoutAuthor, ShoutTopic -from orm.topic import Topic, TopicFollower from services.db import local_session -# from services.viewed import ViewedStorage +from orm.author import AuthorFollower +from orm.shout import ShoutTopic, ShoutAuthor -def add_author_stat_columns(q): - shout_author_aliased = aliased(ShoutAuthor) - q = q.outerjoin(shout_author_aliased).add_columns( - func.count(distinct(shout_author_aliased.shout)).label('shouts_stat') - ) - - authors_table = aliased(AuthorFollower) +def add_stat_columns(q, author_alias, follower_model_alias): + shouts_stat_model = ShoutAuthor if isinstance(follower_model_alias, AuthorFollower) else ShoutTopic + q = q.outerjoin(shouts_stat_model).add_columns(func.count(distinct(shouts_stat_model.shout)).label('shouts_stat')) q = q.outerjoin( - authors_table, authors_table.follower == Author.id - ).add_columns(func.count(distinct(authors_table.author)).label('authors_stat')) - - followers_table = aliased(AuthorFollower) - q = q.outerjoin(followers_table, followers_table.author == Author.id).add_columns( - func.count(distinct(followers_table.follower)).label('followers_stat') + follower_model_alias, follower_model_alias.follower == author_alias.id + ).add_columns(func.count(distinct(follower_model_alias.author)).label('authors_stat')) + q = q.outerjoin(follower_model_alias, follower_model_alias.author == author_alias.id).add_columns( + func.count(distinct(follower_model_alias.follower)).label('followers_stat') ) - - q = q.group_by(Author.id) return q -async def get_authors_from_query(q): - authors = [] +def unpack_stat(q): + records = [] with local_session() as session: - for [author, shouts_stat, authors_stat, followers_stat] in session.execute(q): - author.stat = { - 'shouts': shouts_stat, - 'followers': followers_stat, - 'followings': authors_stat, - # viewed - } - authors.append(author) - return authors - - -def add_topic_stat_columns(q): - aliased_shout_author = aliased(ShoutAuthor) - aliased_topic_follower = aliased(TopicFollower) - - q = ( - q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic) - .add_columns(func.count(distinct(ShoutTopic.shout)).label('shouts_stat')) - .outerjoin(aliased_shout_author, ShoutTopic.shout == aliased_shout_author.shout) - .add_columns( - func.count(distinct(aliased_shout_author.author)).label('authors_stat') - ) - .outerjoin(aliased_topic_follower) - .add_columns( - func.count(distinct(aliased_topic_follower.follower)).label( - 'followers_stat' - ) - ) - ) - - q = q.group_by(Topic.id) - - return q - - -async def get_topics_from_query(q): - topics = [] - with local_session() as session: - for [topic, shouts_stat, authors_stat, followers_stat] in session.execute(q): - topic.stat = { + for [entity, shouts_stat, authors_stat, followers_stat] in session.execute(q): + entity.stat = { 'shouts': shouts_stat, 'authors': authors_stat, - 'followers': followers_stat, - # 'viewed': await ViewedStorage.get_topic(topic.slug), + 'followers': followers_stat } - topics.append(topic) + records.append(entity) - return topics + return records + + +def get_with_stat(q, author_alias, follower_model_alias): + q = add_stat_columns(q, author_alias, follower_model_alias) + return unpack_stat(q) diff --git a/resolvers/topic.py b/resolvers/topic.py index d4b42a9f..3e1615de 100644 --- a/resolvers/topic.py +++ b/resolvers/topic.py @@ -3,7 +3,7 @@ from sqlalchemy import and_, distinct, func, select from orm.author import Author from orm.shout import ShoutTopic from orm.topic import Topic, TopicFollower -from resolvers.stat import add_topic_stat_columns, get_topics_from_query +from resolvers.stat import get_with_stat from services.auth import login_required from services.db import local_session from services.schema import mutation, query @@ -11,33 +11,24 @@ from services.logger import root_logger as logger @query.field('get_topics_all') -async def get_topics_all(_, _info): +def get_topics_all(_, _info): q = select(Topic) - q = add_topic_stat_columns(q) - - return await get_topics_from_query(q) - - -async def topics_followed_by(author_id): - q = select(Topic, TopicFollower) - q = add_topic_stat_columns(q) - q = q.join(TopicFollower).where(TopicFollower.follower == author_id) - - return await get_topics_from_query(q) + q = q.group_by(Topic.id) + topics = get_with_stat(q, Author, TopicFollower) + return topics @query.field('get_topics_by_community') async def get_topics_by_community(_, _info, community_id: int): q = select(Topic).where(Topic.community == community_id) - q = add_topic_stat_columns(q) - - return await get_topics_from_query(q) + q = q.group_by(Topic.id) + topics = await get_with_stat(q, Author, TopicFollower) + return topics @query.field('get_topics_by_author') async def get_topics_by_author(_, _info, author_id=None, slug='', user=''): q = select(Topic) - q = add_topic_stat_columns(q) if author_id: q = q.join(Author).where(Author.id == author_id) elif slug: @@ -45,15 +36,16 @@ async def get_topics_by_author(_, _info, author_id=None, slug='', user=''): elif user: q = q.join(Author).where(Author.user == user) - return await get_topics_from_query(q) + q = q.group_by(Topic.id) + topics = await get_with_stat(q, Author, TopicFollower) + return topics @query.field('get_topic') async def get_topic(_, _info, slug): - q = select(Topic).where(Topic.slug == slug) - q = add_topic_stat_columns(q) - - topics = await get_topics_from_query(q) + q = select(Topic).filter(Topic.slug == slug) + q = q.group_by(Topic.id) + topics = await get_with_stat(q, Author, TopicFollower) if topics: return topics[0] diff --git a/schema/type.graphql b/schema/type.graphql index fa2b2307..99173521 100644 --- a/schema/type.graphql +++ b/schema/type.graphql @@ -1,14 +1,6 @@ -type AuthorFollowings { - unread: Int - topics: [String] - authors: [String] - reactions: [Int] - communities: [String] -} - type AuthorStat { shouts: Int - followings: Int + authors: Int followers: Int rating: Int rating_shouts: Int diff --git a/services/follows.py b/services/follows.py index ff8a93c3..a0ec1995 100644 --- a/services/follows.py +++ b/services/follows.py @@ -5,7 +5,7 @@ import json from orm.author import Author, AuthorFollower from orm.topic import Topic, TopicFollower -from resolvers.stat import add_author_stat_columns, add_topic_stat_columns +from resolvers.stat import get_with_stat from services.rediscache import redis @@ -17,6 +17,7 @@ DEFAULT_FOLLOWS = { ], } + async def update_author(author: Author, ttl = 25 * 60 * 60): redis_key = f'user:{author.user}:author' await redis.execute('SETEX', redis_key, ttl, json.dumps(author.dict())) @@ -77,23 +78,15 @@ async def update_follows_for_user( async def handle_author_follower_change(connection, author_id, follower_id, is_insert): q = select(Author).filter(Author.id == author_id) - q = add_author_stat_columns(q) + authors = get_with_stat(q, Author, AuthorFollower) + author = authors[0] async with connection.begin() as conn: - [author, shouts_stat, followers_stat, followings_stat] = await conn.execute( - q - ).first() - author.stat = { - 'shouts': shouts_stat, - # 'viewed': await ViewedStorage.get_author(author.slug), - 'followers': followers_stat, - 'followings': followings_stat, - } follower = await conn.execute( select(Author).filter(Author.id == follower_id) ).first() if follower and author: await update_follows_for_user( - connection, + conn, follower.user, 'author', { @@ -110,23 +103,14 @@ async def handle_author_follower_change(connection, author_id, follower_id, is_i async def handle_topic_follower_change(connection, topic_id, follower_id, is_insert): q = select(Topic).filter(Topic.id == topic_id) - q = add_topic_stat_columns(q) + topics = get_with_stat(q, Author, TopicFollower) + topic = topics[0] + async with connection.begin() as conn: - [topic, shouts_stat, authors_stat, followers_stat] = await conn.execute( - q - ).first() - topic.stat = { - 'shouts': shouts_stat, - 'authors': authors_stat, - 'followers': followers_stat, - # 'viewed': await ViewedStorage.get_topic(topic.slug), - } - follower = connection.execute( - select(Author).filter(Author.id == follower_id) - ).first() + follower = conn.execute(select(Author).filter(Author.id == follower_id)).first() if follower and topic: await update_follows_for_user( - connection, + conn, follower.user, 'topic', {