diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 030351b0..6a6f7ae7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +[0.2.17] +- schema: enum types workaround, ReactionKind, InviteStatus, ShoutVisibility +- resovlers: optimized reacted shouts updates query + [0.2.16] - resolvers: collab inviting logics - resolvers: queries and mutations revision and renaming diff --git a/orm/invite.py b/orm/invite.py index e8c7fdfa..62dd4bb1 100644 --- a/orm/invite.py +++ b/orm/invite.py @@ -7,9 +7,9 @@ from enum import Enum as Enumeration class InviteStatus(Enumeration): - PENDING = 0 - ACCEPTED = 1 - REJECTED = 2 + PENDING = "PENDING" + ACCEPTED = "ACCEPTED" + REJECTED = "REJECTED" class Invite(Base): @@ -18,7 +18,7 @@ class Invite(Base): inviter_id = Column(ForeignKey("author.id"), nullable=False, index=True) author_id = Column(ForeignKey("author.id"), nullable=False, index=True) shout_id = Column(ForeignKey("shout.id"), nullable=False, index=True) - status = Column(Enum(InviteStatus), default=InviteStatus.PENDING) + status = Column(Enum(InviteStatus), default=InviteStatus.PENDING.value) inviter = relationship(Author, foreign_keys=[inviter_id]) author = relationship(Author, foreign_keys=[author_id]) diff --git a/orm/reaction.py b/orm/reaction.py index f3c0a9f4..cd9332b3 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -10,20 +10,20 @@ class ReactionKind(Enumeration): # TYPE = # rating diff # editor mode - AGREE = 1 # +1 - DISAGREE = 2 # -1 - ASK = 3 # +0 - PROPOSE = 4 # +0 - PROOF = 5 # +1 - DISPROOF = 6 # -1 - ACCEPT = 7 # +1 - REJECT = 8 # -1 + AGREE = "AGREE" # +1 + DISAGREE = "DISAGREE" # -1 + ASK = "ASK" # +0 + PROPOSE = "PROPOSE" # +0 + PROOF = "PROOF" # +1 + DISPROOF = "DISPROOF" # -1 + ACCEPT = "ACCEPT" # +1 + REJECT = "REJECT" # -1 # public feed - QUOTE = 9 # +0 TODO: use to bookmark in collection - COMMENT = 0 # +0 - LIKE = 11 # +1 - DISLIKE = 12 # -1 + QUOTE = "QUOTE" # +0 TODO: use to bookmark in collection + COMMENT = "COMMENT" # +0 + LIKE = "LIKE" # +1 + DISLIKE = "DISLIKE" # -1 class Reaction(Base): @@ -31,13 +31,13 @@ class Reaction(Base): body = Column(String, default="", comment="Reaction Body") created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) - created_by = Column(ForeignKey("author.id"), nullable=False, index=True) updated_at = Column(Integer, nullable=True, comment="Updated at") deleted_at = Column(Integer, nullable=True, comment="Deleted at") deleted_by = Column(ForeignKey("author.id"), nullable=True, index=True) - shout = Column(ForeignKey("shout.id"), nullable=False, index=True) reply_to = Column(ForeignKey("reaction.id"), nullable=True) quote = Column(String, nullable=True, comment="Original quoted text") - kind = Column(Enum(ReactionKind), nullable=False) + shout = Column(ForeignKey("shout.id"), nullable=False, index=True) + created_by = Column(ForeignKey("author.id"), nullable=False, index=True) + kind = Column(Enum(ReactionKind), nullable=False, index=True) oid = Column(String) diff --git a/orm/shout.py b/orm/shout.py index 185777d6..7ff2975c 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -48,9 +48,9 @@ class ShoutCommunity(Base): class ShoutVisibility(Enumeration): - AUTHORS = 0 - COMMUNITY = 1 - PUBLIC = 2 + AUTHORS = "AUTHORS" + COMMUNITY = "COMMUNITY" + PUBLIC = "PUBLIC" class Shout(Base): @@ -78,7 +78,7 @@ class Shout(Base): communities = relationship(lambda: Community, secondary="shout_community") reactions = relationship(lambda: Reaction) - visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS) + visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS.value) lang = Column(String, nullable=False, default="ru", comment="Language") version_of = Column(ForeignKey("shout.id"), nullable=True) diff --git a/pyproject.toml b/pyproject.toml index e47c7c33..e63ba481 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "discoursio-core" -version = "0.2.16" +version = "0.2.17" description = "core module for discours.io" authors = ["discoursio devteam"] license = "MIT" diff --git a/resolvers/author.py b/resolvers/author.py index 7c53c6a4..ca0369f9 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -41,15 +41,15 @@ def add_author_stat_columns(q): q.outerjoin(reaction_aliased, reaction_aliased.shout == shout_author_aliased.shout) .add_columns( func.coalesce(func.sum(case([ - (reaction_aliased.kind == ReactionKind.LIKE, 1), - (reaction_aliased.kind == ReactionKind.DISLIKE, -1), + (reaction_aliased.kind == ReactionKind.LIKE.value, 1), + (reaction_aliased.kind == ReactionKind.DISLIKE.value, -1), ], else_=0)), 0) .label("rating_stat") ) ) q = q.add_columns( - func.count(case([(reaction_aliased.kind == ReactionKind.COMMENT, 1)], else_=0)).label("commented_stat") + func.count(case([(reaction_aliased.kind == ReactionKind.COMMENT.value, 1)], else_=0)).label("commented_stat") ) # Filter based on shouts where the user is the author diff --git a/resolvers/collab.py b/resolvers/collab.py index 75775288..0098ecd4 100644 --- a/resolvers/collab.py +++ b/resolvers/collab.py @@ -17,7 +17,7 @@ async def accept_invite(_, info, invite_id: int): if author: # Check if the invite exists invite = session.query(Invite).filter(Invite.id == invite_id).first() - if invite and invite.author_id == author.id and invite.status == InviteStatus.PENDING: + if invite and invite.author_id == author.id and invite.status == InviteStatus.PENDING.value: # Add the user to the shout authors shout = session.query(Shout).filter(Shout.id == invite.shout_id).first() if shout: @@ -44,7 +44,7 @@ async def reject_invite(_, info, invite_id: int): if author: # Check if the invite exists invite = session.query(Invite).filter(Invite.id == invite_id).first() - if invite and invite.author_id == author.id and invite.status == InviteStatus.PENDING: + if invite and invite.author_id == author.id and invite.status == InviteStatus.PENDING.value: # Delete the invite session.delete(invite) session.commit() @@ -75,7 +75,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = None): Invite.inviter_id == inviter.id, Invite.author_id == author_id, Invite.shout_id == shout.id, - Invite.status == InviteStatus.PENDING, + Invite.status == InviteStatus.PENDING.value, ) .first() ) @@ -84,7 +84,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = None): # Create a new invite new_invite = Invite( - inviter_id=user_id, author_id=author_id, shout_id=shout.id, status=InviteStatus.PENDING + inviter_id=user_id, author_id=author_id, shout_id=shout.id, status=InviteStatus.PENDING.value ) session.add(new_invite) session.commit() @@ -126,7 +126,7 @@ async def remove_invite(_, info, invite_id: int): shout = session.query(Shout).filter(Shout.id == invite.shout_id).first() if shout and shout.deleted_at is None and invite: if invite.inviter_id == author.id or author.id == shout.authors.index(0): - if invite.status == InviteStatus.PENDING: + if invite.status == InviteStatus.PENDING.value: # Delete the invite session.delete(invite) session.commit() diff --git a/resolvers/editor.py b/resolvers/editor.py index e3d7c708..732162a1 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -58,7 +58,7 @@ async def create_shout(_, info, inp): "authors": authors, "slug": inp.get("slug") or f"draft-{time.time()}", "topics": inp.get("topics"), - "visibility": ShoutVisibility.AUTHORS, + "visibility": ShoutVisibility.AUTHORS.value, "created_at": current_time, # Set created_at as Unix timestamp } ) @@ -144,9 +144,9 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False): Shout.update(shout, shout_input) session.add(shout) if publish: - if shout.visibility is ShoutVisibility.AUTHORS: + if shout.visibility is ShoutVisibility.AUTHORS.value: shout_dict = shout.dict() - shout_dict["visibility"] = ShoutVisibility.COMMUNITY + shout_dict["visibility"] = ShoutVisibility.COMMUNITY.value shout_dict["published_at"] = current_time # Set published_at as Unix timestamp Shout.update(shout, shout_dict) session.add(shout) diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 4f9dd0c3..53ec8c81 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -2,7 +2,7 @@ import time from typing import List from sqlalchemy import and_, asc, desc, select, text, func, case -from sqlalchemy.orm import aliased +from sqlalchemy.orm import aliased, joinedload from services.notify import notify_reaction from services.auth import login_required from services.db import local_session @@ -15,23 +15,27 @@ from orm.author import Author def add_reaction_stat_columns(q): aliased_reaction = aliased(Reaction) - q = q.outerjoin(aliased_reaction, Reaction.id == aliased_reaction.reply_to).add_columns( - func.sum(aliased_reaction.id).label("reacted_stat"), - func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT, 1), else_=0)).label("commented_stat"), + q = ( + q.outerjoin(aliased_reaction, Reaction.id == aliased_reaction.reply_to).add_columns( + func.sum(aliased_reaction.id) + .label("reacted_stat"), + func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT.value, 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), + (aliased_reaction.kind == ReactionKind.AGREE.value, 1), + (aliased_reaction.kind == ReactionKind.DISAGREE.value, -1), + (aliased_reaction.kind == ReactionKind.PROOF.value, 1), + (aliased_reaction.kind == ReactionKind.DISPROOF.value, -1), + (aliased_reaction.kind == ReactionKind.ACCEPT.value, 1), + (aliased_reaction.kind == ReactionKind.REJECT.value, -1), + (aliased_reaction.kind == ReactionKind.LIKE.value, 1), + (aliased_reaction.kind == ReactionKind.DISLIKE.value, -1), else_=0, ) - ).label("rating_stat"), - ) + ) + .label("rating_stat"), + )) return q, aliased_reaction @@ -100,9 +104,9 @@ def is_published_author(session, author_id): def check_to_publish(session, author_id, reaction): """set shout to public if publicated approvers amount > 4""" if not reaction.reply_to and reaction.kind in [ - ReactionKind.ACCEPT, - ReactionKind.LIKE, - ReactionKind.PROOF, + ReactionKind.ACCEPT.value, + ReactionKind.LIKE.value, + ReactionKind.PROOF.value, ]: if is_published_author(session, author_id): # now count how many approvers are voted already @@ -122,18 +126,18 @@ def check_to_publish(session, author_id, reaction): def check_to_hide(session, reaction): """hides any shout if 20% of reactions are negative""" if not reaction.reply_to and reaction.kind in [ - ReactionKind.REJECT, - ReactionKind.DISLIKE, - ReactionKind.DISPROOF, + ReactionKind.REJECT.value, + ReactionKind.DISLIKE.value, + ReactionKind.DISPROOF.value, ]: # if is_published_author(author_id): approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all() rejects = 0 for r in approvers_reactions: if r.kind in [ - ReactionKind.REJECT, - ReactionKind.DISLIKE, - ReactionKind.DISPROOF, + ReactionKind.REJECT.value, + ReactionKind.DISLIKE.value, + ReactionKind.DISPROOF.value, ]: rejects += 1 if len(approvers_reactions) / rejects < 5: @@ -165,7 +169,7 @@ async def create_reaction(_, info, reaction): author = session.query(Author).where(Author.user == user_id).first() if shout and author: reaction["created_by"] = author.id - if reaction["kind"] in [ReactionKind.DISLIKE.name, ReactionKind.LIKE.name]: + if reaction["kind"] in [ReactionKind.DISLIKE.value, ReactionKind.LIKE.value]: existing_reaction = ( session.query(Reaction) .where( @@ -183,7 +187,7 @@ async def create_reaction(_, info, reaction): return {"error": "You can't vote twice"} opposite_reaction_kind = ( - ReactionKind.DISLIKE if reaction["kind"] == ReactionKind.LIKE.name else ReactionKind.LIKE + ReactionKind.DISLIKE.value if reaction["kind"] == ReactionKind.LIKE.value else ReactionKind.LIKE.value ) opposite_reaction = ( session.query(Reaction) @@ -205,10 +209,10 @@ async def create_reaction(_, info, reaction): rdict = r.dict() # Proposal accepting logix if rdict.get("reply_to"): - if r.kind is ReactionKind.ACCEPT and author.id in shout.authors: + if r.kind is ReactionKind.ACCEPT.value and author.id in shout.authors: replied_reaction = session.query(Reaction).where(Reaction.id == r.reply_to).first() if replied_reaction: - if replied_reaction.kind is ReactionKind.PROPOSE: + if replied_reaction.kind is ReactionKind.PROPOSE.value: if replied_reaction.range: old_body = shout.body start, end = replied_reaction.range.split(":") @@ -251,7 +255,7 @@ async def update_reaction(_, info, rid, reaction): user_id = info.context["user_id"] with local_session() as session: q = select(Reaction).filter(Reaction.id == rid) - q = add_reaction_stat_columns(q) + q, aliased_reaction = add_reaction_stat_columns(q) q = q.group_by(Reaction.id) [r, reacted_stat, commented_stat, rating_stat] = session.execute(q).unique().one() @@ -301,7 +305,7 @@ async def delete_reaction(_, info, rid): if r.created_by is author.id: return {"error": "access denied"} - if r.kind in [ReactionKind.LIKE, ReactionKind.DISLIKE]: + if r.kind in [ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]: session.delete(r) else: rdict = r.dict() @@ -318,8 +322,6 @@ async def delete_reaction(_, info, rid): def apply_reaction_filters(by, q): - # filter - # if by.get("shout"): q = q.filter(Shout.slug == by["shout"]) @@ -330,7 +332,6 @@ def apply_reaction_filters(by, q): q = q.filter(Author.id == by["created_by"]) if by.get("topic"): - # TODO: check q = q.filter(Shout.topics.contains(by["topic"])) if by.get("comment"): @@ -373,9 +374,12 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): .join(Shout, Reaction.shout == Shout.id) ) + # calculate counters q, aliased_reaction = add_reaction_stat_columns(q) + # filter q = apply_reaction_filters(by, q) + q = q.where(Reaction.deleted_at.is_(None)) # group by q = q.group_by(Reaction.id, Author.id, Shout.id, aliased_reaction.created_at) @@ -385,8 +389,9 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): order_field = by.get("sort", "").replace("-", "") or "created_at" q = q.order_by(order_way(order_field)) - q = q.where(Reaction.deleted_at.is_(None)) + # pagination q = q.limit(limit).offset(offset) + reactions = [] with local_session() as session: result_rows = session.execute(q) @@ -424,6 +429,10 @@ def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[Shout]: .join(Reaction) .filter(Reaction.created_by == follower_id) .filter(Reaction.created_at > author.last_seen) + .options( + joinedload(Reaction.created_by), + joinedload(Reaction.shout) + ) .limit(limit) .offset(offset) .all() @@ -431,7 +440,6 @@ def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[Shout]: return shouts -# @query.field("followedReactions") @login_required @query.field("load_shouts_followed") async def load_shouts_followed(_, info, limit=50, offset=0) -> List[Shout]: diff --git a/resolvers/reader.py b/resolvers/reader.py index ce76c4e1..cebc85e4 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -18,23 +18,23 @@ def add_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.kind == ReactionKind.COMMENT, 1), else_=0)).label("commented_stat"), + func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT.value, 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), + (aliased_reaction.kind == ReactionKind.AGREE.value, 1), + (aliased_reaction.kind == ReactionKind.DISAGREE.value, -1), + (aliased_reaction.kind == ReactionKind.PROOF.value, 1), + (aliased_reaction.kind == ReactionKind.DISPROOF.value, -1), + (aliased_reaction.kind == ReactionKind.ACCEPT.value, 1), + (aliased_reaction.kind == ReactionKind.REJECT.value, -1), + (aliased_reaction.kind == ReactionKind.LIKE.value, 1), + (aliased_reaction.kind == ReactionKind.DISLIKE.value, -1), else_=0, ) ).label("rating_stat"), func.max( case( - (aliased_reaction.kind != ReactionKind.COMMENT, None), + (aliased_reaction.kind != ReactionKind.COMMENT.value, 0), else_=aliased_reaction.created_at, ) ).label("last_comment"), @@ -50,7 +50,7 @@ def apply_filters(q, filters, author_id=None): by_published = filters.get("published") if by_published: - q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC) + q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC.value) by_layouts = filters.get("layouts") if by_layouts: q = q.filter(Shout.layout.in_(by_layouts))