new-version-0-2-13
Some checks failed
deploy / deploy (push) Failing after 1m54s

This commit is contained in:
Untone 2023-11-03 13:10:22 +03:00
parent 1f5e5472c9
commit 435d1e4505
21 changed files with 392 additions and 436 deletions

View File

@ -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] [0.2.12]
- Author.userpic -> Author.pic - Author.userpic -> Author.pic
- CommunityAuthor.role is string now - CommunityAuthor.role is string now

View File

@ -2,8 +2,9 @@ from services.db import Base, engine
from orm.shout import Shout from orm.shout import Shout
from orm.community import Community from orm.community import Community
def init_tables(): def init_tables():
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Community.init_table()
Shout.init_table() Shout.init_table()
Community.init_table()
print("[orm] tables initialized") print("[orm] tables initialized")

View File

@ -1,6 +1,6 @@
from datetime import datetime import time
from sqlalchemy import JSON as JSONType 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 sqlalchemy.orm import relationship
from services.db import Base from services.db import Base
@ -13,10 +13,6 @@ class AuthorRating(Base):
author = Column(ForeignKey("author.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True)
value = Column(Integer) value = Column(Integer)
@staticmethod
def init_table():
pass
class AuthorFollower(Base): class AuthorFollower(Base):
__tablename__ = "author_follower" __tablename__ = "author_follower"
@ -24,23 +20,25 @@ class AuthorFollower(Base):
id = None # type: ignore id = None # type: ignore
follower = Column(ForeignKey("author.id"), primary_key=True, index=True) follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
author = 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) auto = Column(Boolean, nullable=False, default=False)
class Author(Base): class Author(Base):
__tablename__ = "author" __tablename__ = "author"
user = Column(String, nullable=False) # unbounded link with authorizer's User type user = Column(String, unique=True) # 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")
name = Column(String, nullable=True, comment="Display name") name = Column(String, nullable=True, comment="Display name")
slug = Column(String, unique=True, comment="Author's slug") slug = Column(String, unique=True, comment="Author's slug")
bio = Column(String, nullable=True, comment="Bio") # status description
createdAt = Column(DateTime, nullable=False, default=datetime.now) about = Column(String, nullable=True, comment="About") # long and formatted
lastSeen = Column(DateTime, nullable=False, default=datetime.now) # Td se 0e pic = Column(String, nullable=True, comment="Picture")
deletedAt = Column(DateTime, nullable=True, comment="Deleted at")
links = Column(JSONType, nullable=True, comment="Links") links = Column(JSONType, nullable=True, comment="Links")
ratings = relationship(AuthorRating, foreign_keys=AuthorRating.author) 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")

View File

@ -1,5 +1,5 @@
from datetime import datetime import time
from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy import Column, Integer, ForeignKey, String
from services.db import Base from services.db import Base
@ -18,6 +18,6 @@ class Collection(Base):
title = Column(String, nullable=False, comment="Title") title = Column(String, nullable=False, comment="Title")
body = Column(String, nullable=True, comment="Body") body = Column(String, nullable=True, comment="Body")
pic = Column(String, nullable=True, comment="Picture") pic = Column(String, nullable=True, comment="Picture")
createdAt = Column(DateTime, default=datetime.now, comment="Created At") created_at = Column(Integer, default=lambda: int(time.time()))
createdBy = Column(ForeignKey("author.id"), comment="Created By") created_by = Column(ForeignKey("author.id"), comment="Created By")
publishedAt = Column(DateTime, default=datetime.now, comment="Published At") published_at = Column(Integer, default=lambda: int(time.time()))

View File

@ -1,5 +1,5 @@
from datetime import datetime import time
from sqlalchemy import Column, String, ForeignKey, DateTime from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import Base, local_session from services.db import Base, local_session
@ -12,7 +12,7 @@ class CommunityAuthor(Base):
id = None # type: ignore id = None # type: ignore
follower = Column(ForeignKey("author.id"), primary_key=True) follower = Column(ForeignKey("author.id"), primary_key=True)
community = Column(ForeignKey("community.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) role = Column(String, nullable=False)
@ -23,17 +23,16 @@ class Community(Base):
slug = Column(String, nullable=False, unique=True) slug = Column(String, nullable=False, unique=True)
desc = Column(String, nullable=False, default="") desc = Column(String, nullable=False, default="")
pic = 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 @staticmethod
def init_table(): def init_table():
with local_session() as session: 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: if not d:
d = Community.create(name="Дискурс", slug="discours") d = Community.create(name="Дискурс", slug="discours")
session.add(d) print("[orm] created community %s" % d.slug)
session.commit()
Community.default_community = d Community.default_community = d
print('[orm] default community id: %s' % d.id) print("[orm] default community is %s" % d.slug)

View File

@ -1,7 +1,7 @@
from datetime import datetime
from enum import Enum as Enumeration 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 from services.db import Base
import time
class ReactionKind(Enumeration): class ReactionKind(Enumeration):
@ -26,13 +26,12 @@ class Reaction(Base):
__tablename__ = "reaction" __tablename__ = "reaction"
body = Column(String, nullable=True, comment="Reaction Body") body = Column(String, nullable=True, comment="Reaction Body")
createdAt = Column(DateTime, nullable=False, default=datetime.now) created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
createdBy = Column(ForeignKey("author.id"), nullable=False, index=True) created_by = Column(ForeignKey("author.id"), nullable=False, index=True)
updatedAt = Column(DateTime, nullable=True, comment="Updated at") updated_at = Column(Integer, nullable=True, comment="Updated at")
updatedBy = Column(ForeignKey("author.id"), nullable=True, index=True) deleted_at = Column(Integer, nullable=True, comment="Deleted at")
deletedAt = Column(DateTime, nullable=True, comment="Deleted at") deleted_by = Column(ForeignKey("author.id"), nullable=True, index=True)
deletedBy = Column(ForeignKey("author.id"), nullable=True, index=True)
shout = Column(ForeignKey("shout.id"), nullable=False, index=True) shout = Column(ForeignKey("shout.id"), nullable=False, index=True)
replyTo = Column(ForeignKey("reaction.id"), nullable=True) reply_to = Column(ForeignKey("reaction.id"), nullable=True)
range = Column(String, nullable=True, comment="<start index>:<end>") quote = Column(String, nullable=True, comment="a quoted fragment")
kind = Column(Enum(ReactionKind), nullable=False) kind = Column(Enum(ReactionKind), nullable=False)

View File

@ -1,10 +1,9 @@
from datetime import datetime import time
from enum import Enum as Enumeration from enum import Enum as Enumeration
from sqlalchemy import ( from sqlalchemy import (
Enum, Enum,
Boolean, Boolean,
Column, Column,
DateTime,
ForeignKey, ForeignKey,
Integer, Integer,
String, String,
@ -33,10 +32,8 @@ class ShoutReactionsFollower(Base):
follower = Column(ForeignKey("author.id"), primary_key=True, index=True) follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
auto = Column(Boolean, nullable=False, default=False) auto = Column(Boolean, nullable=False, default=False)
createdAt = Column( created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
DateTime, nullable=False, default=datetime.now, comment="Created at" deleted_at = Column(Integer, nullable=True)
)
deletedAt = Column(DateTime, nullable=True)
class ShoutAuthor(Base): class ShoutAuthor(Base):
@ -65,13 +62,13 @@ class ShoutVisibility(Enumeration):
class Shout(Base): class Shout(Base):
__tablename__ = "shout" __tablename__ = "shout"
createdAt = Column(DateTime, nullable=False, default=datetime.now) created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
updatedAt = Column(DateTime, nullable=True) updated_at = Column(Integer, nullable=True)
publishedAt = Column(DateTime, nullable=True) published_at = Column(Integer, nullable=True)
deletedAt = Column(DateTime, nullable=True) deleted_at = Column(Integer, nullable=True)
createdBy = Column(ForeignKey("author.id"), comment="Created By") created_by = Column(ForeignKey("author.id"), comment="Created By")
deletedBy = Column(ForeignKey("author.id"), nullable=True) deleted_by = Column(ForeignKey("author.id"), nullable=True)
body = Column(String, nullable=False, comment="Body") body = Column(String, nullable=False, comment="Body")
slug = Column(String, unique=True) slug = Column(String, unique=True)
@ -85,21 +82,17 @@ class Shout(Base):
authors = relationship(lambda: Author, secondary=ShoutAuthor.__tablename__) authors = relationship(lambda: Author, secondary=ShoutAuthor.__tablename__)
topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__)
communities = relationship( communities = relationship(lambda: Community, secondary=ShoutCommunity.__tablename__)
lambda: Community, secondary=ShoutCommunity.__tablename__
)
reactions = relationship(lambda: Reaction) reactions = relationship(lambda: Reaction)
viewsOld = Column(Integer, default=0) views_old = Column(Integer, default=0)
viewsAckee = Column(Integer, default=0) views_ackee = Column(Integer, default=0)
views = column_property(viewsOld + viewsAckee) views = column_property(views_old + views_ackee)
visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS) visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS)
# TODO: these field should be used or modified
lang = Column(String, nullable=False, default="ru", comment="Language") lang = Column(String, nullable=False, default="ru", comment="Language")
mainTopic = Column(ForeignKey("topic.slug"), nullable=True) version_of = Column(ForeignKey("shout.id"), nullable=True)
versionOf = Column(ForeignKey("shout.id"), nullable=True)
oid = Column(String, nullable=True) oid = Column(String, nullable=True)
@staticmethod @staticmethod
@ -114,5 +107,3 @@ class Shout(Base):
"lang": "ru", "lang": "ru",
} }
s = Shout.create(**entry) s = Shout.create(**entry)
session.add(s)
session.commit()

