From 435d1e4505e777db457bbebf53af387cb3d80978 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 3 Nov 2023 13:10:22 +0300 Subject: [PATCH] new-version-0-2-13 --- CHANGELOG.txt | 11 ++ orm/__init__.py | 3 +- orm/author.py | 30 ++--- orm/collection.py | 10 +- orm/community.py | 17 ++- orm/reaction.py | 19 ++- orm/shout.py | 37 ++---- orm/topic.py | 6 +- pyproject.toml | 2 +- resolvers/author.py | 25 ++-- resolvers/community.py | 2 +- resolvers/editor.py | 85 +++--------- resolvers/reaction.py | 66 +++++----- resolvers/reader.py | 59 ++++----- resolvers/topic.py | 5 +- schemas/core.graphql | 289 ++++++++++++++++++++--------------------- services/db.py | 53 ++++++-- services/following.py | 2 +- services/notify.py | 2 +- services/viewed.py | 99 ++++++-------- test/test.json | 6 +- 21 files changed, 392 insertions(+), 436 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c87294c5..aa1751c8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,14 @@ +[0.2.13] +- services: db context manager +- services: ViewedStorage fixes +- services: views are not stored in core db anymore +- schema: snake case in model fields names +- schema: no DateTime scalar +- resolvers: get_my_feed comments filter reactions body.is_not("") +- resolvers: get_my_feed query fix +- resolvers: LoadReactionsBy.days -> LoadReactionsBy.time_ago +- resolvers: LoadShoutsBy.days -> LoadShoutsBy.time_ago + [0.2.12] - Author.userpic -> Author.pic - CommunityAuthor.role is string now diff --git a/orm/__init__.py b/orm/__init__.py index a8279110..77097285 100644 --- a/orm/__init__.py +++ b/orm/__init__.py @@ -2,8 +2,9 @@ from services.db import Base, engine from orm.shout import Shout from orm.community import Community + def init_tables(): Base.metadata.create_all(engine) - Community.init_table() Shout.init_table() + Community.init_table() print("[orm] tables initialized") diff --git a/orm/author.py b/orm/author.py index 323f520b..c98e7192 100644 --- a/orm/author.py +++ b/orm/author.py @@ -1,6 +1,6 @@ -from datetime import datetime +import time from sqlalchemy import JSON as JSONType -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from services.db import Base @@ -13,10 +13,6 @@ class AuthorRating(Base): author = Column(ForeignKey("author.id"), primary_key=True, index=True) value = Column(Integer) - @staticmethod - def init_table(): - pass - class AuthorFollower(Base): __tablename__ = "author_follower" @@ -24,23 +20,25 @@ class AuthorFollower(Base): id = None # type: ignore follower = Column(ForeignKey("author.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True) - createdAt = Column(DateTime, nullable=False, default=datetime.now) + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) auto = Column(Boolean, nullable=False, default=False) class Author(Base): __tablename__ = "author" - user = Column(String, nullable=False) # unbounded link with authorizer's User type - bio = Column(String, nullable=True, comment="Bio") # status description - about = Column(String, nullable=True, comment="About") # long and formatted - pic = Column(String, nullable=True, comment="Userpic") + user = Column(String, unique=True) # unbounded link with authorizer's User type + name = Column(String, nullable=True, comment="Display name") slug = Column(String, unique=True, comment="Author's slug") - - createdAt = Column(DateTime, nullable=False, default=datetime.now) - lastSeen = Column(DateTime, nullable=False, default=datetime.now) # Td se 0e - deletedAt = Column(DateTime, nullable=True, comment="Deleted at") - + bio = Column(String, nullable=True, comment="Bio") # status description + about = Column(String, nullable=True, comment="About") # long and formatted + pic = Column(String, nullable=True, comment="Picture") links = Column(JSONType, nullable=True, comment="Links") + ratings = relationship(AuthorRating, foreign_keys=AuthorRating.author) + + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) + last_seen = Column(Integer, nullable=False, default=lambda: int(time.time())) + updated_at = Column(Integer, nullable=False, default=lambda: int(time.time())) + deleted_at = Column(Integer, nullable=True, comment="Deleted at") diff --git a/orm/collection.py b/orm/collection.py index 9bd14742..8c90f9b0 100644 --- a/orm/collection.py +++ b/orm/collection.py @@ -1,5 +1,5 @@ -from datetime import datetime -from sqlalchemy import Column, DateTime, ForeignKey, String +import time +from sqlalchemy import Column, Integer, ForeignKey, String from services.db import Base @@ -18,6 +18,6 @@ class Collection(Base): title = Column(String, nullable=False, comment="Title") body = Column(String, nullable=True, comment="Body") pic = Column(String, nullable=True, comment="Picture") - createdAt = Column(DateTime, default=datetime.now, comment="Created At") - createdBy = Column(ForeignKey("author.id"), comment="Created By") - publishedAt = Column(DateTime, default=datetime.now, comment="Published At") + created_at = Column(Integer, default=lambda: int(time.time())) + created_by = Column(ForeignKey("author.id"), comment="Created By") + published_at = Column(Integer, default=lambda: int(time.time())) diff --git a/orm/community.py b/orm/community.py index ee54979c..ce400f13 100644 --- a/orm/community.py +++ b/orm/community.py @@ -1,5 +1,5 @@ -from datetime import datetime -from sqlalchemy import Column, String, ForeignKey, DateTime +import time +from sqlalchemy import Column, String, ForeignKey, Integer from sqlalchemy.orm import relationship from services.db import Base, local_session @@ -12,7 +12,7 @@ class CommunityAuthor(Base): id = None # type: ignore follower = Column(ForeignKey("author.id"), primary_key=True) community = Column(ForeignKey("community.id"), primary_key=True) - joinedAt = Column(DateTime, nullable=False, default=datetime.now) + joined_at = Column(Integer, nullable=False, default=lambda: int(time.time())) role = Column(String, nullable=False) @@ -23,17 +23,16 @@ class Community(Base): slug = Column(String, nullable=False, unique=True) desc = Column(String, nullable=False, default="") pic = Column(String, nullable=False, default="") - createdAt = Column(DateTime, nullable=False, default=datetime.now) + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) - authors = relationship(lambda: Author, secondary=CommunityAuthor.__tablename__, nullable=True) + authors = relationship(lambda: Author, secondary=CommunityAuthor.__tablename__) @staticmethod def init_table(): with local_session() as session: - d = (session.query(Community).filter(Community.slug == "discours").first()) + d = session.query(Community).filter(Community.slug == "discours").first() if not d: d = Community.create(name="Дискурс", slug="discours") - session.add(d) - session.commit() + print("[orm] created community %s" % d.slug) Community.default_community = d - print('[orm] default community id: %s' % d.id) + print("[orm] default community is %s" % d.slug) diff --git a/orm/reaction.py b/orm/reaction.py index 3e300922..79b7760b 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -1,7 +1,7 @@ -from datetime import datetime from enum import Enum as Enumeration -from sqlalchemy import Column, DateTime, Enum, ForeignKey, String +from sqlalchemy import Column, Integer, Enum, ForeignKey, String from services.db import Base +import time class ReactionKind(Enumeration): @@ -26,13 +26,12 @@ class Reaction(Base): __tablename__ = "reaction" body = Column(String, nullable=True, comment="Reaction Body") - createdAt = Column(DateTime, nullable=False, default=datetime.now) - createdBy = Column(ForeignKey("author.id"), nullable=False, index=True) - updatedAt = Column(DateTime, nullable=True, comment="Updated at") - updatedBy = Column(ForeignKey("author.id"), nullable=True, index=True) - deletedAt = Column(DateTime, nullable=True, comment="Deleted at") - deletedBy = Column(ForeignKey("author.id"), nullable=True, index=True) + 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) - replyTo = Column(ForeignKey("reaction.id"), nullable=True) - range = Column(String, nullable=True, comment=":") + reply_to = Column(ForeignKey("reaction.id"), nullable=True) + quote = Column(String, nullable=True, comment="a quoted fragment") kind = Column(Enum(ReactionKind), nullable=False) diff --git a/orm/shout.py b/orm/shout.py index 511f3ca6..0ea91bb0 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -1,10 +1,9 @@ -from datetime import datetime +import time from enum import Enum as Enumeration from sqlalchemy import ( Enum, Boolean, Column, - DateTime, ForeignKey, Integer, String, @@ -33,10 +32,8 @@ class ShoutReactionsFollower(Base): follower = Column(ForeignKey("author.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) auto = Column(Boolean, nullable=False, default=False) - createdAt = Column( - DateTime, nullable=False, default=datetime.now, comment="Created at" - ) - deletedAt = Column(DateTime, nullable=True) + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) + deleted_at = Column(Integer, nullable=True) class ShoutAuthor(Base): @@ -65,13 +62,13 @@ class ShoutVisibility(Enumeration): class Shout(Base): __tablename__ = "shout" - createdAt = Column(DateTime, nullable=False, default=datetime.now) - updatedAt = Column(DateTime, nullable=True) - publishedAt = Column(DateTime, nullable=True) - deletedAt = Column(DateTime, nullable=True) + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) + updated_at = Column(Integer, nullable=True) + published_at = Column(Integer, nullable=True) + deleted_at = Column(Integer, nullable=True) - createdBy = Column(ForeignKey("author.id"), comment="Created By") - deletedBy = Column(ForeignKey("author.id"), nullable=True) + created_by = Column(ForeignKey("author.id"), comment="Created By") + deleted_by = Column(ForeignKey("author.id"), nullable=True) body = Column(String, nullable=False, comment="Body") slug = Column(String, unique=True) @@ -85,21 +82,17 @@ class Shout(Base): authors = relationship(lambda: Author, secondary=ShoutAuthor.__tablename__) topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) - communities = relationship( - lambda: Community, secondary=ShoutCommunity.__tablename__ - ) + communities = relationship(lambda: Community, secondary=ShoutCommunity.__tablename__) reactions = relationship(lambda: Reaction) - viewsOld = Column(Integer, default=0) - viewsAckee = Column(Integer, default=0) - views = column_property(viewsOld + viewsAckee) + views_old = Column(Integer, default=0) + views_ackee = Column(Integer, default=0) + views = column_property(views_old + views_ackee) visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS) - # TODO: these field should be used or modified lang = Column(String, nullable=False, default="ru", comment="Language") - mainTopic = Column(ForeignKey("topic.slug"), nullable=True) - versionOf = Column(ForeignKey("shout.id"), nullable=True) + version_of = Column(ForeignKey("shout.id"), nullable=True) oid = Column(String, nullable=True) @staticmethod @@ -114,5 +107,3 @@ class Shout(Base): "lang": "ru", } s = Shout.create(**entry) - session.add(s) - session.commit() diff --git a/orm/topic.py b/orm/topic.py index af4f56bd..1c8d48ef 100644 --- a/orm/topic.py +++ b/orm/topic.py @@ -1,5 +1,5 @@ -from datetime import datetime -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String +import time +from sqlalchemy import Boolean, Column, Integer, ForeignKey, String from services.db import Base @@ -9,7 +9,7 @@ class TopicFollower(Base): id = None # type: ignore follower = Column(ForeignKey("author.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) - createdAt = Column(DateTime, nullable=False, default=datetime.now) + created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) auto = Column(Boolean, nullable=False, default=False) diff --git a/pyproject.toml b/pyproject.toml index f8440349..dce730a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "discoursio-core" -version = "0.2.12" +version = "0.2.13" description = "core module for discours.io" authors = ["discoursio devteam"] license = "MIT" diff --git a/resolvers/author.py b/resolvers/author.py index ece55200..86161db0 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -1,10 +1,11 @@ +import time from typing import List -from datetime import datetime, timedelta, timezone from sqlalchemy import and_, func, distinct, select, literal from sqlalchemy.orm import aliased from services.auth import login_required from services.db import local_session +from services.unread import get_total_unread_counter from services.schema import mutation, query from orm.shout import ShoutAuthor, ShoutTopic from orm.topic import Topic @@ -41,7 +42,7 @@ def add_author_stat_columns(q): # ) q = q.add_columns(literal(0).label("commented_stat")) - # q = q.outerjoin(Reaction, and_(Reaction.createdBy == Author.id, Reaction.body.is_not(None))).add_columns( + # q = q.outerjoin(Reaction, and_(Reaction.created_by == Author.id, Reaction.body.is_not(None))).add_columns( # func.count(distinct(Reaction.id)).label('commented_stat') # ) @@ -81,7 +82,7 @@ def get_authors_from_query(q): async def author_followings(author_id: int): return { - # "unread": await get_total_unread_counter(author_id), # unread inbox messages counter + "unread": await get_total_unread_counter(author_id), # unread inbox messages counter "topics": [ t.slug for t in await followed_topics(author_id) ], # followed topics slugs @@ -168,14 +169,15 @@ async def load_authors_by(_, _info, by, limit, offset): .join(Topic) .where(Topic.slug == by["topic"]) ) - if by.get("lastSeen"): # in days - days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"]) - q = q.filter(Author.lastSeen > days_before) - elif by.get("createdAt"): # in days - days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"]) - q = q.filter(Author.createdAt > days_before) + + if by.get("last_seen"): # in unixtime + before = int(time.time()) - by["last_seen"] + q = q.filter(Author.last_seen > before) + elif by.get("created_at"): # in unixtime + before = int(time.time()) - by["created_at"] + q = q.filter(Author.created_at > before) - q = q.order_by(by.get("order", Author.createdAt)).limit(limit).offset(offset) + q = q.order_by(by.get("order", Author.created_at)).limit(limit).offset(offset) return get_authors_from_query(q) @@ -226,7 +228,8 @@ async def rate_author(_, info, rated_user_id, value): session.query(AuthorRating) .filter( and_( - AuthorRating.rater == author_id, AuthorRating.user == rated_user_id + AuthorRating.rater == author_id, + AuthorRating.user == rated_user_id ) ) .first() diff --git a/resolvers/community.py b/resolvers/community.py index bfa12634..bb3a1b04 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -28,7 +28,7 @@ def add_community_stat_columns(q): # ) q = q.add_columns(literal(0).label("commented_stat")) - # q = q.outerjoin(Reaction, and_(Reaction.createdBy == Author.id, Reaction.body.is_not(None))).add_columns( + # q = q.outerjoin(Reaction, and_(Reaction.created_by == Author.id, Reaction.body.is_not(None))).add_columns( # func.count(distinct(Reaction.id)).label('commented_stat') # ) diff --git a/resolvers/editor.py b/resolvers/editor.py index 25867831..b3e34114 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -1,40 +1,32 @@ -from datetime import datetime, timezone - +import time # For Unix timestamps from sqlalchemy import and_, select from sqlalchemy.orm import joinedload - from services.auth import login_required from services.db import local_session from services.schema import mutation, query -from orm.shout import Shout, ShoutAuthor, ShoutTopic +from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility from orm.topic import Topic from reaction import reactions_follow, reactions_unfollow from services.notify import notify_shout - @query.field("loadDrafts") async def get_drafts(_, info): author = info.context["request"].author - q = ( select(Shout) .options( joinedload(Shout.authors), joinedload(Shout.topics), ) - .where(and_(Shout.deletedAt.is_(None), Shout.createdBy == author.id)) + .where(and_(Shout.deleted_at.is_(None), Shout.created_by == author.id)) ) - q = q.group_by(Shout.id) - shouts = [] with local_session() as session: for [shout] in session.execute(q).unique(): shouts.append(shout) - return shouts - @mutation.field("createShout") @login_required async def create_shout(_, info, inp): @@ -43,7 +35,8 @@ async def create_shout(_, info, inp): topics = ( session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all() ) - + # Replace datetime with Unix timestamp + current_time = int(time.time()) new_shout = Shout.create( **{ "title": inp.get("title"), @@ -54,43 +47,33 @@ async def create_shout(_, info, inp): "layout": inp.get("layout"), "authors": inp.get("authors", []), "slug": inp.get("slug"), - "mainTopic": inp.get("mainTopic"), - "visibility": "authors", - "createdBy": author_id, + "topics": inp.get("topics"), + "visibility": ShoutVisibility.AUTHORS, + "created_id": author_id, + "created_at": current_time, # Set created_at as Unix timestamp } ) - for topic in topics: t = ShoutTopic.create(topic=topic.id, shout=new_shout.id) session.add(t) - # NOTE: shout made by one first author sa = ShoutAuthor.create(shout=new_shout.id, author=author_id) session.add(sa) - session.add(new_shout) - reactions_follow(author_id, new_shout.id, True) - session.commit() - - # TODO - # GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug) - + # TODO: GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug) if new_shout.slug is None: new_shout.slug = f"draft-{new_shout.id}" session.commit() else: notify_shout(new_shout.dict(), "create") - return {"shout": new_shout} - @mutation.field("updateShout") @login_required async def update_shout(_, info, shout_id, shout_input=None, publish=False): author_id = info.context["author_id"] - with local_session() as session: shout = ( session.query(Shout) @@ -101,39 +84,30 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False): .filter(Shout.id == shout_id) .first() ) - if not shout: return {"error": "shout not found"} - - if shout.createdBy != author_id: + if shout.created_by != author_id: return {"error": "access denied"} - updated = False - if shout_input is not None: topics_input = shout_input["topics"] del shout_input["topics"] - new_topics_to_link = [] new_topics = [ topic_input for topic_input in topics_input if topic_input["id"] < 0 ] - for new_topic in new_topics: del new_topic["id"] created_new_topic = Topic.create(**new_topic) session.add(created_new_topic) new_topics_to_link.append(created_new_topic) - if len(new_topics) > 0: session.commit() - for new_topic_to_link in new_topics_to_link: created_unlinked_topic = ShoutTopic.create( shout=shout.id, topic=new_topic_to_link.id ) session.add(created_unlinked_topic) - existing_topics_input = [ topic_input for topic_input in topics_input @@ -145,80 +119,61 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False): if existing_topic_input["id"] not in [topic.id for topic in shout.topics] ] - for existing_topic_to_link_id in existing_topic_to_link_ids: created_unlinked_topic = ShoutTopic.create( shout=shout.id, topic=existing_topic_to_link_id ) session.add(created_unlinked_topic) - topic_to_unlink_ids = [ topic.id for topic in shout.topics if topic.id not in [topic_input["id"] for topic_input in existing_topics_input] ] - shout_topics_to_remove = session.query(ShoutTopic).filter( and_( ShoutTopic.shout == shout.id, ShoutTopic.topic.in_(topic_to_unlink_ids), ) ) - for shout_topic_to_remove in shout_topics_to_remove: session.delete(shout_topic_to_remove) - shout_input["mainTopic"] = shout_input["mainTopic"]["slug"] - if shout_input["mainTopic"] == "": del shout_input["mainTopic"] - + # Replace datetime with Unix timestamp + current_time = int(time.time()) + shout_input["updated_at"] = current_time # Set updated_at as Unix timestamp shout.update(shout_input) updated = True - # TODO: use visibility setting - if publish and shout.visibility == "authors": shout.visibility = "community" - shout.publishedAt = datetime.now(tz=timezone.utc) + shout.published_at = current_time # Set published_at as Unix timestamp updated = True - # notify on publish notify_shout(shout.dict()) - if updated: - shout.updatedAt = datetime.now(tz=timezone.utc) - - session.commit() - + session.commit() # GitTask(inp, user.username, user.email, "update shout %s" % slug) - notify_shout(shout.dict(), "update") - return {"shout": shout} - @mutation.field("deleteShout") @login_required async def delete_shout(_, info, shout_id): author_id = info.context["author_id"] with local_session() as session: shout = session.query(Shout).filter(Shout.id == shout_id).first() - if not shout: return {"error": "invalid shout id"} - - if author_id != shout.createdBy: + if author_id != shout.created_by: return {"error": "access denied"} - for author_id in shout.authors: reactions_unfollow(author_id, shout_id) - - shout.deletedAt = datetime.now(tz=timezone.utc) + # Replace datetime with Unix timestamp + current_time = int(time.time()) + shout.deleted_at = current_time # Set deleted_at as Unix timestamp session.commit() - - notify_shout(shout.dict(), "delete") - return {} diff --git a/resolvers/reaction.py b/resolvers/reaction.py index a9f0964f..5fe727a7 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +import time from sqlalchemy import and_, asc, desc, select, text, func, case from sqlalchemy.orm import aliased from services.notify import notify_reaction @@ -15,7 +15,7 @@ def add_reaction_stat_columns(q): aliased_reaction = aliased(Reaction) q = q.outerjoin( - aliased_reaction, Reaction.id == aliased_reaction.replyTo + aliased_reaction, Reaction.id == aliased_reaction.reply_to ).add_columns( func.sum(aliased_reaction.id).label("reacted_stat"), func.sum(case((aliased_reaction.body.is_not(None), 1), else_=0)).label( @@ -96,7 +96,7 @@ def is_published_author(session, author_id): return ( session.query(Shout) .where(Shout.authors.contains(author_id)) - .filter(and_(Shout.publishedAt.is_not(None), Shout.deletedAt.is_(None))) + .filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))) .count() > 0 ) @@ -104,7 +104,7 @@ 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.replyTo and reaction.kind in [ + if not reaction.reply_to and reaction.kind in [ ReactionKind.ACCEPT, ReactionKind.LIKE, ReactionKind.PROOF, @@ -118,7 +118,7 @@ def check_to_publish(session, author_id, reaction): author_id, ] for ar in approvers_reactions: - a = ar.createdBy + a = ar.created_by if is_published_author(session, a): approvers.append(a) if len(approvers) > 4: @@ -128,7 +128,7 @@ 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.replyTo and reaction.kind in [ + if not reaction.reply_to and reaction.kind in [ ReactionKind.REJECT, ReactionKind.DISLIKE, ReactionKind.DISPROOF, @@ -152,7 +152,7 @@ def check_to_hide(session, reaction): def set_published(session, shout_id): s = session.query(Shout).where(Shout.id == shout_id).first() - s.publishedAt = datetime.now(tz=timezone.utc) + s.published_at = int(time.time()) s.visibility = text("public") session.add(s) session.commit() @@ -170,7 +170,7 @@ def set_hidden(session, shout_id): async def create_reaction(_, info, reaction): author_id = info.context["author_id"] with local_session() as session: - reaction["createdBy"] = author_id + reaction["created_by"] = author_id shout = session.query(Shout).where(Shout.id == reaction["shout"]).one() if reaction["kind"] in [ReactionKind.DISLIKE.name, ReactionKind.LIKE.name]: @@ -179,9 +179,9 @@ async def create_reaction(_, info, reaction): .where( and_( Reaction.shout == reaction["shout"], - Reaction.createdBy == author_id, + Reaction.created_by == author_id, Reaction.kind == reaction["kind"], - Reaction.replyTo == reaction.get("replyTo"), + Reaction.reply_to == reaction.get("reply_to"), ) ) .first() @@ -200,9 +200,9 @@ async def create_reaction(_, info, reaction): .where( and_( Reaction.shout == reaction["shout"], - Reaction.createdBy == author_id, + Reaction.created_by == author_id, Reaction.kind == opposite_reaction_kind, - Reaction.replyTo == reaction.get("replyTo"), + Reaction.reply_to == reaction.get("reply_to"), ) ) .first() @@ -215,12 +215,12 @@ async def create_reaction(_, info, reaction): # Proposal accepting logix if ( - r.replyTo is not None + r.reply_to is not None and r.kind == ReactionKind.ACCEPT and author_id in shout.dict()["authors"] ): replied_reaction = ( - session.query(Reaction).where(Reaction.id == r.replyTo).first() + session.query(Reaction).where(Reaction.id == r.reply_to).first() ) if replied_reaction and replied_reaction.kind == ReactionKind.PROPOSE: if replied_reaction.range: @@ -237,7 +237,7 @@ async def create_reaction(_, info, reaction): rdict = r.dict() rdict["shout"] = shout.dict() author = session.query(Author).where(Author.id == author_id).first() - rdict["createdBy"] = author.dict() + rdict["created_by"] = author.dict() # self-regulation mechanics @@ -274,11 +274,11 @@ async def update_reaction(_, info, rid, reaction={}): if not r: return {"error": "invalid reaction id"} - if r.createdBy != author_id: + if r.created_by != author_id: return {"error": "access denied"} r.body = reaction["body"] - r.updatedAt = datetime.now(tz=timezone.utc) + r.updated_at = int(time.time()) if r.kind != reaction["kind"]: # NOTE: change mind detection can be here pass @@ -304,13 +304,13 @@ async def delete_reaction(_, info, rid): r = session.query(Reaction).filter(Reaction.id == rid).first() if not r: return {"error": "invalid reaction id"} - if r.createdBy != author_id: + if r.created_by != author_id: return {"error": "access denied"} if r.kind in [ReactionKind.LIKE, ReactionKind.DISLIKE]: session.delete(r) else: - r.deletedAt = datetime.now(tz=timezone.utc) + r.deleted_at = int(time.time()) session.commit() notify_reaction(r.dict(), "delete") @@ -325,11 +325,11 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): :param by: { :shout - filter by slug :shouts - filer by shout slug list - :createdBy - to filter by author + :created_by - to filter by author :topic - to filter by topic :search - to search by reactions' body :comment - true if body.length > 0 - :days - a number of days ago + :time_ago - amount of time ago :sort - a fieldname to sort desc by default } :param limit: int amount of shouts @@ -339,7 +339,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): q = ( select(Reaction, Author, Shout) - .join(Author, Reaction.createdBy == Author.id) + .join(Author, Reaction.created_by == Author.id) .join(Shout, Reaction.shout == Shout.id) ) @@ -348,8 +348,8 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): elif by.get("shouts"): q = q.filter(Shout.slug.in_(by["shouts"])) - if by.get("createdBy"): - q = q.filter(Author.id == by.get("createdBy")) + if by.get("created_by"): + q = q.filter(Author.id == by.get("created_by")) if by.get("topic"): # TODO: check @@ -361,15 +361,15 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): if len(by.get("search", "")) > 2: q = q.filter(Reaction.body.ilike(f'%{by["body"]}%')) - if by.get("days"): - after = datetime.now(tz=timezone.utc) - timedelta(days=int(by["days"]) or 30) - q = q.filter(Reaction.createdAt > after) # FIXME: use comparing operator? + if by.get("time_ago"): + after = int(time.time()) - int(by.get("time_ago", 0)) + q = q.filter(Reaction.created_at > after) order_way = asc if by.get("sort", "").startswith("-") else desc - order_field = by.get("sort", "").replace("-", "") or Reaction.createdAt + order_field = by.get("sort", "").replace("-", "") or Reaction.created_at q = q.group_by(Reaction.id, Author.id, Shout.id).order_by(order_way(order_field)) q = add_reaction_stat_columns(q) - q = q.where(Reaction.deletedAt.is_(None)) + q = q.where(Reaction.deleted_at.is_(None)) q = q.limit(limit).offset(offset) reactions = [] session = info.context["session"] @@ -381,7 +381,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): commented_stat, rating_stat, ] in session.execute(q): - reaction.createdBy = author + reaction.created_by = author reaction.shout = shout reaction.stat = { "rating": rating_stat, @@ -393,7 +393,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): # ? 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.created_at) return reactions @@ -407,8 +407,8 @@ async def followed_reactions(_, info): author = session.query(Author).where(Author.id == author_id).first() reactions = ( session.query(Reaction.shout) - .where(Reaction.createdBy == author.id) - .filter(Reaction.createdAt > author.lastSeen) + .where(Reaction.created_by == author.id) + .filter(Reaction.created_at > author.last_seen) .all() ) diff --git a/resolvers/reader.py b/resolvers/reader.py index 4b23c251..5ea93268 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -1,5 +1,4 @@ -from datetime import datetime, timedelta, timezone - +import time from aiohttp.web_exceptions import HTTPException from sqlalchemy.orm import joinedload, aliased from sqlalchemy.sql.expression import desc, asc, select, func, case, and_, nulls_last @@ -10,11 +9,11 @@ from orm.topic import TopicFollower from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.author import AuthorFollower +from servies.viewed import ViewedStorage 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( @@ -23,7 +22,7 @@ def add_stat_columns(q): func.sum( case( # do not count comments' reactions - (aliased_reaction.replyTo.is_not(None), 0), + (aliased_reaction.body.is_not(""), 0), (aliased_reaction.kind == ReactionKind.AGREE, 1), (aliased_reaction.kind == ReactionKind.DISAGREE, -1), (aliased_reaction.kind == ReactionKind.PROOF, 1), @@ -38,7 +37,7 @@ def add_stat_columns(q): func.max( case( (aliased_reaction.kind != ReactionKind.COMMENT, None), - else_=aliased_reaction.createdAt, + else_=aliased_reaction.created_at, ) ).label("last_comment"), ) @@ -48,7 +47,7 @@ def add_stat_columns(q): def apply_filters(q, filters, author_id=None): if filters.get("reacted") and author_id: - q.join(Reaction, Reaction.createdBy == author_id) + q.join(Reaction, Reaction.created_by == author_id) v = filters.get("visibility") if v == "public": @@ -66,11 +65,9 @@ def apply_filters(q, filters, author_id=None): q = q.filter(Shout.title.ilike(f'%{filters.get("title")}%')) if filters.get("body"): q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s')) - if filters.get("days"): - before = datetime.now(tz=timezone.utc) - timedelta( - days=int(filters.get("days")) or 30 - ) - q = q.filter(Shout.createdAt > before) + if filters.get("time_ago"): + before = int(time.time()) - int(filters.get("time_ago")) + q = q.filter(Shout.created_at > before) return q @@ -89,11 +86,12 @@ async def load_shout(_, _info, slug=None, shout_id=None): if shout_id is not None: q = q.filter(Shout.id == shout_id) - q = q.filter(Shout.deletedAt.is_(None)).group_by(Shout.id) + q = q.filter(Shout.deleted_at.is_(None)).group_by(Shout.id) try: [ shout, + viewed_stat, reacted_stat, commented_stat, rating_stat, @@ -101,7 +99,7 @@ async def load_shout(_, _info, slug=None, shout_id=None): ] = session.execute(q).first() shout.stat = { - "viewed": shout.views, + "viewed": viewed_stat, "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat, @@ -134,7 +132,7 @@ async def load_shouts_by(_, info, options): } offset: 0 limit: 50 - order_by: 'createdAt' | 'commented' | 'reacted' | 'rating' + order_by: 'created_at' | 'commented' | 'reacted' | 'rating' order_by_desc: true } @@ -147,7 +145,7 @@ async def load_shouts_by(_, info, options): joinedload(Shout.authors), joinedload(Shout.topics), ) - .where(Shout.deletedAt.is_(None)) + .where(Shout.deleted_at.is_(None)) ) q = add_stat_columns(q) @@ -155,7 +153,7 @@ async def load_shouts_by(_, info, options): author_id = info.context["author_id"] q = apply_filters(q, options.get("filters", {}), author_id) - order_by = options.get("order_by", Shout.publishedAt) + order_by = options.get("order_by", Shout.published_at) query_order_by = ( desc(order_by) if options.get("order_by_desc", True) else asc(order_by) @@ -175,6 +173,7 @@ async def load_shouts_by(_, info, options): with local_session() as session: for [ shout, + viewed_stat, reacted_stat, commented_stat, rating_stat, @@ -182,7 +181,7 @@ async def load_shouts_by(_, info, options): ] in session.execute(q).unique(): shouts.append(shout) shout.stat = { - "viewed": shout.views, + "viewed": viewed_stat, "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat, @@ -196,12 +195,17 @@ async def load_shouts_by(_, info, options): async def get_my_feed(_, info, options): author_id = info.context["author_id"] with local_session() as session: + author_followed_authors = select(AuthorFollower.author).where(AuthorFollower.follower == author_id) + author_followed_topics = select(TopicFollower.topic).where(TopicFollower.follower == author_id) + subquery = ( select(Shout.id) - .join(ShoutAuthor) - .join(AuthorFollower, AuthorFollower.follower._is(author_id)) - .join(ShoutTopic) - .join(TopicFollower, TopicFollower.follower._is(author_id)) + .where(Shout.id == ShoutAuthor.shout) + .where(Shout.id == ShoutTopic.shout) + .where( + (ShoutAuthor.author.in_(author_followed_authors)) + | (ShoutTopic.topic.in_(author_followed_topics)) + ) ) q = ( @@ -212,8 +216,8 @@ async def get_my_feed(_, info, options): ) .where( and_( - Shout.publishedAt.is_not(None), - Shout.deletedAt.is_(None), + Shout.published_at.is_not(None), + Shout.deleted_at.is_(None), Shout.id.in_(subquery), ) ) @@ -222,7 +226,7 @@ async def get_my_feed(_, info, options): q = add_stat_columns(q) q = apply_filters(q, options.get("filters", {}), author_id) - order_by = options.get("order_by", Shout.publishedAt) + order_by = options.get("order_by", Shout.published_at) query_order_by = ( desc(order_by) if options.get("order_by_desc", True) else asc(order_by) @@ -238,7 +242,6 @@ async def get_my_feed(_, info, options): ) shouts = [] - shouts_map = {} for [ shout, reacted_stat, @@ -246,13 +249,11 @@ async def get_my_feed(_, info, options): rating_stat, _last_comment, ] in session.execute(q).unique(): - shouts.append(shout) shout.stat = { - "viewed": shout.views, + "viewed": ViewedStorage.get_shout(shout.slug), "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat, } - shouts_map[shout.id] = shout - # FIXME: shouts_map does not go anywhere? + shouts.append(shout) return shouts diff --git a/resolvers/topic.py b/resolvers/topic.py index bfd97d45..7b179dc5 100644 --- a/resolvers/topic.py +++ b/resolvers/topic.py @@ -136,10 +136,7 @@ def topic_follow(follower_id, slug): try: with local_session() as session: topic = session.query(Topic).where(Topic.slug == slug).one() - - following = TopicFollower.create(topic=topic.id, follower=follower_id) - session.add(following) - session.commit() + _following = TopicFollower.create(topic=topic.id, follower=follower_id) return True except Exception: return False diff --git a/schemas/core.graphql b/schemas/core.graphql index 6e5249d4..4cbc4d31 100644 --- a/schemas/core.graphql +++ b/schemas/core.graphql @@ -1,9 +1,3 @@ -# Скалярные типы данных -scalar DateTime - - -# Перечисления - enum ShoutVisibility { AUTHORS COMMUNITY @@ -42,10 +36,145 @@ enum FollowingEntity { REACTIONS } +# Типы + + +type AuthorFollowings { + unread: Int + topics: [String] + authors: [String] + reactions: [Int] + communities: [String] +} + +type AuthorStat { + followings: Int + followers: Int + rating: Int + commented: Int + shouts: Int +} + +type Author { + id: Int! + user: String! # user.id + slug: String! # user.nickname + name: String # user.preferred_username + pic: String + bio: String + about: String + links: [String] + created_at: Int + last_seen: Int + updated_at: Int + deleted_at: Int + # ratings + stat: AuthorStat # synthetic +} + +type ReactionUpdating { + error: String + status: ReactionStatus + reaction: Reaction +} + +type Rating { + rater: String! + value: Int! +} + +type Reaction { + id: Int! + shout: Shout! + created_at: Int! + created_by: Author! + updated_at: Int + deleted_at: Int + deleted_by: Author + range: String + kind: ReactionKind! + body: String + reply_to: Int + stat: Stat + old_id: String + old_thread: String +} + +type Shout { + id: Int! + slug: String! + body: String! + lead: String + description: String + created_at: Int! + topics: [Topic] + authors: [Author] + communities: [Community] + # mainTopic: String + title: String + subtitle: String + lang: String + community: String + cover: String + layout: String + version_of: String + visibility: ShoutVisibility + updated_at: Int + updated_by: Author + deleted_at: Int + deleted_by: Author + published_at: Int + media: String + stat: Stat +} + +type Stat { + viewed: Int + reacted: Int + rating: Int + commented: Int + ranking: Int +} + +type Community { + id: Int! + slug: String! + name: String! + desc: String + pic: String! + created_at: Int! + created_by: Author! +} + +type Collection { + id: Int! + slug: String! + title: String! + desc: String + amount: Int + published_at: Int + created_at: Int! + created_by: Author! +} + +type TopicStat { + shouts: Int! + followers: Int! + authors: Int! +} + +type Topic { + id: Int! + slug: String! + title: String + body: String + pic: String + stat: TopicStat + oid: String +} # Входные типы - input ShoutInput { slug: String title: String @@ -57,7 +186,6 @@ input ShoutInput { authors: [String] topics: [TopicInput] community: Int - mainTopic: TopicInput subtitle: String cover: String } @@ -65,7 +193,7 @@ input ShoutInput { input ProfileInput { slug: String name: String - userpic: String + pic: String links: [String] bio: String about: String @@ -84,12 +212,12 @@ input ReactionInput { shout: Int! range: String body: String - replyTo: Int + reply_to: Int } input AuthorsBy { - lastSeen: DateTime - createdAt: DateTime + last_seen: Int + created_at: Int slug: String name: String topic: String @@ -138,43 +266,12 @@ input ReactionBy { search: String comment: Boolean topic: String - createdBy: String + created_by: String days: Int sort: String } - -# Типы - - -type AuthorFollowings { - unread: Int - topics: [String] - authors: [String] - reactions: [Int] - communities: [String] -} - -type AuthorStat { - followings: Int - followers: Int - rating: Int - commented: Int - shouts: Int -} - -type Author { - id: Int! - user: String! - slug: String! - name: String - pic: String - bio: String - about: String - links: [String] - stat: AuthorStat - lastSeen: DateTime -} +# output type type Result { error: String @@ -191,108 +288,6 @@ type Result { communities: [Community] } -type ReactionUpdating { - error: String - status: ReactionStatus - reaction: Reaction -} - -type Rating { - rater: String! - value: Int! -} - -type Reaction { - id: Int! - shout: Shout! - createdAt: DateTime! - createdBy: Author! - updatedAt: DateTime - deletedAt: DateTime - deletedBy: Author - range: String - kind: ReactionKind! - body: String - replyTo: Int - stat: Stat - old_id: String - old_thread: String -} - -type Shout { - id: Int! - slug: String! - body: String! - lead: String - description: String - createdAt: DateTime! - topics: [Topic] - authors: [Author] - communities: [Community] - mainTopic: String - title: String - subtitle: String - lang: String - community: String - cover: String - layout: String - versionOf: String - visibility: ShoutVisibility - updatedAt: DateTime - updatedBy: Author - deletedAt: DateTime - deletedBy: Author - publishedAt: DateTime - media: String - stat: Stat -} - -type Stat { - viewed: Int - reacted: Int - rating: Int - commented: Int - ranking: Int -} - -type Community { - id: Int! - slug: String! - name: String! - desc: String - pic: String! - createdAt: DateTime! - createdBy: Author! -} - -type Collection { - id: Int! - slug: String! - title: String! - desc: String - amount: Int - publishedAt: DateTime - createdAt: DateTime! - createdBy: Author! -} - -type TopicStat { - shouts: Int! - followers: Int! - authors: Int! -} - -type Topic { - id: Int! - slug: String! - title: String - body: String - pic: String - stat: TopicStat - oid: String -} - - # Мутации type Mutation { diff --git a/services/db.py b/services/db.py index 8d2e65ad..1c16d22e 100644 --- a/services/db.py +++ b/services/db.py @@ -1,23 +1,35 @@ +from contextlib import contextmanager +import logging from typing import TypeVar, Any, Dict, Generic, Callable - from sqlalchemy import create_engine, Column, Integer from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from sqlalchemy.sql.schema import Table - from settings import DB_URL -engine = create_engine( - DB_URL, echo=False, pool_size=10, max_overflow=20 -) +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +engine = create_engine(DB_URL, echo=False, pool_size=10, max_overflow=20) +Session = sessionmaker(bind=engine, expire_on_commit=False) T = TypeVar("T") REGISTRY: Dict[str, type] = {} +@contextmanager def local_session(): - return Session(bind=engine, expire_on_commit=False) + session = Session() + try: + yield session + session.commit() + except Exception as e: + print(f"[services.db] Error session: {e}") + session.rollback() + raise + finally: + session.close() class Base(declarative_base()): @@ -36,21 +48,36 @@ class Base(declarative_base()): @classmethod def create(cls: Generic[T], **kwargs) -> Generic[T]: - instance = cls(**kwargs) - return instance.save() + try: + instance = cls(**kwargs) + return instance.save() + except Exception as e: + print(f"[services.db] Error create: {e}") + return None def save(self) -> Generic[T]: with local_session() as session: - session.add(self) - session.commit() + try: + session.add(self) + except Exception as e: + print(f"[services.db] Error save: {e}") return self def update(self, input): column_names = self.__table__.columns.keys() - for (name, value) in input.items(): + for name, value in input.items(): if name in column_names: setattr(self, name, value) + with local_session() as session: + try: + session.commit() + except Exception as e: + print(f"[services.db] Error update: {e}") def dict(self) -> Dict[str, Any]: column_names = self.__table__.columns.keys() - return {c: getattr(self, c) for c in column_names} + try: + return {c: getattr(self, c) for c in column_names} + except Exception as e: + print(f"[services.db] Error dict: {e}") + return {} diff --git a/services/following.py b/services/following.py index ec889664..8b38b5fa 100644 --- a/services/following.py +++ b/services/following.py @@ -40,7 +40,7 @@ class FollowingManager: try: async with FollowingManager.lock: for entity in FollowingManager[kind]: - if payload.shout['createdBy'] == entity.uid: + if payload.shout['created_by'] == entity.uid: entity.queue.put_nowait(payload) except Exception as e: print(Exception(e)) diff --git a/services/notify.py b/services/notify.py index 87c0caf2..1ca32827 100644 --- a/services/notify.py +++ b/services/notify.py @@ -29,7 +29,7 @@ async def notify_shout(shout, action: str = "create"): async def notify_follower(follower: dict, author_id: int, action: str = "follow"): fields = follower.keys() for k in fields: - if k not in ["id", "name", "slug", "userpic"]: + if k not in ["id", "name", "slug", "pic"]: del follower[k] channel_name = f"follower:{author_id}" data = { diff --git a/services/viewed.py b/services/viewed.py index a29a3ecd..005701f2 100644 --- a/services/viewed.py +++ b/services/viewed.py @@ -57,6 +57,7 @@ class ViewedStorage: lock = asyncio.Lock() by_shouts = {} by_topics = {} + by_reactions = {} views = None pages = None domains = None @@ -75,16 +76,16 @@ class ViewedStorage: {"Authorization": "Bearer %s" % str(token)}, schema=schema_str ) print( - "[stat] * authorized permanentely by ackee.discours.io: %s" % token + "[services.viewed] * authorized permanentely by ackee.discours.io: %s" % token ) else: - print("[stat] * please set ACKEE_TOKEN") + print("[services.viewed] * please set ACKEE_TOKEN") self.disabled = True @staticmethod async def update_pages(): """query all the pages from ackee sorted by views count""" - print("[stat] ⎧ updating ackee pages data ---") + print("[services.viewed] ⎧ updating ackee pages data ---") start = time.time() self = ViewedStorage try: @@ -100,12 +101,12 @@ class ViewedStorage: await ViewedStorage.increment(slug, shouts[slug]) except Exception: pass - print("[stat] ⎪ %d pages collected " % len(shouts.keys())) + print("[services.viewed] ⎪ %d pages collected " % len(shouts.keys())) except Exception as e: raise e end = time.time() - print("[stat] ⎪ update_pages took %fs " % (end - start)) + print("[services.viewed] ⎪ update_pages took %fs " % (end - start)) @staticmethod async def get_facts(): @@ -113,26 +114,19 @@ class ViewedStorage: async with self.lock: return self.client.execute_async(load_facts) - # unused yet @staticmethod async def get_shout(shout_slug): """getting shout views metric by slug""" self = ViewedStorage async with self.lock: - shout_views = self.by_shouts.get(shout_slug) - if not shout_views: - shout_views = 0 - with local_session() as session: - try: - shout = ( - session.query(Shout).where(Shout.slug == shout_slug).one() - ) - self.by_shouts[shout_slug] = shout.views - self.update_topics(session, shout_slug) - except Exception as e: - raise e + return self.by_shouts.get(shout_slug, 0) - return shout_views + @staticmethod + async def get_reaction(shout_slug, reaction_id): + """getting reaction views metric by slug""" + self = ViewedStorage + async with self.lock: + return self.by_reactions.get(shout_slug, {}).get(reaction_id, 0) @staticmethod async def get_topic(topic_slug): @@ -145,51 +139,36 @@ class ViewedStorage: return topic_views @staticmethod - def update_topics(session, shout_slug): + def update_topics( shout_slug): """updates topics counters by shout slug""" self = ViewedStorage - for [shout_topic, topic] in ( - session.query(ShoutTopic, Topic) - .join(Topic) - .join(Shout) - .where(Shout.slug == shout_slug) - .all() - ): - if not self.by_topics.get(topic.slug): - self.by_topics[topic.slug] = {} - self.by_topics[topic.slug][shout_slug] = self.by_shouts[shout_slug] + with local_session() as session: + for [shout_topic, topic] in ( + session.query(ShoutTopic, Topic) + .join(Topic) + .join(Shout) + .where(Shout.slug == shout_slug) + .all() + ): + if not self.by_topics.get(topic.slug): + self.by_topics[topic.slug] = {} + self.by_topics[topic.slug][shout_slug] = self.by_shouts[shout_slug] @staticmethod async def increment(shout_slug, amount=1, viewer="ackee"): """the only way to change views counter""" self = ViewedStorage async with self.lock: - # TODO optimize, currenty we execute 1 DB transaction per shout - with local_session() as session: - shout = session.query(Shout).where(Shout.slug == shout_slug).one() - if viewer == "old-discours": - # this is needed for old db migration - if shout.viewsOld == amount: - print(f"[stat] ⎪ viewsOld amount: {amount}") - else: - print( - f"[stat] ⎪ viewsOld amount changed: {shout.viewsOld} --> {amount}" - ) - shout.viewsOld = amount - else: - if shout.viewsAckee == amount: - print(f"[stat] ⎪ viewsAckee amount: {amount}") - else: - print( - f"[stat] ⎪ viewsAckee amount changed: {shout.viewsAckee} --> {amount}" - ) - shout.viewsAckee = amount + self.by_shouts[shout_slug] = self.by_shouts.get(shout_slug, 0) + amount + self.update_topics(shout_slug) - session.commit() - - # this part is currently unused - self.by_shouts[shout_slug] = self.by_shouts.get(shout_slug, 0) + amount - self.update_topics(session, shout_slug) + @staticmethod + async def increment_reaction(shout_slug, reaction_id, amount=1, viewer="ackee"): + """the only way to change views counter""" + self = ViewedStorage + async with self.lock: + self.by_reactions[shout_slug][reaction_id] = self.by_reactions[shout_slug].get(reaction_id, 0) + amount + self.update_topics(shout_slug) @staticmethod async def worker(): @@ -201,23 +180,23 @@ class ViewedStorage: while True: try: - print("[stat] - updating views...") + print("[services.viewed] - updating views...") await self.update_pages() failed = 0 except Exception: failed += 1 - print("[stat] - update failed #%d, wait 10 seconds" % failed) + print("[services.viewed] - update failed #%d, wait 10 seconds" % failed) if failed > 3: - print("[stat] - not trying to update anymore") + print("[services.viewed] - not trying to update anymore") break if failed == 0: when = datetime.now(timezone.utc) + timedelta(seconds=self.period) t = format(when.astimezone().isoformat()) print( - "[stat] ⎩ next update: %s" + "[services.viewed] ⎩ next update: %s" % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0]) ) await asyncio.sleep(self.period) else: await asyncio.sleep(10) - print("[stat] - trying to update data again") + print("[services.viewed] - trying to update data again") diff --git a/test/test.json b/test/test.json index 3ba053e7..728951d2 100644 --- a/test/test.json +++ b/test/test.json @@ -27,11 +27,11 @@ "id": 2, "name": "Дискурс", "slug": "discours", - "userpic": null + "pic": null } ], - "createdAt": "2023-09-04T10:15:08.666569", - "publishedAt": "2023-09-04T12:35:20.024954", + "created_at": "2023-09-04T10:15:08.666569", + "published_at": "2023-09-04T12:35:20.024954", "stat": { "viewed": 6, "reacted": null,