diff --git a/orm/__init__.py b/orm/__init__.py index bd2c9fb7..b83e8ad8 100644 --- a/orm/__init__.py +++ b/orm/__init__.py @@ -6,6 +6,7 @@ from orm.reaction import Reaction from orm.shout import Shout from orm.topic import Topic, TopicFollower from orm.user import User, UserRating +from orm.viewed import ViewedEntry # NOTE: keep orm module isolated @@ -21,6 +22,7 @@ __all__ = [ "Notification", "Reaction", "UserRating", + "ViewedEntry" ] @@ -33,4 +35,5 @@ def init_tables(): Role.init_table() UserRating.init_table() Shout.init_table() + ViewedEntry.init_table() print("[orm] tables initialized") diff --git a/orm/shout.py b/orm/shout.py index dfe9d749..5f3e2f4e 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -1,7 +1,7 @@ from datetime import datetime -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, JSON -from sqlalchemy.orm import column_property, relationship +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, JSON +from sqlalchemy.orm import relationship from base.orm import Base, local_session from orm.reaction import Reaction @@ -61,11 +61,6 @@ class Shout(Base): authors = relationship(lambda: User, secondary=ShoutAuthor.__tablename__) topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) - # views from the old Discours website - viewsOld = Column(Integer, default=0) - # views from Ackee tracker on the new Discours website - viewsAckee = Column(Integer, default=0) - views = column_property(viewsOld + viewsAckee) reactions = relationship(lambda: Reaction) # TODO: these field should be used or modified diff --git a/orm/viewed.py b/orm/viewed.py new file mode 100644 index 00000000..3f2441d5 --- /dev/null +++ b/orm/viewed.py @@ -0,0 +1,24 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer +from base.orm import Base, local_session + + +class ViewedEntry(Base): + __tablename__ = "viewed" + + viewer = Column(ForeignKey("user.id"), index=True, default=1) + shout = Column(ForeignKey("shout.id"), index=True, default=1) + amount = Column(Integer, default=1) + createdAt = Column( + DateTime, nullable=False, default=datetime.now, comment="Created at" + ) + + @staticmethod + def init_table(): + with local_session() as session: + entry = { + "amount": 0 + } + viewed = ViewedEntry.create(**entry) + session.add(viewed) + session.commit() diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 7b36c5b6..46e7f028 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -8,7 +8,7 @@ from auth.credentials import AuthCredentials from base.exceptions import ObjectNotExist, OperationNotAllowed from base.orm import local_session from base.resolvers import query -from orm import TopicFollower +from orm import ViewedEntry, TopicFollower from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.user import AuthorFollower @@ -101,8 +101,24 @@ async def load_shout(_, info, slug=None, shout_id=None): try: [shout, reacted_stat, commented_stat, rating_stat, last_comment] = session.execute(q).first() + viewed_stat_query = select().select_from( + Shout + ).where( + Shout.id == shout.id + ).join( + ViewedEntry + ).group_by( + Shout.id + ).add_columns( + func.sum(ViewedEntry.amount).label('viewed_stat') + ) + + # Debug tip: + # print(viewed_stat_query.compile(compile_kwargs={"literal_binds": True})) + viewed_stat = session.execute(viewed_stat_query).scalar() + shout.stat = { - "viewed": shout.views, + "viewed": viewed_stat, "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat @@ -117,6 +133,23 @@ async def load_shout(_, info, slug=None, shout_id=None): raise ObjectNotExist("Slug was not found: %s" % slug) +def add_viewed_stat(session, shouts_map): + viewed_stat_query = select( + Shout.id + ).where( + Shout.id.in_(shouts_map.keys()) + ).join( + ViewedEntry + ).group_by( + Shout.id + ).add_columns( + func.sum(ViewedEntry.amount).label('viewed_stat') + ) + + for [shout_id, viewed_stat] in session.execute(viewed_stat_query).unique(): + shouts_map[shout_id].stat['viewed'] = viewed_stat + + @query.field("loadShouts") async def load_shouts_by(_, info, options): """ @@ -166,13 +199,15 @@ async def load_shouts_by(_, info, options): for [shout, reacted_stat, commented_stat, rating_stat, last_comment] in session.execute(q).unique(): shouts.append(shout) shout.stat = { - "viewed": shout.views, + "viewed": 0, "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat } shouts_map[shout.id] = shout + add_viewed_stat(session, shouts_map) + return shouts @@ -242,11 +277,13 @@ async def get_my_feed(_, info, options): for [shout, reacted_stat, commented_stat, rating_stat, last_comment] in session.execute(q).unique(): shouts.append(shout) shout.stat = { - "viewed": shout.views, + "viewed": 0, "reacted": reacted_stat, "commented": commented_stat, "rating": rating_stat } shouts_map[shout.id] = shout + add_viewed_stat(session, shouts_map) + return shouts diff --git a/services/stat/viewed.py b/services/stat/viewed.py index 905ade43..779a6e93 100644 --- a/services/stat/viewed.py +++ b/services/stat/viewed.py @@ -11,6 +11,7 @@ from sqlalchemy import func from base.orm import local_session from orm import User, Topic from orm.shout import ShoutTopic, Shout +from orm.viewed import ViewedEntry load_facts = gql(""" query getDomains { @@ -127,7 +128,10 @@ class ViewedStorage: with local_session() as session: try: shout = session.query(Shout).where(Shout.slug == shout_slug).one() - self.by_shouts[shout_slug] = shout.views + shout_views = session.query(func.sum(ViewedEntry.amount)).where( + ViewedEntry.shout == shout.id + ).all()[0][0] + self.by_shouts[shout_slug] = shout_views self.update_topics(session, shout_slug) except Exception as e: raise e @@ -156,30 +160,37 @@ class ViewedStorage: self.by_topics[topic.slug][shout_slug] = self.by_shouts[shout_slug] @staticmethod - async def increment(shout_slug, amount=1, viewer='ackee'): + async def increment(shout_slug, amount=1, viewer='anonymous'): """ 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"viewsOld amount: {amount}") - else: - print(f"viewsOld amount changed: {shout.viewsOld} --> {amount}") - shout.viewsOld = amount + # TODO: user slug -> id + viewed = session.query( + ViewedEntry + ).join( + Shout, Shout.id == ViewedEntry.shout + ).join( + User, User.id == ViewedEntry.viewer + ).filter( + User.slug == viewer, + Shout.slug == shout_slug + ).first() + + if viewed: + viewed.amount = amount + print("amount: %d" % amount) else: - if shout.viewsAckee == amount: - print(f"viewsAckee amount: {amount}") - else: - print(f"viewsAckee amount changed: {shout.viewsAckee} --> {amount}") - shout.viewsAckee = amount + shout = session.query(Shout).where(Shout.slug == shout_slug).one() + viewer = session.query(User).where(User.slug == viewer).one() + new_viewed = ViewedEntry.create(**{ + "viewer": viewer.id, + "shout": shout.id, + "amount": amount + }) + session.add(new_viewed) 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)