View File

@ -1,5 +1,5 @@
from datetime import datetime import time
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String from sqlalchemy import Boolean, Column, Integer, ForeignKey, String
from services.db import Base from services.db import Base
@ -9,7 +9,7 @@ class TopicFollower(Base):
id = None # type: ignore id = None # type: ignore
follower = Column(ForeignKey("author.id"), primary_key=True, index=True) follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.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) auto = Column(Boolean, nullable=False, default=False)

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "discoursio-core" name = "discoursio-core"
version = "0.2.12" version = "0.2.13"
description = "core module for discours.io" description = "core module for discours.io"
authors = ["discoursio devteam"] authors = ["discoursio devteam"]
license = "MIT" license = "MIT"

View File

@ -1,10 +1,11 @@
import time
from typing import List from typing import List
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func, distinct, select, literal from sqlalchemy import and_, func, distinct, select, literal
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.unread import get_total_unread_counter
from services.schema import mutation, query from services.schema import mutation, query
from orm.shout import ShoutAuthor, ShoutTopic from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic 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.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') # 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): async def author_followings(author_id: int):
return { 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": [ "topics": [
t.slug for t in await followed_topics(author_id) t.slug for t in await followed_topics(author_id)
], # followed topics slugs ], # followed topics slugs
@ -168,14 +169,15 @@ async def load_authors_by(_, _info, by, limit, offset):
.join(Topic) .join(Topic)
.where(Topic.slug == by["topic"]) .where(Topic.slug == by["topic"])
) )
if by.get("lastSeen"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"]) if by.get("last_seen"): # in unixtime
q = q.filter(Author.lastSeen > days_before) before = int(time.time()) - by["last_seen"]
elif by.get("createdAt"): # in days q = q.filter(Author.last_seen > before)
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"]) elif by.get("created_at"): # in unixtime
q = q.filter(Author.createdAt > days_before) 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) return get_authors_from_query(q)
@ -226,7 +228,8 @@ async def rate_author(_, info, rated_user_id, value):
session.query(AuthorRating) session.query(AuthorRating)
.filter( .filter(
and_( and_(
AuthorRating.rater == author_id, AuthorRating.user == rated_user_id AuthorRating.rater == author_id,
AuthorRating.user == rated_user_id
) )
) )
.first() .first()

