From 99dcfca89a0a76e9af461da79fa67d5362b3f9f3 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Mon, 28 Nov 2022 23:16:39 +0100 Subject: [PATCH] reaction stat, author stat --- resolvers/zine/_common.py | 31 +++++++ resolvers/zine/load.py | 27 +----- resolvers/zine/profile.py | 165 +++++++++++++++++++----------------- resolvers/zine/reactions.py | 51 ++++------- 4 files changed, 136 insertions(+), 138 deletions(-) create mode 100644 resolvers/zine/_common.py diff --git a/resolvers/zine/_common.py b/resolvers/zine/_common.py new file mode 100644 index 00000000..7046be92 --- /dev/null +++ b/resolvers/zine/_common.py @@ -0,0 +1,31 @@ +from sqlalchemy import func, case +from sqlalchemy.orm import aliased +from orm.reaction import Reaction, ReactionKind + + +def add_common_stat_columns(q): + aliased_reaction = aliased(Reaction) + + q = q.outerjoin(aliased_reaction).add_columns( + func.sum( + aliased_reaction.id + ).label('reacted_stat'), + func.sum( + case( + (aliased_reaction.body.is_not(None), 1), + else_=0 + ) + ).label('commented_stat'), + func.sum(case( + (aliased_reaction.kind == ReactionKind.AGREE, 1), + (aliased_reaction.kind == ReactionKind.DISAGREE, -1), + (aliased_reaction.kind == ReactionKind.PROOF, 1), + (aliased_reaction.kind == ReactionKind.DISPROOF, -1), + (aliased_reaction.kind == ReactionKind.ACCEPT, 1), + (aliased_reaction.kind == ReactionKind.REJECT, -1), + (aliased_reaction.kind == ReactionKind.LIKE, 1), + (aliased_reaction.kind == ReactionKind.DISLIKE, -1), + else_=0) + ).label('rating_stat')) + + return q diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index cfb9a413..bc76db02 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -6,36 +6,13 @@ from base.resolvers import query from orm import ViewedEntry from orm.shout import Shout, ShoutAuthor from orm.reaction import Reaction, ReactionKind +from resolvers.zine._common import add_common_stat_columns def add_stat_columns(q): q = q.outerjoin(ViewedEntry).add_columns(func.sum(ViewedEntry.amount).label('viewed_stat')) - aliased_reaction = aliased(Reaction) - - q = q.outerjoin(aliased_reaction).add_columns( - func.sum( - aliased_reaction.id - ).label('reacted_stat'), - func.sum( - case( - (aliased_reaction.body.is_not(None), 1), - else_=0 - ) - ).label('commented_stat'), - func.sum(case( - (aliased_reaction.kind == ReactionKind.AGREE, 1), - (aliased_reaction.kind == ReactionKind.DISAGREE, -1), - (aliased_reaction.kind == ReactionKind.PROOF, 1), - (aliased_reaction.kind == ReactionKind.DISPROOF, -1), - (aliased_reaction.kind == ReactionKind.ACCEPT, 1), - (aliased_reaction.kind == ReactionKind.REJECT, -1), - (aliased_reaction.kind == ReactionKind.LIKE, 1), - (aliased_reaction.kind == ReactionKind.DISLIKE, -1), - else_=0) - ).label('rating_stat')) - - return q + return add_common_stat_columns(q) def apply_filters(q, filters, user=None): diff --git a/resolvers/zine/profile.py b/resolvers/zine/profile.py index b28e7c24..be38eeff 100644 --- a/resolvers/zine/profile.py +++ b/resolvers/zine/profile.py @@ -1,14 +1,14 @@ from typing import List from datetime import datetime, timedelta, timezone -from sqlalchemy import and_, func, select -from sqlalchemy.orm import selectinload +from sqlalchemy import and_, func, distinct, select +from sqlalchemy.orm import aliased, joinedload from auth.authenticate import login_required from base.orm import local_session from base.resolvers import mutation, query from orm.reaction import Reaction from orm.shout import ShoutAuthor, ShoutTopic -from orm.topic import Topic, TopicFollower +from orm.topic import Topic from orm.user import AuthorFollower, Role, User, UserRating, UserRole # from .community import followed_communities @@ -16,6 +16,51 @@ from resolvers.inbox.unread import get_total_unread_counter from resolvers.zine.topics import followed_by_user +def add_author_stat_columns(q): + author_followers = aliased(AuthorFollower) + author_following = aliased(AuthorFollower) + + q = q.outerjoin(ShoutAuthor).add_columns( + func.count(distinct(ShoutAuthor.shout)).label('shouts_stat') + ).outerjoin(author_followers, author_followers.author == User.slug).add_columns( + func.count(distinct(author_followers.follower)).label('followers_stat') + ).outerjoin(author_following, author_following.follower == User.slug).add_columns( + func.count(distinct(author_following.author)).label('followings_stat') + ).outerjoin(UserRating).add_columns( + # TODO: check + func.sum(UserRating.value).label('rating_stat') + ).outerjoin(Reaction, and_(Reaction.createdBy == User.slug, Reaction.body.is_not(None))).add_columns( + func.count(distinct(Reaction.id)).label('commented_stat') + ) + + q = q.group_by(User.id) + + return q + + +def add_stat(author, stat_columns): + [shouts_stat, followers_stat, followings_stat, rating_stat, commented_stat] = stat_columns + author.stat = { + "shouts": shouts_stat, + "followers": followers_stat, + "followings": followings_stat, + "rating": rating_stat, + "commented": commented_stat + } + + return author + + +def get_authors_from_query(q): + authors = [] + with local_session() as session: + for [author, *stat_columns] in session.execute(q): + author = add_stat(author, stat_columns) + authors.append(author) + + return authors + + async def user_subscriptions(slug: str): return { "unread": await get_total_unread_counter(slug), # unread inbox messages counter @@ -26,23 +71,6 @@ async def user_subscriptions(slug: str): } -async def get_author_stat(slug): - with local_session() as session: - return { - "shouts": session.query(ShoutAuthor).where(ShoutAuthor.user == slug).count(), - "followers": session.query(AuthorFollower).where(AuthorFollower.author == slug).count(), - "followings": session.query(AuthorFollower).where(AuthorFollower.follower == slug).count(), - "rating": session.query(func.sum(UserRating.value)).where(UserRating.user == slug).first(), - "commented": session.query( - Reaction.id - ).where( - Reaction.createdBy == slug - ).filter( - Reaction.body.is_not(None) - ).count() - } - - # @query.field("userFollowedDiscussions") @login_required async def followed_discussions(_, info, slug) -> List[Topic]: @@ -77,29 +105,20 @@ async def get_followed_authors(_, _info, slug) -> List[User]: async def followed_authors(slug) -> List[User]: - authors = [] - with local_session() as session: - authors = ( - session.query(User) - .join(AuthorFollower, User.slug == AuthorFollower.author) - .where(AuthorFollower.follower == slug) - .all() - ) - for author in authors: - author.stat = await get_author_stat(author.slug) - return authors + q = select(User) + q = add_author_stat_columns(q) + q = q.join(AuthorFollower).where(AuthorFollower.follower == slug) + + return get_authors_from_query(q) @query.field("userFollowers") async def user_followers(_, _info, slug) -> List[User]: - with local_session() as session: - users = ( - session.query(User) - .join(AuthorFollower, User.slug == AuthorFollower.follower) - .where(AuthorFollower.author == slug) - .all() - ) - return users + q = select(User) + q = add_author_stat_columns(q) + q = q.join(AuthorFollower).where(AuthorFollower.author == slug) + + return get_authors_from_query(q) async def get_user_roles(slug): @@ -107,11 +126,12 @@ async def get_user_roles(slug): user = session.query(User).where(User.slug == slug).first() roles = ( session.query(Role) - .options(selectinload(Role.permissions)) + .options(joinedload(Role.permissions)) .join(UserRole) .where(UserRole.user_id == user.id) .all() ) + return roles @@ -179,50 +199,41 @@ def author_unfollow(user, slug): @query.field("authorsAll") async def get_authors_all(_, _info): - with local_session() as session: - authors = session.query(User).join(ShoutAuthor).all() - for author in authors: - author.stat = await get_author_stat(author.slug) - return authors + q = select(User) + q = add_author_stat_columns(q) + q = q.join(ShoutAuthor) + + return get_authors_from_query(q) @query.field("getAuthor") async def get_author(_, _info, slug): - with local_session() as session: - author = session.query(User).where(User.slug == slug).first() - author.stat = await get_author_stat(author.slug) - return author + q = select(User).where(User.slug == slug) + q = add_author_stat_columns(q) + + authors = get_authors_from_query(q) + return authors[0] @query.field("loadAuthorsBy") async def load_authors_by(_, info, by, limit, offset): - with local_session() as session: - aq = session.query(User) - if by.get("slug"): - aq = aq.filter(User.slug.ilike(f"%{by['slug']}%")) - elif by.get("name"): - aq = aq.filter(User.name.ilike(f"%{by['name']}%")) - elif by.get("topic"): - aq = aq.join(ShoutAuthor).join(ShoutTopic).where(ShoutTopic.topic == by["topic"]) - if by.get("lastSeen"): # in days - days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"]) - aq = aq.filter(User.lastSeen > days_before) - elif by.get("createdAt"): # in days - days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"]) - aq = aq.filter(User.createdAt > days_before) + q = select(User) + q = add_author_stat_columns(q) + if by.get("slug"): + q = q.filter(User.slug.ilike(f"%{by['slug']}%")) + elif by.get("name"): + q = q.filter(User.name.ilike(f"%{by['name']}%")) + elif by.get("topic"): + q = q.join(ShoutAuthor).join(ShoutTopic).where(ShoutTopic.topic == by["topic"]) + if by.get("lastSeen"): # in days + days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"]) + q = q.filter(User.lastSeen > days_before) + elif by.get("createdAt"): # in days + days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"]) + q = q.filter(User.createdAt > days_before) - aq = aq.group_by( - User.id - ).order_by( - by.get("order") or "createdAt" - ).limit(limit).offset(offset) + q = q.order_by( + by.get("order", User.createdAt) + ).limit(limit).offset(offset) - print(aq) - - authors = [] - for [author] in session.execute(aq): - if by.get("stat"): - author.stat = await get_author_stat(author.slug) - authors.append(author) - - return authors + return get_authors_from_query(q) diff --git a/resolvers/zine/reactions.py b/resolvers/zine/reactions.py index 4c239d19..876e4791 100644 --- a/resolvers/zine/reactions.py +++ b/resolvers/zine/reactions.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta, timezone -from sqlalchemy import and_, asc, desc, select, text, func, case +from sqlalchemy import and_, asc, desc, select, text, func from sqlalchemy.orm import aliased from auth.authenticate import login_required from base.orm import local_session @@ -7,34 +7,11 @@ from base.resolvers import mutation, query from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutReactionsFollower from orm.user import User +from resolvers.zine._common import add_common_stat_columns def add_reaction_stat_columns(q): - aliased_reaction = aliased(Reaction) - - q = q.outerjoin(aliased_reaction).add_columns( - func.sum( - aliased_reaction.id - ).label('reacted_stat'), - func.sum( - case( - (aliased_reaction.body.is_not(None), 1), - else_=0 - ) - ).label('commented_stat'), - func.sum(case( - (aliased_reaction.kind == ReactionKind.AGREE, 1), - (aliased_reaction.kind == ReactionKind.DISAGREE, -1), - (aliased_reaction.kind == ReactionKind.PROOF, 1), - (aliased_reaction.kind == ReactionKind.DISPROOF, -1), - (aliased_reaction.kind == ReactionKind.ACCEPT, 1), - (aliased_reaction.kind == ReactionKind.REJECT, -1), - (aliased_reaction.kind == ReactionKind.LIKE, 1), - (aliased_reaction.kind == ReactionKind.DISLIKE, -1), - else_=0) - ).label('rating_stat')) - - return q + return add_common_stat_columns(q) def reactions_follow(user: User, slug: str, auto=False): @@ -181,9 +158,9 @@ async def update_reaction(_, info, inp): with local_session() as session: user = session.query(User).where(User.id == user_id).first() q = select(Reaction).filter(Reaction.id == inp.id) - q = calc_reactions(q) + q = add_reaction_stat_columns(q) - [reaction, rating, commented, reacted] = session.execute(q).unique().one() + [reaction, reacted_stat, commented_stat, rating_stat] = session.execute(q).unique().one() if not reaction: return {"error": "invalid reaction id"} @@ -199,9 +176,9 @@ async def update_reaction(_, info, inp): reaction.range = inp.get("range") session.commit() reaction.stat = { - "commented": commented, - "reacted": reacted, - "rating": rating + "commented": commented_stat, + "reacted": reacted_stat, + "rating": rating_stat } return {"reaction": reaction} @@ -269,6 +246,7 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0): q = q.filter(Reaction.createdAt > after) order_way = asc if by.get("sort", "").startswith("-") else desc + # replace "-" -> "" ? order_field = by.get("sort") or Reaction.createdAt q = q.group_by( @@ -277,23 +255,24 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0): order_way(order_field) ) - q = calc_reactions(q) + q = add_reaction_stat_columns(q) q = q.where(Reaction.deletedAt.is_(None)) q = q.limit(limit).offset(offset) reactions = [] with local_session() as session: - for [reaction, user, shout, rating, commented, reacted] in session.execute(q): + for [reaction, user, shout, reacted_stat, commented_stat, rating_stat] in session.execute(q): reaction.createdBy = user reaction.shout = shout reaction.stat = { - "rating": rating, - "commented": commented, - "reacted": reacted + "rating": rating_stat, + "commented": commented_stat, + "reacted": reacted_stat } reactions.append(reaction) + # ? if by.get("stat"): reactions.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)