reaction stat, author stat

This commit is contained in:
Igor Lobanov 2022-11-28 23:16:39 +01:00
parent e5b3bdc63c
commit 99dcfca89a
4 changed files with 136 additions and 138 deletions

31
resolvers/zine/_common.py Normal file
View File

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

View File

@ -6,36 +6,13 @@ from base.resolvers import query
from orm import ViewedEntry from orm import ViewedEntry
from orm.shout import Shout, ShoutAuthor from orm.shout import Shout, ShoutAuthor
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from resolvers.zine._common import add_common_stat_columns
def add_stat_columns(q): def add_stat_columns(q):
q = q.outerjoin(ViewedEntry).add_columns(func.sum(ViewedEntry.amount).label('viewed_stat')) q = q.outerjoin(ViewedEntry).add_columns(func.sum(ViewedEntry.amount).label('viewed_stat'))
aliased_reaction = aliased(Reaction) return add_common_stat_columns(q)
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
def apply_filters(q, filters, user=None): def apply_filters(q, filters, user=None):

View File

@ -1,14 +1,14 @@
from typing import List from typing import List
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, distinct, select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import aliased, joinedload
from auth.authenticate import login_required from auth.authenticate import login_required
from base.orm import local_session from base.orm import local_session
from base.resolvers import mutation, query from base.resolvers import mutation, query
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.shout import ShoutAuthor, ShoutTopic 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 orm.user import AuthorFollower, Role, User, UserRating, UserRole
# from .community import followed_communities # 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 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): async def user_subscriptions(slug: str):
return { return {
"unread": await get_total_unread_counter(slug), # unread inbox messages counter "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") # @query.field("userFollowedDiscussions")
@login_required @login_required
async def followed_discussions(_, info, slug) -> List[Topic]: 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]: async def followed_authors(slug) -> List[User]:
authors = [] q = select(User)
with local_session() as session: q = add_author_stat_columns(q)
authors = ( q = q.join(AuthorFollower).where(AuthorFollower.follower == slug)
session.query(User)
.join(AuthorFollower, User.slug == AuthorFollower.author) return get_authors_from_query(q)
.where(AuthorFollower.follower == slug)
.all()
)
for author in authors:
author.stat = await get_author_stat(author.slug)
return authors
@query.field("userFollowers") @query.field("userFollowers")
async def user_followers(_, _info, slug) -> List[User]: async def user_followers(_, _info, slug) -> List[User]:
with local_session() as session: q = select(User)
users = ( q = add_author_stat_columns(q)
session.query(User) q = q.join(AuthorFollower).where(AuthorFollower.author == slug)
.join(AuthorFollower, User.slug == AuthorFollower.follower)
.where(AuthorFollower.author == slug) return get_authors_from_query(q)
.all()
)
return users
async def get_user_roles(slug): 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() user = session.query(User).where(User.slug == slug).first()
roles = ( roles = (
session.query(Role) session.query(Role)
.options(selectinload(Role.permissions)) .options(joinedload(Role.permissions))
.join(UserRole) .join(UserRole)
.where(UserRole.user_id == user.id) .where(UserRole.user_id == user.id)
.all() .all()
) )
return roles return roles
@ -179,50 +199,41 @@ def author_unfollow(user, slug):
@query.field("authorsAll") @query.field("authorsAll")
async def get_authors_all(_, _info): async def get_authors_all(_, _info):
with local_session() as session: q = select(User)
authors = session.query(User).join(ShoutAuthor).all() q = add_author_stat_columns(q)
for author in authors: q = q.join(ShoutAuthor)
author.stat = await get_author_stat(author.slug)
return authors return get_authors_from_query(q)
@query.field("getAuthor") @query.field("getAuthor")
async def get_author(_, _info, slug): async def get_author(_, _info, slug):
with local_session() as session: q = select(User).where(User.slug == slug)
author = session.query(User).where(User.slug == slug).first() q = add_author_stat_columns(q)
author.stat = await get_author_stat(author.slug)
return author authors = get_authors_from_query(q)
return authors[0]
@query.field("loadAuthorsBy") @query.field("loadAuthorsBy")
async def load_authors_by(_, info, by, limit, offset): async def load_authors_by(_, info, by, limit, offset):
with local_session() as session: q = select(User)
aq = session.query(User) q = add_author_stat_columns(q)
if by.get("slug"): if by.get("slug"):
aq = aq.filter(User.slug.ilike(f"%{by['slug']}%")) q = q.filter(User.slug.ilike(f"%{by['slug']}%"))
elif by.get("name"): elif by.get("name"):
aq = aq.filter(User.name.ilike(f"%{by['name']}%")) q = q.filter(User.name.ilike(f"%{by['name']}%"))
elif by.get("topic"): elif by.get("topic"):
aq = aq.join(ShoutAuthor).join(ShoutTopic).where(ShoutTopic.topic == by["topic"]) q = q.join(ShoutAuthor).join(ShoutTopic).where(ShoutTopic.topic == by["topic"])
if by.get("lastSeen"): # in days if by.get("lastSeen"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"]) days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"])
aq = aq.filter(User.lastSeen > days_before) q = q.filter(User.lastSeen > days_before)
elif by.get("createdAt"): # in days elif by.get("createdAt"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"]) days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"])
aq = aq.filter(User.createdAt > days_before) q = q.filter(User.createdAt > days_before)
aq = aq.group_by( q = q.order_by(
User.id by.get("order", User.createdAt)
).order_by( ).limit(limit).offset(offset)
by.get("order") or "createdAt"
).limit(limit).offset(offset)
print(aq) return get_authors_from_query(q)
authors = []
for [author] in session.execute(aq):
if by.get("stat"):
author.stat = await get_author_stat(author.slug)
authors.append(author)
return authors

View File

@ -1,5 +1,5 @@
from datetime import datetime, timedelta, timezone 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 sqlalchemy.orm import aliased
from auth.authenticate import login_required from auth.authenticate import login_required
from base.orm import local_session from base.orm import local_session
@ -7,34 +7,11 @@ from base.resolvers import mutation, query
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutReactionsFollower from orm.shout import Shout, ShoutReactionsFollower
from orm.user import User from orm.user import User
from resolvers.zine._common import add_common_stat_columns
def add_reaction_stat_columns(q): def add_reaction_stat_columns(q):
aliased_reaction = aliased(Reaction) return add_common_stat_columns(q)
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
def reactions_follow(user: User, slug: str, auto=False): def reactions_follow(user: User, slug: str, auto=False):
@ -181,9 +158,9 @@ async def update_reaction(_, info, inp):
with local_session() as session: with local_session() as session:
user = session.query(User).where(User.id == user_id).first() user = session.query(User).where(User.id == user_id).first()
q = select(Reaction).filter(Reaction.id == inp.id) 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: if not reaction:
return {"error": "invalid reaction id"} return {"error": "invalid reaction id"}
@ -199,9 +176,9 @@ async def update_reaction(_, info, inp):
reaction.range = inp.get("range") reaction.range = inp.get("range")
session.commit() session.commit()
reaction.stat = { reaction.stat = {
"commented": commented, "commented": commented_stat,
"reacted": reacted, "reacted": reacted_stat,
"rating": rating "rating": rating_stat
} }
return {"reaction": reaction} return {"reaction": reaction}
@ -269,6 +246,7 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
q = q.filter(Reaction.createdAt > after) q = q.filter(Reaction.createdAt > after)
order_way = asc if by.get("sort", "").startswith("-") else desc order_way = asc if by.get("sort", "").startswith("-") else desc
# replace "-" -> "" ?
order_field = by.get("sort") or Reaction.createdAt order_field = by.get("sort") or Reaction.createdAt
q = q.group_by( q = q.group_by(
@ -277,23 +255,24 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
order_way(order_field) order_way(order_field)
) )
q = calc_reactions(q) q = add_reaction_stat_columns(q)
q = q.where(Reaction.deletedAt.is_(None)) q = q.where(Reaction.deletedAt.is_(None))
q = q.limit(limit).offset(offset) q = q.limit(limit).offset(offset)
reactions = [] reactions = []
with local_session() as session: 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.createdBy = user
reaction.shout = shout reaction.shout = shout
reaction.stat = { reaction.stat = {
"rating": rating, "rating": rating_stat,
"commented": commented, "commented": commented_stat,
"reacted": reacted "reacted": reacted_stat
} }
reactions.append(reaction) reactions.append(reaction)
# ?
if by.get("stat"): if by.get("stat"):
reactions.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt) reactions.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)