View File

@ -28,7 +28,7 @@ def add_community_stat_columns(q):
# ) # )
q = q.add_columns(literal(0).label("commented_stat")) 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') # func.count(distinct(Reaction.id)).label('commented_stat')
# ) # )

View File

@ -1,40 +1,32 @@
from datetime import datetime, timezone import time # For Unix timestamps
from sqlalchemy import and_, select from sqlalchemy import and_, select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.schema import mutation, query 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 orm.topic import Topic
from reaction import reactions_follow, reactions_unfollow from reaction import reactions_follow, reactions_unfollow
from services.notify import notify_shout from services.notify import notify_shout
@query.field("loadDrafts") @query.field("loadDrafts")
async def get_drafts(_, info): async def get_drafts(_, info):
author = info.context["request"].author author = info.context["request"].author
q = ( q = (
select(Shout) select(Shout)
.options( .options(
joinedload(Shout.authors), joinedload(Shout.authors),
joinedload(Shout.topics), 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) q = q.group_by(Shout.id)
shouts = [] shouts = []
with local_session() as session: with local_session() as session:
for [shout] in session.execute(q).unique(): for [shout] in session.execute(q).unique():
shouts.append(shout) shouts.append(shout)
return shouts return shouts
@mutation.field("createShout") @mutation.field("createShout")
@login_required @login_required
async def create_shout(_, info, inp): async def create_shout(_, info, inp):
@ -43,7 +35,8 @@ async def create_shout(_, info, inp):
topics = ( topics = (
session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all() 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( new_shout = Shout.create(
**{ **{
"title": inp.get("title"), "title": inp.get("title"),
@ -54,43 +47,33 @@ async def create_shout(_, info, inp):
"layout": inp.get("layout"), "layout": inp.get("layout"),
"authors": inp.get("authors", []), "authors": inp.get("authors", []),
"slug": inp.get("slug"), "slug": inp.get("slug"),
"mainTopic": inp.get("mainTopic"), "topics": inp.get("topics"),
"visibility": "authors", "visibility": ShoutVisibility.AUTHORS,
"createdBy": author_id, "created_id": author_id,
"created_at": current_time, # Set created_at as Unix timestamp
} }
) )
for topic in topics: for topic in topics:
t = ShoutTopic.create(topic=topic.id, shout=new_shout.id) t = ShoutTopic.create(topic=topic.id, shout=new_shout.id)
session.add(t) session.add(t)
# NOTE: shout made by one first author # NOTE: shout made by one first author
sa = ShoutAuthor.create(shout=new_shout.id, author=author_id) sa = ShoutAuthor.create(shout=new_shout.id, author=author_id)
session.add(sa) session.add(sa)
session.add(new_shout) session.add(new_shout)
reactions_follow(author_id, new_shout.id, True) reactions_follow(author_id, new_shout.id, True)
session.commit() 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: if new_shout.slug is None:
new_shout.slug = f"draft-{new_shout.id}" new_shout.slug = f"draft-{new_shout.id}"
session.commit() session.commit()
else: else:
notify_shout(new_shout.dict(), "create") notify_shout(new_shout.dict(), "create")
return {"shout": new_shout} return {"shout": new_shout}
@mutation.field("updateShout") @mutation.field("updateShout")
@login_required @login_required
async def update_shout(_, info, shout_id, shout_input=None, publish=False): async def update_shout(_, info, shout_id, shout_input=None, publish=False):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
shout = ( shout = (
session.query(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) .filter(Shout.id == shout_id)
.first() .first()
) )
if not shout: if not shout:
return {"error": "shout not found"} return {"error": "shout not found"}
if shout.created_by != author_id:
if shout.createdBy != author_id:
return {"error": "access denied"} return {"error": "access denied"}
updated = False updated = False
if shout_input is not None: if shout_input is not None:
topics_input = shout_input["topics"] topics_input = shout_input["topics"]
del shout_input["topics"] del shout_input["topics"]
new_topics_to_link = [] new_topics_to_link = []
new_topics = [ new_topics = [
topic_input for topic_input in topics_input if topic_input["id"] < 0 topic_input for topic_input in topics_input if topic_input["id"] < 0
] ]
for new_topic in new_topics: for new_topic in new_topics:
del new_topic["id"] del new_topic["id"]
created_new_topic = Topic.create(**new_topic) created_new_topic = Topic.create(**new_topic)
session.add(created_new_topic) session.add(created_new_topic)
new_topics_to_link.append(created_new_topic) new_topics_to_link.append(created_new_topic)
if len(new_topics) > 0: if len(new_topics) > 0:
session.commit() session.commit()
for new_topic_to_link in new_topics_to_link: for new_topic_to_link in new_topics_to_link:
created_unlinked_topic = ShoutTopic.create( created_unlinked_topic = ShoutTopic.create(
shout=shout.id, topic=new_topic_to_link.id shout=shout.id, topic=new_topic_to_link.id
) )
session.add(created_unlinked_topic) session.add(created_unlinked_topic)
existing_topics_input = [ existing_topics_input = [
topic_input topic_input
for topic_input in topics_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"] if existing_topic_input["id"]
not in [topic.id for topic in shout.topics] not in [topic.id for topic in shout.topics]
] ]
for existing_topic_to_link_id in existing_topic_to_link_ids: for existing_topic_to_link_id in existing_topic_to_link_ids:
created_unlinked_topic = ShoutTopic.create( created_unlinked_topic = ShoutTopic.create(
shout=shout.id, topic=existing_topic_to_link_id shout=shout.id, topic=existing_topic_to_link_id
) )
session.add(created_unlinked_topic) session.add(created_unlinked_topic)
topic_to_unlink_ids = [ topic_to_unlink_ids = [
topic.id topic.id
for topic in shout.topics for topic in shout.topics
if topic.id if topic.id
not in [topic_input["id"] for topic_input in existing_topics_input] not in [topic_input["id"] for topic_input in existing_topics_input]
] ]
shout_topics_to_remove = session.query(ShoutTopic).filter( shout_topics_to_remove = session.query(ShoutTopic).filter(
and_( and_(
ShoutTopic.shout == shout.id, ShoutTopic.shout == shout.id,
ShoutTopic.topic.in_(topic_to_unlink_ids), ShoutTopic.topic.in_(topic_to_unlink_ids),
) )
) )
for shout_topic_to_remove in shout_topics_to_remove: for shout_topic_to_remove in shout_topics_to_remove:
session.delete(shout_topic_to_remove) session.delete(shout_topic_to_remove)
shout_input["mainTopic"] = shout_input["mainTopic"]["slug"] shout_input["mainTopic"] = shout_input["mainTopic"]["slug"]
if shout_input["mainTopic"] == "": if shout_input["mainTopic"] == "":
del 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) shout.update(shout_input)
updated = True updated = True
# TODO: use visibility setting # TODO: use visibility setting
if publish and shout.visibility == "authors": if publish and shout.visibility == "authors":
shout.visibility = "community" shout.visibility = "community"
shout.publishedAt = datetime.now(tz=timezone.utc) shout.published_at = current_time # Set published_at as Unix timestamp
updated = True updated = True
# notify on publish # notify on publish
notify_shout(shout.dict()) notify_shout(shout.dict())
if updated: if updated:
shout.updatedAt = datetime.now(tz=timezone.utc) session.commit()
session.commit()
# GitTask(inp, user.username, user.email, "update shout %s" % slug) # GitTask(inp, user.username, user.email, "update shout %s" % slug)
notify_shout(shout.dict(), "update") notify_shout(shout.dict(), "update")
return {"shout": shout} return {"shout": shout}
@mutation.field("deleteShout") @mutation.field("deleteShout")
@login_required @login_required
async def delete_shout(_, info, shout_id): async def delete_shout(_, info, shout_id):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
shout = session.query(Shout).filter(Shout.id == shout_id).first() shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout: if not shout:
return {"error": "invalid shout id"} return {"error": "invalid shout id"}
if author_id != shout.created_by:
if author_id != shout.createdBy:
return {"error": "access denied"} return {"error": "access denied"}
for author_id in shout.authors: for author_id in shout.authors:
reactions_unfollow(author_id, shout_id) reactions_unfollow(author_id, shout_id)
# Replace datetime with Unix timestamp
shout.deletedAt = datetime.now(tz=timezone.utc) current_time = int(time.time())
shout.deleted_at = current_time # Set deleted_at as Unix timestamp
session.commit() session.commit()
notify_shout(shout.dict(), "delete") notify_shout(shout.dict(), "delete")
return {} return {}

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone import time
from sqlalchemy import and_, asc, desc, select, text, func, case from sqlalchemy import and_, asc, desc, select, text, func, case
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from services.notify import notify_reaction from services.notify import notify_reaction
@ -15,7 +15,7 @@ def add_reaction_stat_columns(q):
aliased_reaction = aliased(Reaction) aliased_reaction = aliased(Reaction)
q = q.outerjoin( q = q.outerjoin(
aliased_reaction, Reaction.id == aliased_reaction.replyTo aliased_reaction, Reaction.id == aliased_reaction.reply_to
).add_columns( ).add_columns(
func.sum(aliased_reaction.id).label("reacted_stat"), func.sum(aliased_reaction.id).label("reacted_stat"),
func.sum(case((aliased_reaction.body.is_not(None), 1), else_=0)).label( 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 ( return (
session.query(Shout) session.query(Shout)
.where(Shout.authors.contains(author_id)) .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() .count()
> 0 > 0
) )
@ -104,7 +104,7 @@ def is_published_author(session, author_id):
def check_to_publish(session, author_id, reaction): def check_to_publish(session, author_id, reaction):
"""set shout to public if publicated approvers amount > 4""" """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.ACCEPT,
ReactionKind.LIKE, ReactionKind.LIKE,
ReactionKind.PROOF, ReactionKind.PROOF,
@ -118,7 +118,7 @@ def check_to_publish(session, author_id, reaction):
author_id, author_id,
] ]
for ar in approvers_reactions: for ar in approvers_reactions:
a = ar.createdBy a = ar.created_by
if is_published_author(session, a): if is_published_author(session, a):
approvers.append(a) approvers.append(a)
if len(approvers) > 4: if len(approvers) > 4:
@ -128,7 +128,7 @@ def check_to_publish(session, author_id, reaction):
def check_to_hide(session, reaction): def check_to_hide(session, reaction):
"""hides any shout if 20% of reactions are negative""" """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.REJECT,
ReactionKind.DISLIKE, ReactionKind.DISLIKE,
ReactionKind.DISPROOF, ReactionKind.DISPROOF,
@ -152,7 +152,7 @@ def check_to_hide(session, reaction):
def set_published(session, shout_id): def set_published(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first() 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") s.visibility = text("public")
session.add(s) session.add(s)
session.commit() session.commit()
@ -170,7 +170,7 @@ def set_hidden(session, shout_id):
async def create_reaction(_, info, reaction): async def create_reaction(_, info, reaction):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
reaction["createdBy"] = author_id reaction["created_by"] = author_id
shout = session.query(Shout).where(Shout.id == reaction["shout"]).one() shout = session.query(Shout).where(Shout.id == reaction["shout"]).one()
if reaction["kind"] in [ReactionKind.DISLIKE.name, ReactionKind.LIKE.name]: if reaction["kind"] in [ReactionKind.DISLIKE.name, ReactionKind.LIKE.name]:
@ -179,9 +179,9 @@ async def create_reaction(_, info, reaction):
.where( .where(
and_( and_(
Reaction.shout == reaction["shout"], Reaction.shout == reaction["shout"],
Reaction.createdBy == author_id, Reaction.created_by == author_id,
Reaction.kind == reaction["kind"], Reaction.kind == reaction["kind"],
Reaction.replyTo == reaction.get("replyTo"), Reaction.reply_to == reaction.get("reply_to"),
) )
) )
.first() .first()
@ -200,9 +200,9 @@ async def create_reaction(_, info, reaction):
.where( .where(
and_( and_(
Reaction.shout == reaction["shout"], Reaction.shout == reaction["shout"],
Reaction.createdBy == author_id, Reaction.created_by == author_id,
Reaction.kind == opposite_reaction_kind, Reaction.kind == opposite_reaction_kind,
Reaction.replyTo == reaction.get("replyTo"), Reaction.reply_to == reaction.get("reply_to"),
) )
) )
.first() .first()
@ -215,12 +215,12 @@ async def create_reaction(_, info, reaction):
# Proposal accepting logix # Proposal accepting logix
if ( if (
r.replyTo is not None r.reply_to is not None
and r.kind == ReactionKind.ACCEPT and r.kind == ReactionKind.ACCEPT
and author_id in shout.dict()["authors"] and author_id in shout.dict()["authors"]
): ):
replied_reaction = ( 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 and replied_reaction.kind == ReactionKind.PROPOSE:
if replied_reaction.range: if replied_reaction.range:
@ -237,7 +237,7 @@ async def create_reaction(_, info, reaction):
rdict = r.dict() rdict = r.dict()
rdict["shout"] = shout.dict() rdict["shout"] = shout.dict()
author = session.query(Author).where(Author.id == author_id).first() author = session.query(Author).where(Author.id == author_id).first()
rdict["createdBy"] = author.dict() rdict["created_by"] = author.dict()
# self-regulation mechanics # self-regulation mechanics
@ -274,11 +274,11 @@ async def update_reaction(_, info, rid, reaction={}):
if not r: if not r:
return {"error": "invalid reaction id"} return {"error": "invalid reaction id"}
if r.createdBy != author_id: if r.created_by != author_id:
return {"error": "access denied"} return {"error": "access denied"}
r.body = reaction["body"] r.body = reaction["body"]
r.updatedAt = datetime.now(tz=timezone.utc) r.updated_at = int(time.time())
if r.kind != reaction["kind"]: if r.kind != reaction["kind"]:
# NOTE: change mind detection can be here # NOTE: change mind detection can be here
pass pass
@ -304,13 +304,13 @@ async def delete_reaction(_, info, rid):
r = session.query(Reaction).filter(Reaction.id == rid).first() r = session.query(Reaction).filter(Reaction.id == rid).first()
if not r: if not r:
return {"error": "invalid reaction id"} return {"error": "invalid reaction id"}
if r.createdBy != author_id: if r.created_by != author_id:
return {"error": "access denied"} return {"error": "access denied"}
if r.kind in [ReactionKind.LIKE, ReactionKind.DISLIKE]: if r.kind in [ReactionKind.LIKE, ReactionKind.DISLIKE]:
session.delete(r) session.delete(r)
else: else:
r.deletedAt = datetime.now(tz=timezone.utc) r.deleted_at = int(time.time())
session.commit() session.commit()
notify_reaction(r.dict(), "delete") notify_reaction(r.dict(), "delete")
@ -325,11 +325,11 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
:param by: { :param by: {
:shout - filter by slug :shout - filter by slug
:shouts - filer by shout slug list :shouts - filer by shout slug list
:createdBy - to filter by author :created_by - to filter by author
:topic - to filter by topic :topic - to filter by topic
:search - to search by reactions' body :search - to search by reactions' body
:comment - true if body.length > 0 :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 :sort - a fieldname to sort desc by default
} }
:param limit: int amount of shouts :param limit: int amount of shouts
@ -339,7 +339,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
q = ( q = (
select(Reaction, Author, Shout) select(Reaction, Author, Shout)
.join(Author, Reaction.createdBy == Author.id) .join(Author, Reaction.created_by == Author.id)
.join(Shout, Reaction.shout == Shout.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"): elif by.get("shouts"):
q = q.filter(Shout.slug.in_(by["shouts"])) q = q.filter(Shout.slug.in_(by["shouts"]))
if by.get("createdBy"): if by.get("created_by"):
q = q.filter(Author.id == by.get("createdBy")) q = q.filter(Author.id == by.get("created_by"))
if by.get("topic"): if by.get("topic"):
# TODO: check # TODO: check
@ -361,15 +361,15 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
if len(by.get("search", "")) > 2: if len(by.get("search", "")) > 2:
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%')) q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
if by.get("days"): if by.get("time_ago"):
after = datetime.now(tz=timezone.utc) - timedelta(days=int(by["days"]) or 30) after = int(time.time()) - int(by.get("time_ago", 0))
q = q.filter(Reaction.createdAt > after) # FIXME: use comparing operator? q = q.filter(Reaction.created_at > after)
order_way = asc if by.get("sort", "").startswith("-") else desc 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 = q.group_by(Reaction.id, Author.id, Shout.id).order_by(order_way(order_field))
q = add_reaction_stat_columns(q) 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) q = q.limit(limit).offset(offset)
reactions = [] reactions = []
session = info.context["session"] session = info.context["session"]
@ -381,7 +381,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
commented_stat, commented_stat,
rating_stat, rating_stat,
] in session.execute(q): ] in session.execute(q):
reaction.createdBy = author reaction.created_by = author
reaction.shout = shout reaction.shout = shout
reaction.stat = { reaction.stat = {
"rating": rating_stat, "rating": rating_stat,
@ -393,7 +393,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
# ? # ?
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.created_at)
return reactions return reactions
@ -407,8 +407,8 @@ async def followed_reactions(_, info):
author = session.query(Author).where(Author.id == author_id).first() author = session.query(Author).where(Author.id == author_id).first()
reactions = ( reactions = (
session.query(Reaction.shout) session.query(Reaction.shout)
.where(Reaction.createdBy == author.id) .where(Reaction.created_by == author.id)
.filter(Reaction.createdAt > author.lastSeen) .filter(Reaction.created_at > author.last_seen)
.all() .all()
) )

View File

@ -1,5 +1,4 @@
from datetime import datetime, timedelta, timezone import time
from aiohttp.web_exceptions import HTTPException from aiohttp.web_exceptions import HTTPException
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func, case, and_, nulls_last 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.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.author import AuthorFollower from orm.author import AuthorFollower
from servies.viewed import ViewedStorage
def add_stat_columns(q): def add_stat_columns(q):
aliased_reaction = aliased(Reaction) aliased_reaction = aliased(Reaction)
q = q.outerjoin(aliased_reaction).add_columns( q = q.outerjoin(aliased_reaction).add_columns(
func.sum(aliased_reaction.id).label("reacted_stat"), func.sum(aliased_reaction.id).label("reacted_stat"),
func.sum( func.sum(
@ -23,7 +22,7 @@ def add_stat_columns(q):
func.sum( func.sum(
case( case(
# do not count comments' reactions # 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.AGREE, 1),
(aliased_reaction.kind == ReactionKind.DISAGREE, -1), (aliased_reaction.kind == ReactionKind.DISAGREE, -1),
(aliased_reaction.kind == ReactionKind.PROOF, 1), (aliased_reaction.kind == ReactionKind.PROOF, 1),
@ -38,7 +37,7 @@ def add_stat_columns(q):
func.max( func.max(
case( case(
(aliased_reaction.kind != ReactionKind.COMMENT, None), (aliased_reaction.kind != ReactionKind.COMMENT, None),
else_=aliased_reaction.createdAt, else_=aliased_reaction.created_at,
) )
).label("last_comment"), ).label("last_comment"),
) )
@ -48,7 +47,7 @@ def add_stat_columns(q):
def apply_filters(q, filters, author_id=None): def apply_filters(q, filters, author_id=None):
if filters.get("reacted") and author_id: 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") v = filters.get("visibility")
if v == "public": 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")}%')) q = q.filter(Shout.title.ilike(f'%{filters.get("title")}%'))
if filters.get("body"): if filters.get("body"):
q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s')) q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s'))
if filters.get("days"): if filters.get("time_ago"):
before = datetime.now(tz=timezone.utc) - timedelta( before = int(time.time()) - int(filters.get("time_ago"))
days=int(filters.get("days")) or 30 q = q.filter(Shout.created_at > before)
)
q = q.filter(Shout.createdAt > before)
return q return q
@ -89,11 +86,12 @@ async def load_shout(_, _info, slug=None, shout_id=None):
if shout_id is not None: if shout_id is not None:
q = q.filter(Shout.id == shout_id) 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: try:
[ [
shout, shout,
viewed_stat,
reacted_stat, reacted_stat,
commented_stat, commented_stat,
rating_stat, rating_stat,
@ -101,7 +99,7 @@ async def load_shout(_, _info, slug=None, shout_id=None):
] = session.execute(q).first() ] = session.execute(q).first()
shout.stat = { shout.stat = {
"viewed": shout.views, "viewed": viewed_stat,
"reacted": reacted_stat, "reacted": reacted_stat,
"commented": commented_stat, "commented": commented_stat,
"rating": rating_stat, "rating": rating_stat,
@ -134,7 +132,7 @@ async def load_shouts_by(_, info, options):
} }
offset: 0 offset: 0
limit: 50 limit: 50
order_by: 'createdAt' | 'commented' | 'reacted' | 'rating' order_by: 'created_at' | 'commented' | 'reacted' | 'rating'
order_by_desc: true order_by_desc: true
} }
@ -147,7 +145,7 @@ async def load_shouts_by(_, info, options):
joinedload(Shout.authors), joinedload(Shout.authors),
joinedload(Shout.topics), joinedload(Shout.topics),
) )
.where(Shout.deletedAt.is_(None)) .where(Shout.deleted_at.is_(None))
) )
q = add_stat_columns(q) q = add_stat_columns(q)
@ -155,7 +153,7 @@ async def load_shouts_by(_, info, options):
author_id = info.context["author_id"] author_id = info.context["author_id"]
q = apply_filters(q, options.get("filters", {}), 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 = ( query_order_by = (
desc(order_by) if options.get("order_by_desc", True) else asc(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: with local_session() as session:
for [ for [
shout, shout,
viewed_stat,
reacted_stat, reacted_stat,
commented_stat, commented_stat,
rating_stat, rating_stat,
@ -182,7 +181,7 @@ async def load_shouts_by(_, info, options):
] in session.execute(q).unique(): ] in session.execute(q).unique():
shouts.append(shout) shouts.append(shout)
shout.stat = { shout.stat = {
"viewed": shout.views, "viewed": viewed_stat,
"reacted": reacted_stat, "reacted": reacted_stat,
"commented": commented_stat, "commented": commented_stat,
"rating": rating_stat, "rating": rating_stat,
@ -196,12 +195,17 @@ async def load_shouts_by(_, info, options):
async def get_my_feed(_, info, options): async def get_my_feed(_, info, options):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: 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 = ( subquery = (
select(Shout.id) select(Shout.id)
.join(ShoutAuthor) .where(Shout.id == ShoutAuthor.shout)
.join(AuthorFollower, AuthorFollower.follower._is(author_id)) .where(Shout.id == ShoutTopic.shout)
.join(ShoutTopic) .where(
.join(TopicFollower, TopicFollower.follower._is(author_id)) (ShoutAuthor.author.in_(author_followed_authors))
| (ShoutTopic.topic.in_(author_followed_topics))
)
) )
q = ( q = (
@ -212,8 +216,8 @@ async def get_my_feed(_, info, options):
) )
.where( .where(
and_( and_(
Shout.publishedAt.is_not(None), Shout.published_at.is_not(None),
Shout.deletedAt.is_(None), Shout.deleted_at.is_(None),
Shout.id.in_(subquery), Shout.id.in_(subquery),
) )
) )
@ -222,7 +226,7 @@ async def get_my_feed(_, info, options):
q = add_stat_columns(q) q = add_stat_columns(q)
q = apply_filters(q, options.get("filters", {}), 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 = ( query_order_by = (
desc(order_by) if options.get("order_by_desc", True) else asc(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 = []
shouts_map = {}
for [ for [
shout, shout,
reacted_stat, reacted_stat,
@ -246,13 +249,11 @@ async def get_my_feed(_, info, options):
rating_stat, rating_stat,
_last_comment, _last_comment,
] in session.execute(q).unique(): ] in session.execute(q).unique():
shouts.append(shout)
shout.stat = { shout.stat = {
"viewed": shout.views, "viewed": ViewedStorage.get_shout(shout.slug),
"reacted": reacted_stat, "reacted": reacted_stat,
"commented": commented_stat, "commented": commented_stat,
"rating": rating_stat, "rating": rating_stat,
} }
shouts_map[shout.id] = shout shouts.append(shout)
# FIXME: shouts_map does not go anywhere?
return shouts return shouts

View File

@ -136,10 +136,7 @@ def topic_follow(follower_id, slug):
try: try:
with local_session() as session: with local_session() as session:
topic = session.query(Topic).where(Topic.slug == slug).one() topic = session.query(Topic).where(Topic.slug == slug).one()
_following = TopicFollower.create(topic=topic.id, follower=follower_id)
following = TopicFollower.create(topic=topic.id, follower=follower_id)
session.add(following)
session.commit()
return True return True
except Exception: except Exception:
return False return False

View File

@ -1,9 +1,3 @@
# Скалярные типы данных
scalar DateTime
# Перечисления
enum ShoutVisibility { enum ShoutVisibility {
AUTHORS AUTHORS
COMMUNITY COMMUNITY
@ -42,10 +36,145 @@ enum FollowingEntity {
REACTIONS 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 { input ShoutInput {
slug: String slug: String
title: String title: String
@ -57,7 +186,6 @@ input ShoutInput {
authors: [String] authors: [String]
topics: [TopicInput] topics: [TopicInput]
community: Int community: Int
mainTopic: TopicInput
subtitle: String subtitle: String
cover: String cover: String
} }
@ -65,7 +193,7 @@ input ShoutInput {
input ProfileInput { input ProfileInput {
slug: String slug: String
name: String name: String
userpic: String pic: String
links: [String] links: [String]
bio: String bio: String
about: String about: String
@ -84,12 +212,12 @@ input ReactionInput {
shout: Int! shout: Int!
range: String range: String
body: String body: String
replyTo: Int reply_to: Int
} }
input AuthorsBy { input AuthorsBy {
lastSeen: DateTime last_seen: Int
createdAt: DateTime created_at: Int
slug: String slug: String
name: String name: String
topic: String topic: String
@ -138,43 +266,12 @@ input ReactionBy {
search: String search: String
comment: Boolean comment: Boolean
topic: String topic: String
createdBy: String created_by: String
days: Int days: Int
sort: String sort: String
} }
# output type
# Типы
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
}
type Result { type Result {
error: String error: String
@ -191,108 +288,6 @@ type Result {
communities: [Community] 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 { type Mutation {

View File

@ -1,23 +1,35 @@
from contextlib import contextmanager
import logging
from typing import TypeVar, Any, Dict, Generic, Callable from typing import TypeVar, Any, Dict, Generic, Callable
from sqlalchemy import create_engine, Column, Integer from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.schema import Table from sqlalchemy.sql.schema import Table
from settings import DB_URL from settings import DB_URL
engine = create_engine( logging.basicConfig(level=logging.INFO)
DB_URL, echo=False, pool_size=10, max_overflow=20 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") T = TypeVar("T")
REGISTRY: Dict[str, type] = {} REGISTRY: Dict[str, type] = {}
@contextmanager
def local_session(): 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()): class Base(declarative_base()):
@ -36,21 +48,36 @@ class Base(declarative_base()):
@classmethod @classmethod
def create(cls: Generic[T], **kwargs) -> Generic[T]: def create(cls: Generic[T], **kwargs) -> Generic[T]:
instance = cls(**kwargs) try:
return instance.save() instance = cls(**kwargs)
return instance.save()
except Exception as e:
print(f"[services.db] Error create: {e}")
return None
def save(self) -> Generic[T]: def save(self) -> Generic[T]:
with local_session() as session: with local_session() as session:
session.add(self) try:
session.commit() session.add(self)
except Exception as e:
print(f"[services.db] Error save: {e}")
return self return self
def update(self, input): def update(self, input):
column_names = self.__table__.columns.keys() column_names = self.__table__.columns.keys()
for (name, value) in input.items(): for name, value in input.items():
if name in column_names: if name in column_names:
setattr(self, name, value) 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]: def dict(self) -> Dict[str, Any]:
column_names = self.__table__.columns.keys() 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 {}

View File

@ -40,7 +40,7 @@ class FollowingManager:
try: try:
async with FollowingManager.lock: async with FollowingManager.lock:
for entity in FollowingManager[kind]: for entity in FollowingManager[kind]:
if payload.shout['createdBy'] == entity.uid: if payload.shout['created_by'] == entity.uid:
entity.queue.put_nowait(payload) entity.queue.put_nowait(payload)
except Exception as e: except Exception as e:
print(Exception(e)) print(Exception(e))

View File

@ -29,7 +29,7 @@ async def notify_shout(shout, action: str = "create"):
async def notify_follower(follower: dict, author_id: int, action: str = "follow"): async def notify_follower(follower: dict, author_id: int, action: str = "follow"):
fields = follower.keys() fields = follower.keys()
for k in fields: for k in fields:
if k not in ["id", "name", "slug", "userpic"]: if k not in ["id", "name", "slug", "pic"]:
del follower[k] del follower[k]
channel_name = f"follower:{author_id}" channel_name = f"follower:{author_id}"
data = { data = {

View File

@ -57,6 +57,7 @@ class ViewedStorage:
lock = asyncio.Lock() lock = asyncio.Lock()
by_shouts = {} by_shouts = {}
by_topics = {} by_topics = {}
by_reactions = {}
views = None views = None
pages = None pages = None
domains = None domains = None
@ -75,16 +76,16 @@ class ViewedStorage:
{"Authorization": "Bearer %s" % str(token)}, schema=schema_str {"Authorization": "Bearer %s" % str(token)}, schema=schema_str
) )
print( print(
"[stat] * authorized permanentely by ackee.discours.io: %s" % token "[services.viewed] * authorized permanentely by ackee.discours.io: %s" % token
) )
else: else:
print("[stat] * please set ACKEE_TOKEN") print("[services.viewed] * please set ACKEE_TOKEN")
self.disabled = True self.disabled = True
@staticmethod @staticmethod
async def update_pages(): async def update_pages():
"""query all the pages from ackee sorted by views count""" """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() start = time.time()
self = ViewedStorage self = ViewedStorage
try: try:
@ -100,12 +101,12 @@ class ViewedStorage:
await ViewedStorage.increment(slug, shouts[slug]) await ViewedStorage.increment(slug, shouts[slug])
except Exception: except Exception:
pass pass
print("[stat] ⎪ %d pages collected " % len(shouts.keys())) print("[services.viewed] ⎪ %d pages collected " % len(shouts.keys()))
except Exception as e: except Exception as e:
raise e raise e
end = time.time() end = time.time()
print("[stat] ⎪ update_pages took %fs " % (end - start)) print("[services.viewed] ⎪ update_pages took %fs " % (end - start))
@staticmethod @staticmethod
async def get_facts(): async def get_facts():
@ -113,26 +114,19 @@ class ViewedStorage:
async with self.lock: async with self.lock:
return self.client.execute_async(load_facts) return self.client.execute_async(load_facts)
# unused yet
@staticmethod @staticmethod
async def get_shout(shout_slug): async def get_shout(shout_slug):
"""getting shout views metric by slug""" """getting shout views metric by slug"""
self = ViewedStorage self = ViewedStorage
async with self.lock: async with self.lock:
shout_views = self.by_shouts.get(shout_slug) return self.by_shouts.get(shout_slug, 0)
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 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 @staticmethod
async def get_topic(topic_slug): async def get_topic(topic_slug):
@ -145,51 +139,36 @@ class ViewedStorage:
return topic_views return topic_views
@staticmethod @staticmethod
def update_topics(session, shout_slug): def update_topics( shout_slug):
"""updates topics counters by shout slug""" """updates topics counters by shout slug"""
self = ViewedStorage self = ViewedStorage
for [shout_topic, topic] in ( with local_session() as session:
session.query(ShoutTopic, Topic) for [shout_topic, topic] in (
.join(Topic) session.query(ShoutTopic, Topic)
.join(Shout) .join(Topic)
.where(Shout.slug == shout_slug) .join(Shout)
.all() .where(Shout.slug == shout_slug)
): .all()
if not self.by_topics.get(topic.slug): ):
self.by_topics[topic.slug] = {} if not self.by_topics.get(topic.slug):
self.by_topics[topic.slug][shout_slug] = self.by_shouts[shout_slug] self.by_topics[topic.slug] = {}
self.by_topics[topic.slug][shout_slug] = self.by_shouts[shout_slug]
@staticmethod @staticmethod
async def increment(shout_slug, amount=1, viewer="ackee"): async def increment(shout_slug, amount=1, viewer="ackee"):
"""the only way to change views counter""" """the only way to change views counter"""
self = ViewedStorage self = ViewedStorage
async with self.lock: async with self.lock:
# TODO optimize, currenty we execute 1 DB transaction per shout self.by_shouts[shout_slug] = self.by_shouts.get(shout_slug, 0) + amount
with local_session() as session: self.update_topics(shout_slug)
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
session.commit() @staticmethod
async def increment_reaction(shout_slug, reaction_id, amount=1, viewer="ackee"):
# this part is currently unused """the only way to change views counter"""
self.by_shouts[shout_slug] = self.by_shouts.get(shout_slug, 0) + amount self = ViewedStorage
self.update_topics(session, shout_slug) 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 @staticmethod
async def worker(): async def worker():
@ -201,23 +180,23 @@ class ViewedStorage:
while True: while True:
try: try:
print("[stat] - updating views...") print("[services.viewed] - updating views...")
await self.update_pages() await self.update_pages()
failed = 0 failed = 0
except Exception: except Exception:
failed += 1 failed += 1
print("[stat] - update failed #%d, wait 10 seconds" % failed) print("[services.viewed] - update failed #%d, wait 10 seconds" % failed)
if failed > 3: if failed > 3:
print("[stat] - not trying to update anymore") print("[services.viewed] - not trying to update anymore")
break break
if failed == 0: if failed == 0:
when = datetime.now(timezone.utc) + timedelta(seconds=self.period) when = datetime.now(timezone.utc) + timedelta(seconds=self.period)
t = format(when.astimezone().isoformat()) t = format(when.astimezone().isoformat())
print( print(
"[stat] ⎩ next update: %s" "[services.viewed] ⎩ next update: %s"
% (t.split("T")[0] + " " + t.split("T")[1].split(".")[0]) % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
) )
await asyncio.sleep(self.period) await asyncio.sleep(self.period)
else: else:
await asyncio.sleep(10) await asyncio.sleep(10)
print("[stat] - trying to update data again") print("[services.viewed] - trying to update data again")

View File

@ -27,11 +27,11 @@
"id": 2, "id": 2,
"name": "Дискурс", "name": "Дискурс",
"slug": "discours", "slug": "discours",
"userpic": null "pic": null
} }
], ],
"createdAt": "2023-09-04T10:15:08.666569", "created_at": "2023-09-04T10:15:08.666569",
"publishedAt": "2023-09-04T12:35:20.024954", "published_at": "2023-09-04T12:35:20.024954",
"stat": { "stat": {
"viewed": 6, "viewed": 6,
"reacted": null, "reacted": null,