granian+precommit

This commit is contained in:
2024-01-25 22:41:27 +03:00
parent ad3fd32a6e
commit 4a5f1d634a
35 changed files with 835 additions and 764 deletions

View File

@@ -23,50 +23,51 @@ from resolvers.reader import (
load_shouts_by,
load_shouts_feed,
load_shouts_random_top,
load_shouts_random_topic,
load_shouts_search,
load_shouts_unrated,
load_shouts_random_topic,
)
from resolvers.topic import get_topic, get_topics_all, get_topics_by_author, get_topics_by_community
__all__ = [
# author
"get_author",
"get_author_id",
"get_authors_all",
"get_author_followers",
"get_author_followed",
"load_authors_by",
"rate_author",
"update_profile",
'get_author',
'get_author_id',
'get_authors_all',
'get_author_followers',
'get_author_followed',
'load_authors_by',
'rate_author',
'update_profile',
# community
"get_community",
"get_communities_all",
'get_community',
'get_communities_all',
# topic
"get_topic",
"get_topics_all",
"get_topics_by_community",
"get_topics_by_author",
'get_topic',
'get_topics_all',
'get_topics_by_community',
'get_topics_by_author',
# reader
"get_shout",
"load_shouts_by",
"load_shouts_feed",
"load_shouts_search",
"load_shouts_followed",
"load_shouts_unrated",
"load_shouts_random_top",
"load_shouts_random_topic",
'get_shout',
'load_shouts_by',
'load_shouts_feed',
'load_shouts_search',
'load_shouts_followed',
'load_shouts_unrated',
'load_shouts_random_top',
'load_shouts_random_topic',
# follower
"follow",
"unfollow",
"get_my_followed",
'follow',
'unfollow',
'get_my_followed',
# editor
"create_shout",
"update_shout",
"delete_shout",
'create_shout',
'update_shout',
'delete_shout',
# reaction
"create_reaction",
"update_reaction",
"delete_reaction",
"load_reactions_by",
'create_reaction',
'update_reaction',
'delete_reaction',
'load_reactions_by',
]

View File

@@ -1,14 +1,14 @@
import logging
import time
from typing import List
import logging
from sqlalchemy import and_, case, distinct, func, literal, select, cast, Integer
from sqlalchemy import and_, distinct, func, select
from sqlalchemy.orm import aliased
from orm.author import Author, AuthorFollower, AuthorRating
from orm.community import Community
from orm.reaction import Reaction, ReactionKind
from orm.shout import ShoutAuthor, ShoutTopic, Shout
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.community import followed_communities
from resolvers.reaction import reacted_shouts_updates as followed_reactions
@@ -19,25 +19,26 @@ from services.schema import mutation, query
from services.unread import get_total_unread_counter
from services.viewed import ViewedStorage
logging.basicConfig()
logger = logging.getLogger("\t[resolvers.author]\t")
logger = logging.getLogger('\t[resolvers.author]\t')
logger.setLevel(logging.DEBUG)
def add_author_stat_columns(q):
shout_author_aliased = aliased(ShoutAuthor)
q = q.outerjoin(shout_author_aliased, shout_author_aliased.author == Author.id).add_columns(
func.count(distinct(shout_author_aliased.shout)).label("shouts_stat")
func.count(distinct(shout_author_aliased.shout)).label('shouts_stat')
)
followers_table = aliased(AuthorFollower)
q = q.outerjoin(followers_table, followers_table.author == Author.id).add_columns(
func.count(distinct(followers_table.follower)).label("followers_stat")
func.count(distinct(followers_table.follower)).label('followers_stat')
)
followings_table = aliased(AuthorFollower)
q = q.outerjoin(followings_table, followings_table.follower == Author.id).add_columns(
func.count(distinct(followers_table.author)).label("followings_stat")
func.count(distinct(followers_table.author)).label('followings_stat')
)
q = q.group_by(Author.id)
@@ -49,10 +50,10 @@ async def get_authors_from_query(q):
with local_session() as session:
for [author, shouts_stat, followers_stat, followings_stat] in session.execute(q):
author.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
"followings": followings_stat,
"viewed": await ViewedStorage.get_author(author.slug),
'shouts': shouts_stat,
'followers': followers_stat,
'followings': followings_stat,
'viewed': await ViewedStorage.get_author(author.slug),
}
authors.append(author)
return authors
@@ -61,24 +62,24 @@ async def get_authors_from_query(q):
async def author_followings(author_id: int):
# NOTE: topics, authors, shout-reactions and communities slugs list
return {
"unread": await get_total_unread_counter(author_id),
"topics": [t.slug for t in await followed_topics(author_id)],
"authors": [a.slug for a in await followed_authors(author_id)],
"reactions": [s.slug for s in await followed_reactions(author_id)],
"communities": [c.slug for c in [followed_communities(author_id)] if isinstance(c, Community)],
'unread': await get_total_unread_counter(author_id),
'topics': [t.slug for t in await followed_topics(author_id)],
'authors': [a.slug for a in await followed_authors(author_id)],
'reactions': [s.slug for s in await followed_reactions(author_id)],
'communities': [c.slug for c in [followed_communities(author_id)] if isinstance(c, Community)],
}
@mutation.field("update_profile")
@mutation.field('update_profile')
@login_required
async def update_profile(_, info, profile):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).where(Author.user == user_id).first()
Author.update(author, profile)
session.add(author)
session.commit()
return {"error": None, "author": author}
return {'error': None, 'author': author}
# for mutation.field("follow")
@@ -111,7 +112,7 @@ def author_unfollow(follower_id, slug):
# TODO: caching query
@query.field("get_authors_all")
@query.field('get_authors_all')
async def get_authors_all(_, _info):
with local_session() as session:
return session.query(Author).all()
@@ -122,40 +123,59 @@ def count_author_comments_rating(session, author_id) -> int:
replies_likes = (
session.query(replied_alias)
.join(Reaction, replied_alias.id == Reaction.reply_to)
.where(and_(replied_alias.created_by == author_id, replied_alias.kind == ReactionKind.COMMENT.value))
.where(
and_(
replied_alias.created_by == author_id,
replied_alias.kind == ReactionKind.COMMENT.value,
)
)
.filter(replied_alias.kind == ReactionKind.LIKE.value)
.count()
) or 0
replies_dislikes = (
session.query(replied_alias)
.join(Reaction, replied_alias.id == Reaction.reply_to)
.where(and_(replied_alias.created_by == author_id, replied_alias.kind == ReactionKind.COMMENT.value))
.where(
and_(
replied_alias.created_by == author_id,
replied_alias.kind == ReactionKind.COMMENT.value,
)
)
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
.count()
) or 0
return replies_likes - replies_dislikes
return replies_likes - replies_dislikes
def count_author_shouts_rating(session, author_id) -> int:
shouts_likes = (
session.query(Reaction, Shout)
.join(Shout, Shout.id == Reaction.shout)
.filter(and_(Shout.authors.any(id=author_id), Reaction.kind == ReactionKind.LIKE.value))
.filter(
and_(
Shout.authors.any(id=author_id),
Reaction.kind == ReactionKind.LIKE.value,
)
)
.count()
or 0
)
shouts_dislikes = (
session.query(Reaction, Shout)
.join(Shout, Shout.id == Reaction.shout)
.filter(and_(Shout.authors.any(id=author_id), Reaction.kind == ReactionKind.DISLIKE.value))
.filter(
and_(
Shout.authors.any(id=author_id),
Reaction.kind == ReactionKind.DISLIKE.value,
)
)
.count()
or 0
)
return shouts_likes - shouts_dislikes
async def load_author_with_stats(q):
q = add_author_stat_columns(q)
@@ -175,25 +195,25 @@ async def load_author_with_stats(q):
)
.count()
)
ratings_sum = (
session.query(
func.sum(
case((AuthorRating.plus == True, cast(1, Integer)),
else_=cast(-1, Integer))).label("rating")
)
.filter(AuthorRating.author == author.id)
.scalar()
likes_count = (
session.query(AuthorRating)
.filter(and_(AuthorRating.author == author.id, AuthorRating.plus == True))
.count()
)
author.stat["rating"] = ratings_sum or 0
author.stat["rating_shouts"] = count_author_shouts_rating(session, author.id)
author.stat["rating_comments"] = count_author_comments_rating(session, author.id)
author.stat["commented"] = comments_count
dislikes_count = (
session.query(AuthorRating)
.filter(and_(AuthorRating.author == author.id, AuthorRating.plus != True))
.count()
)
author.stat['rating'] = likes_count - dislikes_count
author.stat['rating_shouts'] = count_author_shouts_rating(session, author.id)
author.stat['rating_comments'] = count_author_comments_rating(session, author.id)
author.stat['commented'] = comments_count
return author
@query.field("get_author")
async def get_author(_, _info, slug="", author_id=None):
@query.field('get_author')
async def get_author(_, _info, slug='', author_id=None):
q = None
if slug or author_id:
if bool(slug):
@@ -204,38 +224,37 @@ async def get_author(_, _info, slug="", author_id=None):
return await load_author_with_stats(q)
@query.field("get_author_id")
@query.field('get_author_id')
async def get_author_id(_, _info, user: str):
with local_session() as session:
logger.info(f"getting author id for {user}")
q = select(Author).filter(Author.user == user)
return await load_author_with_stats(q)
logger.info(f'getting author id for {user}')
q = select(Author).filter(Author.user == user)
return await load_author_with_stats(q)
@query.field("load_authors_by")
@query.field('load_authors_by')
async def load_authors_by(_, _info, by, limit, offset):
q = select(Author)
q = add_author_stat_columns(q)
if by.get("slug"):
if by.get('slug'):
q = q.filter(Author.slug.ilike(f"%{by['slug']}%"))
elif by.get("name"):
elif by.get('name'):
q = q.filter(Author.name.ilike(f"%{by['name']}%"))
elif by.get("topic"):
q = q.join(ShoutAuthor).join(ShoutTopic).join(Topic).where(Topic.slug == by["topic"])
elif by.get('topic'):
q = q.join(ShoutAuthor).join(ShoutTopic).join(Topic).where(Topic.slug == by['topic'])
if by.get("last_seen"): # in unixtime
before = int(time.time()) - by["last_seen"]
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"]
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.created_at)).limit(limit).offset(offset)
q = q.order_by(by.get('order', Author.created_at)).limit(limit).offset(offset)
return await get_authors_from_query(q)
@query.field("get_author_followed")
async def get_author_followed(_, _info, slug="", user=None, author_id=None) -> List[Author]:
@query.field('get_author_followed')
async def get_author_followed(_, _info, slug='', user=None, author_id=None) -> List[Author]:
author_id_query = None
if slug:
author_id_query = select(Author.id).where(Author.slug == slug)
@@ -246,12 +265,12 @@ async def get_author_followed(_, _info, slug="", user=None, author_id=None) -> L
author_id = session.execute(author_id_query).scalar()
if author_id is None:
raise ValueError("Author not found")
raise ValueError('Author not found')
else:
return await followed_authors(author_id) # Author[]
@query.field("get_author_followers")
@query.field('get_author_followers')
async def get_author_followers(_, _info, slug) -> List[Author]:
q = select(Author)
q = add_author_stat_columns(q)
@@ -274,10 +293,10 @@ async def followed_authors(follower_id):
return await get_authors_from_query(q)
@mutation.field("rate_author")
@mutation.field('rate_author')
@login_required
async def rate_author(_, info, rated_slug, value):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
@@ -285,7 +304,12 @@ async def rate_author(_, info, rated_slug, value):
if rater and rated_author:
rating: AuthorRating = (
session.query(AuthorRating)
.filter(and_(AuthorRating.rater == rater.id, AuthorRating.author == rated_author.id))
.filter(
and_(
AuthorRating.rater == rater.id,
AuthorRating.author == rated_author.id,
)
)
.first()
)
if rating:
@@ -299,13 +323,13 @@ async def rate_author(_, info, rated_slug, value):
session.add(rating)
session.commit()
except Exception as err:
return {"error": err}
return {'error': err}
return {}
async def create_author(user_id: str, slug: str, name: str = ""):
async def create_author(user_id: str, slug: str, name: str = ''):
with local_session() as session:
new_author = Author(user=user_id, slug=slug, name=name)
session.add(new_author)
session.commit()
logger.info(f"author created by webhook {new_author.dict()}")
logger.info(f'author created by webhook {new_author.dict()}')

View File

@@ -6,10 +6,10 @@ from services.db import local_session
from services.schema import mutation
@mutation.field("accept_invite")
@mutation.field('accept_invite')
@login_required
async def accept_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
user_id = info.context['user_id']
# Check if the user exists
with local_session() as session:
@@ -26,19 +26,19 @@ async def accept_invite(_, info, invite_id: int):
session.delete(invite)
session.add(shout)
session.commit()
return {"success": True, "message": "Invite accepted"}
return {'success': True, 'message': 'Invite accepted'}
else:
return {"error": "Shout not found"}
return {'error': 'Shout not found'}
else:
return {"error": "Invalid invite or already accepted/rejected"}
return {'error': 'Invalid invite or already accepted/rejected'}
else:
return {"error": "User not found"}
return {'error': 'User not found'}
@mutation.field("reject_invite")
@mutation.field('reject_invite')
@login_required
async def reject_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
user_id = info.context['user_id']
# Check if the user exists
with local_session() as session:
@@ -50,17 +50,17 @@ async def reject_invite(_, info, invite_id: int):
# Delete the invite
session.delete(invite)
session.commit()
return {"success": True, "message": "Invite rejected"}
return {'success': True, 'message': 'Invite rejected'}
else:
return {"error": "Invalid invite or already accepted/rejected"}
return {'error': 'Invalid invite or already accepted/rejected'}
else:
return {"error": "User not found"}
return {'error': 'User not found'}
@mutation.field("create_invite")
@mutation.field('create_invite')
@login_required
async def create_invite(_, info, slug: str = "", author_id: int = 0):
user_id = info.context["user_id"]
async def create_invite(_, info, slug: str = '', author_id: int = 0):
user_id = info.context['user_id']
# Check if the inviter is the owner of the shout
with local_session() as session:
@@ -82,7 +82,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
.first()
)
if existing_invite:
return {"error": "Invite already sent"}
return {'error': 'Invite already sent'}
# Create a new invite
new_invite = Invite(
@@ -91,17 +91,17 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
session.add(new_invite)
session.commit()
return {"error": None, "invite": new_invite}
return {'error': None, 'invite': new_invite}
else:
return {"error": "Invalid author"}
return {'error': 'Invalid author'}
else:
return {"error": "Access denied"}
return {'error': 'Access denied'}
@mutation.field("remove_author")
@mutation.field('remove_author')
@login_required
async def remove_author(_, info, slug: str = "", author_id: int = 0):
user_id = info.context["user_id"]
async def remove_author(_, info, slug: str = '', author_id: int = 0):
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
@@ -111,13 +111,13 @@ async def remove_author(_, info, slug: str = "", author_id: int = 0):
shout.authors = [author for author in shout.authors if author.id != author_id]
session.commit()
return {}
return {"error": "Access denied"}
return {'error': 'Access denied'}
@mutation.field("remove_invite")
@mutation.field('remove_invite')
@login_required
async def remove_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
user_id = info.context['user_id']
# Check if the user exists
with local_session() as session:
@@ -135,6 +135,6 @@ async def remove_invite(_, info, invite_id: int):
session.commit()
return {}
else:
return {"error": "Invalid invite or already accepted/rejected"}
return {'error': 'Invalid invite or already accepted/rejected'}
else:
return {"error": "Author not found"}
return {'error': 'Author not found'}

View File

@@ -1,4 +1,6 @@
from sqlalchemy import and_, distinct, func, literal, select
import logging
from sqlalchemy import and_, distinct, func, select
from sqlalchemy.orm import aliased
from orm.author import Author
@@ -8,16 +10,19 @@ from services.db import local_session
from services.schema import query
logger = logging.getLogger('\t[resolvers.community]\t')
logger.setLevel(logging.DEBUG)
def add_community_stat_columns(q):
community_followers = aliased(CommunityAuthor)
shout_community_aliased = aliased(ShoutCommunity)
q = q.outerjoin(shout_community_aliased).add_columns(
func.count(distinct(shout_community_aliased.shout)).label("shouts_stat")
func.count(distinct(shout_community_aliased.shout)).label('shouts_stat')
)
q = q.outerjoin(community_followers, community_followers.author == Author.id).add_columns(
func.count(distinct(community_followers.follower)).label("followers_stat")
func.count(distinct(community_followers.follower)).label('followers_stat')
)
q = q.group_by(Author.id)
@@ -30,8 +35,8 @@ def get_communities_from_query(q):
with local_session() as session:
for [c, shouts_stat, followers_stat] in session.execute(q):
c.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
'shouts': shouts_stat,
'followers': followers_stat,
# "commented": commented_stat,
}
ccc.append(c)
@@ -69,8 +74,8 @@ def community_follow(follower_id, slug):
session.add(cf)
session.commit()
return True
except Exception:
pass
except Exception as ex:
logger.debug(ex)
return False
@@ -90,7 +95,7 @@ def community_unfollow(follower_id, slug):
return False
@query.field("get_communities_all")
@query.field('get_communities_all')
async def get_communities_all(_, _info):
q = select(Author)
q = add_community_stat_columns(q)
@@ -98,7 +103,7 @@ async def get_communities_all(_, _info):
return get_communities_from_query(q)
@query.field("get_community")
@query.field('get_community')
async def get_community(_, _info, slug):
q = select(Community).where(Community.slug == slug)
q = add_community_stat_columns(q)

View File

@@ -13,10 +13,10 @@ from services.notify import notify_shout
from services.schema import mutation, query
@query.field("get_shouts_drafts")
@query.field('get_shouts_drafts')
@login_required
async def get_shouts_drafts(_, info):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
@@ -36,29 +36,29 @@ async def get_shouts_drafts(_, info):
return shouts
@mutation.field("create_shout")
@mutation.field('create_shout')
@login_required
async def create_shout(_, info, inp):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
shout_dict = None
if author:
current_time = int(time.time())
slug = inp.get("slug") or f"draft-{current_time}"
slug = inp.get('slug') or f'draft-{current_time}'
shout_dict = {
"title": inp.get("title", ""),
"subtitle": inp.get("subtitle", ""),
"lead": inp.get("lead", ""),
"description": inp.get("description", ""),
"body": inp.get("body", ""),
"layout": inp.get("layout", "article"),
"created_by": author.id,
"authors": [],
"slug": slug,
"topics": inp.get("topics", []),
"visibility": ShoutVisibility.AUTHORS.value,
"created_at": current_time, # Set created_at as Unix timestamp
'title': inp.get('title', ''),
'subtitle': inp.get('subtitle', ''),
'lead': inp.get('lead', ''),
'description': inp.get('description', ''),
'body': inp.get('body', ''),
'layout': inp.get('layout', 'article'),
'created_by': author.id,
'authors': [],
'slug': slug,
'topics': inp.get('topics', []),
'visibility': ShoutVisibility.AUTHORS.value,
'created_at': current_time, # Set created_at as Unix timestamp
}
new_shout = Shout(**shout_dict)
@@ -72,21 +72,23 @@ async def create_shout(_, info, inp):
sa = ShoutAuthor(shout=shout.id, author=author.id)
session.add(sa)
topics = session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all()
topics = session.query(Topic).filter(Topic.slug.in_(inp.get('topics', []))).all()
for topic in topics:
t = ShoutTopic(topic=topic.id, shout=shout.id)
session.add(t)
reactions_follow(author.id, shout.id, True)
await notify_shout(shout_dict, "create")
return {"shout": shout_dict}
await notify_shout(shout_dict, 'create')
return {'shout': shout_dict}
@mutation.field("update_shout")
@mutation.field('update_shout')
@login_required
async def update_shout(_, info, shout_id, shout_input=None, publish=False):
user_id = info.context["user_id"]
async def update_shout( # noqa: C901
_, info, shout_id, shout_input=None, publish=False
):
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
shout_dict = None
@@ -103,16 +105,16 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
.first()
)
if not shout:
return {"error": "shout not found"}
return {'error': 'shout not found'}
if shout.created_by is not author.id and author.id not in shout.authors:
return {"error": "access denied"}
return {'error': 'access denied'}
if shout_input is not None:
topics_input = shout_input["topics"]
del shout_input["topics"]
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]
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"]
del new_topic['id']
created_new_topic = Topic(**new_topic)
session.add(created_new_topic)
new_topics_to_link.append(created_new_topic)
@@ -121,11 +123,11 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
for new_topic_to_link in new_topics_to_link:
created_unlinked_topic = ShoutTopic(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 if topic_input.get("id", 0) > 0]
existing_topics_input = [topic_input for topic_input in topics_input if topic_input.get('id', 0) > 0]
existing_topic_to_link_ids = [
existing_topic_input["id"]
existing_topic_input['id']
for existing_topic_input in existing_topics_input
if existing_topic_input["id"] not in [topic.id for topic in shout.topics]
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(shout=shout.id, topic=existing_topic_to_link_id)
@@ -133,7 +135,7 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
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]
if topic.id not in [topic_input['id'] for topic_input in existing_topics_input]
]
shout_topics_to_remove = session.query(ShoutTopic).filter(
and_(
@@ -145,68 +147,68 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
session.delete(shout_topic_to_remove)
# Replace datetime with Unix timestamp
shout_input["updated_at"] = current_time # Set updated_at as Unix timestamp
shout_input['updated_at'] = current_time # Set updated_at as Unix timestamp
Shout.update(shout, shout_input)
session.add(shout)
# main topic
if "main_topic" in shout_input:
if 'main_topic' in shout_input:
old_main_topic = (
session.query(ShoutTopic)
.filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.main == True))
.first()
)
main_topic = session.query(Topic).filter(Topic.slug == shout_input["main_topic"]).first()
main_topic = session.query(Topic).filter(Topic.slug == shout_input['main_topic']).first()
if isinstance(main_topic, Topic):
new_main_topic = (
session.query(ShoutTopic)
.filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.topic == main_topic.id))
.filter(
and_(
ShoutTopic.shout == shout.id,
ShoutTopic.topic == main_topic.id,
)
)
.first()
)
if isinstance(old_main_topic, ShoutTopic) and isinstance(new_main_topic, ShoutTopic) \
and old_main_topic is not new_main_topic:
ShoutTopic.update(old_main_topic, {"main": False})
if (
isinstance(old_main_topic, ShoutTopic)
and isinstance(new_main_topic, ShoutTopic)
and old_main_topic is not new_main_topic
):
ShoutTopic.update(old_main_topic, {'main': False})
session.add(old_main_topic)
ShoutTopic.update(new_main_topic, {"main": True})
ShoutTopic.update(new_main_topic, {'main': True})
session.add(new_main_topic)
session.commit()
if publish:
if shout.visibility is ShoutVisibility.AUTHORS.value:
shout_dict = shout.dict()
shout_dict["visibility"] = ShoutVisibility.COMMUNITY.value
shout_dict["published_at"] = current_time # Set published_at as Unix timestamp
Shout.update(shout, shout_dict)
session.add(shout)
await notify_shout(shout.dict(), "public")
shout_dict = shout.dict()
session.commit()
if not publish:
await notify_shout(shout_dict, "update")
return {"shout": shout_dict}
await notify_shout(shout_dict, 'update')
return {'shout': shout_dict}
@mutation.field("delete_shout")
@mutation.field('delete_shout')
@login_required
async def delete_shout(_, info, shout_id):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.id == user_id).first()
shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout:
return {"error": "invalid shout id"}
return {'error': 'invalid shout id'}
if isinstance(author, Author) and isinstance(shout, Shout):
# TODO: add editor role allowed here
if shout.created_by is not author.id and author.id not in shout.authors:
return {"error": "access denied"}
return {'error': 'access denied'}
for author_id in shout.authors:
reactions_unfollow(author_id, shout_id)
shout_dict = shout.dict()
shout_dict["deleted_at"] = int(time.time())
shout_dict['deleted_at'] = int(time.time())
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()
await notify_shout(shout_dict, "delete")
await notify_shout(shout_dict, 'delete')
return {}

View File

@@ -1,9 +1,5 @@
import time
from typing import List
import logging
from sqlalchemy import select
from sqlalchemy.orm import aliased
from typing import List
from orm.author import Author, AuthorFollower
from orm.community import Community
@@ -22,84 +18,84 @@ from services.schema import mutation, query
logging.basicConfig()
logger = logging.getLogger("\t[resolvers.reaction]\t")
logger = logging.getLogger('\t[resolvers.reaction]\t')
logger.setLevel(logging.DEBUG)
@mutation.field("follow")
@mutation.field('follow')
@login_required
async def follow(_, info, what, slug):
try:
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
actor = session.query(Author).filter(Author.user == user_id).first()
if actor:
follower_id = actor.id
if what == "AUTHOR":
if what == 'AUTHOR':
if author_follow(follower_id, slug):
result = FollowingResult("NEW", "author", slug)
await FollowingManager.push("author", result)
result = FollowingResult('NEW', 'author', slug)
await FollowingManager.push('author', result)
author = session.query(Author.id).where(Author.slug == slug).one()
follower = session.query(Author).where(Author.id == follower_id).one()
await notify_follower(follower.dict(), author.id)
elif what == "TOPIC":
elif what == 'TOPIC':
if topic_follow(follower_id, slug):
result = FollowingResult("NEW", "topic", slug)
await FollowingManager.push("topic", result)
elif what == "COMMUNITY":
result = FollowingResult('NEW', 'topic', slug)
await FollowingManager.push('topic', result)
elif what == 'COMMUNITY':
if community_follow(follower_id, slug):
result = FollowingResult("NEW", "community", slug)
await FollowingManager.push("community", result)
elif what == "REACTIONS":
result = FollowingResult('NEW', 'community', slug)
await FollowingManager.push('community', result)
elif what == 'REACTIONS':
if reactions_follow(follower_id, slug):
result = FollowingResult("NEW", "shout", slug)
await FollowingManager.push("shout", result)
result = FollowingResult('NEW', 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e:
logger.debug(info, what, slug)
logger.error(e)
return {"error": str(e)}
return {'error': str(e)}
return {}
@mutation.field("unfollow")
@mutation.field('unfollow')
@login_required
async def unfollow(_, info, what, slug):
user_id = info.context["user_id"]
user_id = info.context['user_id']
try:
with local_session() as session:
actor = session.query(Author).filter(Author.user == user_id).first()
if actor:
follower_id = actor.id
if what == "AUTHOR":
if what == 'AUTHOR':
if author_unfollow(follower_id, slug):
result = FollowingResult("DELETED", "author", slug)
await FollowingManager.push("author", result)
result = FollowingResult('DELETED', 'author', slug)
await FollowingManager.push('author', result)
author = session.query(Author.id).where(Author.slug == slug).one()
follower = session.query(Author).where(Author.id == follower_id).one()
await notify_follower(follower.dict(), author.id, "unfollow")
elif what == "TOPIC":
await notify_follower(follower.dict(), author.id, 'unfollow')
elif what == 'TOPIC':
if topic_unfollow(follower_id, slug):
result = FollowingResult("DELETED", "topic", slug)
await FollowingManager.push("topic", result)
elif what == "COMMUNITY":
result = FollowingResult('DELETED', 'topic', slug)
await FollowingManager.push('topic', result)
elif what == 'COMMUNITY':
if community_unfollow(follower_id, slug):
result = FollowingResult("DELETED", "community", slug)
await FollowingManager.push("community", result)
elif what == "REACTIONS":
result = FollowingResult('DELETED', 'community', slug)
await FollowingManager.push('community', result)
elif what == 'REACTIONS':
if reactions_unfollow(follower_id, slug):
result = FollowingResult("DELETED", "shout", slug)
await FollowingManager.push("shout", result)
result = FollowingResult('DELETED', 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e:
return {"error": str(e)}
return {'error': str(e)}
return {}
@query.field("get_my_followed")
@query.field('get_my_followed')
@login_required
async def get_my_followed(_, info):
user_id = info.context["user_id"]
user_id = info.context['user_id']
topics = []
authors = []
communities = []
@@ -114,10 +110,7 @@ async def get_my_followed(_, info):
.filter(AuthorFollower.author == Author.id)
)
topics_query = (
session.query(Topic)
.join(TopicFollower, TopicFollower.follower == author_id)
)
topics_query = session.query(Topic).join(TopicFollower, TopicFollower.follower == author_id)
for [author] in session.execute(authors_query):
authors.append(author)
@@ -127,12 +120,11 @@ async def get_my_followed(_, info):
communities = session.query(Community).all()
return {"topics": topics, "authors": authors, "communities": communities}
return {'topics': topics, 'authors': authors, 'communities': communities}
@query.field("get_shout_followers")
def get_shout_followers(_, _info, slug: str = "", shout_id: int | None = None) -> List[Author]:
@query.field('get_shout_followers')
def get_shout_followers(_, _info, slug: str = '', shout_id: int | None = None) -> List[Author]:
followers = []
with local_session() as session:
shout = None

View File

@@ -1,32 +1,36 @@
import logging
import time
from typing import List
import logging
from sqlalchemy import and_, asc, case, desc, func, select, text, or_
from sqlalchemy import and_, case, desc, func, select
from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql import union
from orm.author import Author
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutReactionsFollower, ShoutVisibility
from services.auth import login_required, add_user_role
from services.auth import add_user_role, login_required
from services.db import local_session
from services.notify import notify_reaction
from services.schema import mutation, query
from services.viewed import ViewedStorage
logging.basicConfig()
logger = logging.getLogger("\t[resolvers.reaction]\t")
logger = logging.getLogger('\t[resolvers.reaction]\t')
logger.setLevel(logging.DEBUG)
def add_stat_columns(q, aliased_reaction):
def add_stat_columns(q, aliased_reaction):
q = q.outerjoin(aliased_reaction).add_columns(
func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT.value, 1), else_=0)).label("comments_stat"),
func.sum(case((aliased_reaction.kind == ReactionKind.LIKE.value, 1), else_=0)).label("likes_stat"),
func.sum(case((aliased_reaction.kind == ReactionKind.DISLIKE.value, 1), else_=0)).label("dislikes_stat"),
func.max(case((aliased_reaction.kind != ReactionKind.COMMENT.value, None),else_=aliased_reaction.created_at)).label("last_comment"),
func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT.value, 1), else_=0)).label('comments_stat'),
func.sum(case((aliased_reaction.kind == ReactionKind.LIKE.value, 1), else_=0)).label('likes_stat'),
func.sum(case((aliased_reaction.kind == ReactionKind.DISLIKE.value, 1), else_=0)).label('dislikes_stat'),
func.max(
case(
(aliased_reaction.kind != ReactionKind.COMMENT.value, None),
else_=aliased_reaction.created_at,
)
).label('last_comment'),
)
return q
@@ -77,8 +81,8 @@ def reactions_unfollow(author_id, shout_id: int):
session.delete(following)
session.commit()
return True
except Exception:
pass
except Exception as ex:
logger.debug(ex)
return False
@@ -92,6 +96,7 @@ def is_published_author(session, author_id):
> 0
)
def is_negative(x):
return x in [
ReactionKind.ACCEPT.value,
@@ -99,6 +104,7 @@ def is_negative(x):
ReactionKind.PROOF.value,
]
def is_positive(x):
return x in [
ReactionKind.ACCEPT.value,
@@ -106,6 +112,7 @@ def is_positive(x):
ReactionKind.PROOF.value,
]
def check_to_publish(session, approver_id, reaction):
"""set shout to public if publicated approvers amount > 4"""
if not reaction.reply_to and is_positive(reaction.kind):
@@ -142,7 +149,7 @@ async def set_published(session, shout_id, approver_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
s.published_at = int(time.time())
s.published_by = approver_id
Shout.update(s, {"visibility": ShoutVisibility.PUBLIC.value})
Shout.update(s, {'visibility': ShoutVisibility.PUBLIC.value})
author = session.query(Author).filter(Author.id == s.created_by).first()
if author:
await add_user_role(str(author.user))
@@ -152,7 +159,7 @@ async def set_published(session, shout_id, approver_id):
def set_hidden(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
Shout.update(s, {"visibility": ShoutVisibility.COMMUNITY.value})
Shout.update(s, {'visibility': ShoutVisibility.COMMUNITY.value})
session.add(s)
session.commit()
@@ -164,19 +171,19 @@ async def _create_reaction(session, shout, author, reaction):
session.commit()
# Proposal accepting logic
if rdict.get("reply_to"):
if r.kind in ["LIKE", "APPROVE"] and author.id in shout.authors:
if rdict.get('reply_to'):
if r.kind in ['LIKE', 'APPROVE'] and author.id in shout.authors:
replied_reaction = session.query(Reaction).filter(Reaction.id == r.reply_to).first()
if replied_reaction:
if replied_reaction.kind is ReactionKind.PROPOSE.value:
if replied_reaction.range:
old_body = shout.body
start, end = replied_reaction.range.split(":")
start, end = replied_reaction.range.split(':')
start = int(start)
end = int(end)
new_body = old_body[:start] + replied_reaction.body + old_body[end:]
shout_dict = shout.dict()
shout_dict["body"] = new_body
shout_dict['body'] = new_body
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()
@@ -188,43 +195,44 @@ async def _create_reaction(session, shout, author, reaction):
await set_published(session, shout.id, author.id)
# Reactions auto-following
reactions_follow(author.id, reaction["shout"], True)
reactions_follow(author.id, reaction['shout'], True)
rdict["shout"] = shout.dict()
rdict["created_by"] = author.dict()
rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0}
rdict['shout'] = shout.dict()
rdict['created_by'] = author.dict()
rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0}
# Notifications call
await notify_reaction(rdict, "create")
await notify_reaction(rdict, 'create')
return rdict
@mutation.field("create_reaction")
@mutation.field('create_reaction')
@login_required
async def create_reaction(_, info, reaction):
user_id = info.context["user_id"]
user_id = info.context['user_id']
shout_id = reaction.get("shout")
shout_id = reaction.get('shout')
if not shout_id:
return {"error": "Shout ID is required to create a reaction."}
return {'error': 'Shout ID is required to create a reaction.'}
try:
with local_session() as session:
shout = session.query(Shout).filter(Shout.id == shout_id).one()
author = session.query(Author).filter(Author.user == user_id).first()
if shout and author:
reaction["created_by"] = author.id
kind = reaction.get("kind")
reaction['created_by'] = author.id
kind = reaction.get('kind')
shout_id = shout.id
if not kind and reaction.get("body"):
if not kind and reaction.get('body'):
kind = ReactionKind.COMMENT.value
if not kind:
return { "error": "cannot create reaction with this kind"}
return {'error': 'cannot create reaction with this kind'}
if kind in ["LIKE", "DISLIKE", "AGREE", "DISAGREE"]:
if kind in ['LIKE', 'DISLIKE', 'AGREE', 'DISAGREE']:
same_reaction = (
session.query(Reaction)
.filter(
@@ -232,51 +240,51 @@ async def create_reaction(_, info, reaction):
Reaction.shout == shout_id,
Reaction.created_by == author.id,
Reaction.kind == kind,
Reaction.reply_to == reaction.get("reply_to"),
Reaction.reply_to == reaction.get('reply_to'),
)
)
.first()
)
if same_reaction is not None:
return {"error": "You can't like or dislike same thing twice"}
return {'error': "You can't like or dislike same thing twice"}
opposite_reaction_kind = (
ReactionKind.DISLIKE.value
if reaction["kind"] == ReactionKind.LIKE.value
if reaction['kind'] == ReactionKind.LIKE.value
else ReactionKind.LIKE.value
)
opposite_reaction = (
session.query(Reaction)
.filter(
and_(
Reaction.shout == reaction["shout"],
Reaction.shout == reaction['shout'],
Reaction.created_by == author.id,
Reaction.kind == opposite_reaction_kind,
Reaction.reply_to == reaction.get("reply_to"),
Reaction.reply_to == reaction.get('reply_to'),
)
)
.first()
)
if opposite_reaction is not None:
return {"error": "Remove opposite vote first"}
return {'error': 'Remove opposite vote first'}
else:
rdict = await _create_reaction(session, shout, author, reaction)
return {"reaction": rdict}
return {'reaction': rdict}
except Exception as e:
import traceback
traceback.print_exc()
logger.error(f"{type(e).__name__}: {e}")
logger.error(f'{type(e).__name__}: {e}')
return {"error": "Cannot create reaction."}
return {'error': 'Cannot create reaction.'}
@mutation.field("update_reaction")
@mutation.field('update_reaction')
@login_required
async def update_reaction(_, info, rid, reaction):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
q = select(Reaction).filter(Reaction.id == rid)
aliased_reaction = aliased(Reaction)
@@ -286,83 +294,84 @@ async def update_reaction(_, info, rid, reaction):
[r, commented_stat, likes_stat, dislikes_stat, _l] = session.execute(q).unique().one()
if not r:
return {"error": "invalid reaction id"}
return {'error': 'invalid reaction id'}
author = session.query(Author).filter(Author.user == user_id).first()
if author:
if r.created_by != author.id:
return {"error": "access denied"}
body = reaction.get("body")
return {'error': 'access denied'}
body = reaction.get('body')
if body:
r.body = body
r.updated_at = int(time.time())
if r.kind != reaction["kind"]:
if r.kind != reaction['kind']:
# TODO: change mind detection can be here
pass
session.commit()
r.stat = {
"commented": commented_stat,
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
}
await notify_reaction(r.dict(), "update")
await notify_reaction(r.dict(), 'update')
return {"reaction": r}
return {'reaction': r}
else:
return {"error": "not authorized"}
return {"error": "cannot create reaction"}
return {'error': 'not authorized'}
return {'error': 'cannot create reaction'}
@mutation.field("delete_reaction")
@mutation.field('delete_reaction')
@login_required
async def delete_reaction(_, info, reaction_id):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
r = session.query(Reaction).filter(Reaction.id == reaction_id).first()
if not r:
return {"error": "invalid reaction id"}
return {'error': 'invalid reaction id'}
author = session.query(Author).filter(Author.user == user_id).first()
if author:
if r.created_by is author.id:
return {"error": "access denied"}
return {'error': 'access denied'}
if r.kind in [ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]:
session.delete(r)
session.commit()
await notify_reaction(r.dict(), "delete")
await notify_reaction(r.dict(), 'delete')
else:
return {"error": "access denied"}
return {'error': 'access denied'}
return {}
def apply_reaction_filters(by, q):
if by.get("shout"):
q = q.filter(Shout.slug == by["shout"])
if by.get('shout'):
q = q.filter(Shout.slug == by['shout'])
elif by.get("shouts"):
q = q.filter(Shout.slug.in_(by["shouts"]))
elif by.get('shouts'):
q = q.filter(Shout.slug.in_(by['shouts']))
if by.get("created_by"):
q = q.filter(Author.id == by["created_by"])
if by.get('created_by'):
q = q.filter(Author.id == by['created_by'])
if by.get("topic"):
q = q.filter(Shout.topics.contains(by["topic"]))
if by.get('topic'):
q = q.filter(Shout.topics.contains(by['topic']))
if by.get("comment"):
if by.get('comment'):
q = q.filter(func.length(Reaction.body) > 0)
# NOTE: not using ElasticSearch here
by_search = by.get("search", "")
by_search = by.get('search', '')
if len(by_search) > 2:
q = q.filter(Reaction.body.ilike(f"%{by_search}%"))
q = q.filter(Reaction.body.ilike(f'%{by_search}%'))
if by.get("after"):
after = int(by["after"])
if by.get('after'):
after = int(by['after'])
q = q.filter(Reaction.created_at > after)
return q
@query.field("load_reactions_by")
@query.field('load_reactions_by')
async def load_reactions_by(_, info, by, limit=50, offset=0):
"""
:param info: graphql meta
@@ -399,7 +408,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
q = q.group_by(Reaction.id, Author.id, Shout.id, aliased_reaction.id)
# order by
q = q.order_by(desc("created_at"))
q = q.order_by(desc('created_at'))
# pagination
q = q.limit(limit).offset(offset)
@@ -414,38 +423,45 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
commented_stat,
likes_stat,
dislikes_stat,
_last_comment
_last_comment,
] in result_rows:
reaction.created_by = author
reaction.shout = shout
reaction.stat = {
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
"commented": commented_stat
}
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
'commented': commented_stat,
}
reactions.append(reaction)
# sort if by stat is present
stat_sort = by.get("stat")
stat_sort = by.get('stat')
if stat_sort:
reactions = sorted(reactions, key=lambda r: r.stat.get(stat_sort) or r.created_at, reverse=stat_sort.startswith("-"))
reactions = sorted(
reactions,
key=lambda r: r.stat.get(stat_sort) or r.created_at,
reverse=stat_sort.startswith('-'),
)
return reactions
async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[Shout]:
shouts: List[Shout] = []
with local_session() as session:
author = session.query(Author).filter(Author.id == follower_id).first()
if author:
# Shouts where follower is the author
q1 = select(Shout).outerjoin(
Reaction, and_(Reaction.shout_id == Shout.id, Reaction.created_by == follower_id)
).outerjoin(
Author, Shout.authors.any(id=follower_id)
).options(
joinedload(Shout.reactions),
joinedload(Shout.authors)
q1 = (
select(Shout)
.outerjoin(
Reaction,
and_(
Reaction.shout_id == Shout.id,
Reaction.created_by == follower_id,
),
)
.outerjoin(Author, Shout.authors.any(id=follower_id))
.options(joinedload(Shout.reactions), joinedload(Shout.authors))
)
q1 = add_stat_columns(q1, aliased(Reaction))
q1 = q1.filter(Author.id == follower_id).group_by(Shout.id)
@@ -454,17 +470,14 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S
q2 = (
select(Shout)
.join(Reaction, Reaction.shout_id == Shout.id)
.options(
joinedload(Shout.reactions),
joinedload(Shout.authors)
)
.options(joinedload(Shout.reactions), joinedload(Shout.authors))
.filter(Reaction.created_by == follower_id)
.group_by(Shout.id)
)
q2 = add_stat_columns(q2, aliased(Reaction))
# Sort shouts by the `last_comment` field
combined_query = union(q1, q2).order_by(desc("last_comment")).limit(limit).offset(offset)
combined_query = union(q1, q2).order_by(desc('last_comment')).limit(limit).offset(offset)
results = session.execute(combined_query).scalars()
with local_session() as session:
for [
@@ -472,27 +485,28 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S
commented_stat,
likes_stat,
dislikes_stat,
last_comment
last_comment,
] in results:
shout.stat = {
"viewed": await ViewedStorage.get_shout(shout.slug),
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
"commented": commented_stat,
"last_comment": last_comment
}
'viewed': await ViewedStorage.get_shout(shout.slug),
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
'commented': commented_stat,
'last_comment': last_comment,
}
shouts.append(shout)
return shouts
@query.field("load_shouts_followed")
@query.field('load_shouts_followed')
@login_required
async def load_shouts_followed(_, info, limit=50, offset=0) -> List[Shout]:
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
try:
author_id: int = author.dict()["id"]
author_id: int = author.dict()['id']
shouts = await reacted_shouts_updates(author_id, limit, offset)
return shouts
except Exception as error:

View File

@@ -1,4 +1,4 @@
from sqlalchemy import bindparam, distinct, or_, literal
from sqlalchemy import bindparam, distinct, or_
from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select
from starlette.exceptions import HTTPException
@@ -7,32 +7,32 @@ from orm.author import Author, AuthorFollower
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
from orm.topic import Topic, TopicFollower
from resolvers.reaction import add_stat_columns
from resolvers.topic import get_random_topic
from services.auth import login_required
from services.db import local_session
from services.schema import query
from services.search import SearchService
from services.viewed import ViewedStorage
from resolvers.topic import get_random_topic
from resolvers.reaction import add_stat_columns
def apply_filters(q, filters, author_id=None): # noqa: C901
if filters.get("reacted") and author_id:
def apply_filters(q, filters, author_id=None):
if filters.get('reacted') and author_id:
q.join(Reaction, Reaction.created_by == author_id)
by_published = filters.get("published")
by_published = filters.get('published')
if by_published:
q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC.value)
by_layouts = filters.get("layouts")
by_layouts = filters.get('layouts')
if by_layouts:
q = q.filter(Shout.layout.in_(by_layouts))
by_author = filters.get("author")
by_author = filters.get('author')
if by_author:
q = q.filter(Shout.authors.any(slug=by_author))
by_topic = filters.get("topic")
by_topic = filters.get('topic')
if by_topic:
q = q.filter(Shout.topics.any(slug=by_topic))
by_after = filters.get("after")
by_after = filters.get('after')
if by_after:
ts = int(by_after)
q = q.filter(Shout.created_at > ts)
@@ -40,7 +40,7 @@ def apply_filters(q, filters, author_id=None): # noqa: C901
return q
@query.field("get_shout")
@query.field('get_shout')
async def get_shout(_, _info, slug=None, shout_id=None):
with local_session() as session:
q = select(Shout).options(
@@ -61,13 +61,19 @@ async def get_shout(_, _info, slug=None, shout_id=None):
try:
results = session.execute(q).first()
if results:
[shout, commented_stat, likes_stat, dislikes_stat, _last_comment] = results
[
shout,
commented_stat,
likes_stat,
dislikes_stat,
_last_comment,
] = results
shout.stat = {
"viewed": await ViewedStorage.get_shout(shout.slug),
'viewed': await ViewedStorage.get_shout(shout.slug),
# "reacted": reacted_stat,
"commented": commented_stat,
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
}
for author_caption in session.query(ShoutAuthor).join(Shout).where(Shout.slug == slug):
@@ -78,7 +84,11 @@ async def get_shout(_, _info, slug=None, shout_id=None):
session.query(Topic.slug)
.join(
ShoutTopic,
and_(ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True),
and_(
ShoutTopic.topic == Topic.id,
ShoutTopic.shout == shout.id,
ShoutTopic.main == True,
),
)
.first()
)
@@ -87,10 +97,10 @@ async def get_shout(_, _info, slug=None, shout_id=None):
shout.main_topic = main_topic[0]
return shout
except Exception:
raise HTTPException(status_code=404, detail=f"shout {slug or shout_id} not found")
raise HTTPException(status_code=404, detail=f'shout {slug or shout_id} not found')
@query.field("load_shouts_by")
@query.field('load_shouts_by')
async def load_shouts_by(_, _info, options):
"""
:param options: {
@@ -126,31 +136,39 @@ async def load_shouts_by(_, _info, options):
q = add_stat_columns(q, aliased_reaction)
# filters
q = apply_filters(q, options.get("filters", {}))
q = apply_filters(q, options.get('filters', {}))
# group
q = q.group_by(Shout.id)
# order
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)
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)
q = q.order_by(nulls_last(query_order_by))
# limit offset
offset = options.get("offset", 0)
limit = options.get("limit", 10)
offset = options.get('offset', 0)
limit = options.get('limit', 10)
q = q.limit(limit).offset(offset)
shouts = []
with local_session() as session:
for [shout, commented_stat, likes_stat, dislikes_stat, _last_comment] in session.execute(q).unique():
for [
shout,
commented_stat,
likes_stat,
dislikes_stat,
_last_comment,
] in session.execute(q).unique():
main_topic = (
session.query(Topic.slug)
.join(
ShoutTopic,
and_(
ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True
), # noqa: E712
ShoutTopic.topic == Topic.id,
ShoutTopic.shout == shout.id,
ShoutTopic.main == True,
),
)
.first()
)
@@ -158,19 +176,19 @@ async def load_shouts_by(_, _info, options):
if main_topic:
shout.main_topic = main_topic[0]
shout.stat = {
"viewed": await ViewedStorage.get_shout(shout.slug),
"commented": commented_stat,
"rating": int(likes_stat) - int(dislikes_stat),
'viewed': await ViewedStorage.get_shout(shout.slug),
'commented': commented_stat,
'rating': int(likes_stat) - int(dislikes_stat),
}
shouts.append(shout)
return shouts
@query.field("load_shouts_drafts")
@query.field('load_shouts_drafts')
@login_required
async def load_shouts_drafts(_, info):
user_id = info.context["user_id"]
user_id = info.context['user_id']
q = (
select(Shout)
@@ -193,7 +211,11 @@ async def load_shouts_drafts(_, info):
session.query(Topic.slug)
.join(
ShoutTopic,
and_(ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True),
and_(
ShoutTopic.topic == Topic.id,
ShoutTopic.shout == shout.id,
ShoutTopic.main == True,
),
)
.first()
)
@@ -205,10 +227,10 @@ async def load_shouts_drafts(_, info):
return shouts
@query.field("load_shouts_feed")
@query.field('load_shouts_feed')
@login_required
async def load_shouts_feed(_, info, options):
user_id = info.context["user_id"]
user_id = info.context['user_id']
shouts = []
with local_session() as session:
@@ -221,7 +243,9 @@ async def load_shouts_feed(_, info, options):
select(Shout.id)
.where(Shout.id == ShoutAuthor.shout)
.where(Shout.id == ShoutTopic.shout)
.where((ShoutAuthor.author.in_(reader_followed_authors)) | (ShoutTopic.topic.in_(reader_followed_topics)))
.where(
(ShoutAuthor.author.in_(reader_followed_authors)) | (ShoutTopic.topic.in_(reader_followed_topics))
)
)
q = (
@@ -230,18 +254,24 @@ async def load_shouts_feed(_, info, options):
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None), Shout.id.in_(subquery)))
.where(
and_(
Shout.published_at.is_not(None),
Shout.deleted_at.is_(None),
Shout.id.in_(subquery),
)
)
)
aliased_reaction = aliased(Reaction)
q = add_stat_columns(q, aliased_reaction)
q = apply_filters(q, options.get("filters", {}), reader.id)
q = apply_filters(q, options.get('filters', {}), reader.id)
order_by = options.get("order_by", Shout.published_at)
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)
offset = options.get("offset", 0)
limit = options.get("limit", 10)
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
offset = options.get('offset', 0)
limit = options.get('limit', 10)
q = q.group_by(Shout.id).order_by(nulls_last(query_order_by)).limit(limit).offset(offset)
@@ -252,7 +282,11 @@ async def load_shouts_feed(_, info, options):
session.query(Topic.slug)
.join(
ShoutTopic,
and_(ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True),
and_(
ShoutTopic.topic == Topic.id,
ShoutTopic.shout == shout.id,
ShoutTopic.main == True,
),
)
.first()
)
@@ -260,20 +294,20 @@ async def load_shouts_feed(_, info, options):
if main_topic:
shout.main_topic = main_topic[0]
shout.stat = {
"viewed": await ViewedStorage.get_shout(shout.slug),
"reacted": reacted_stat,
"commented": commented_stat,
'viewed': await ViewedStorage.get_shout(shout.slug),
'reacted': reacted_stat,
'commented': commented_stat,
}
shouts.append(shout)
return shouts
@query.field("load_shouts_search")
@query.field('load_shouts_search')
async def load_shouts_search(_, _info, text, limit=50, offset=0):
if text and len(text) > 2:
results = await SearchService.search(text, limit, offset)
results_dict = {r["slug"]: r for r in results}
results_dict = {r['slug']: r for r in results}
# print(results_dict)
q = (
@@ -289,13 +323,13 @@ async def load_shouts_search(_, _info, text, limit=50, offset=0):
with local_session() as session:
results = list(session.execute(q).unique())
# print(results)
print(f"[resolvers.reader] searched, preparing {len(results)} results")
print(f'[resolvers.reader] searched, preparing {len(results)} results')
for x in results:
shout = x[0]
shout_slug = shout.dict().get("slug", "")
score = results_dict.get(shout_slug, {}).get("score", 0)
shout_slug = shout.dict().get('slug', '')
score = results_dict.get(shout_slug, {}).get('score', 0)
shout_data = shout.dict() # Convert the Shout instance to a dictionary
shout_data["score"] = score # Add the score to the dictionary
shout_data['score'] = score # Add the score to the dictionary
shouts_data.append(shout_data)
return shouts_data
@@ -304,7 +338,7 @@ async def load_shouts_search(_, _info, text, limit=50, offset=0):
@login_required
@query.field("load_shouts_unrated")
@query.field('load_shouts_unrated')
async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
q = (
select(Shout)
@@ -320,7 +354,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
),
)
.outerjoin(Author, Author.user == bindparam("user_id"))
.outerjoin(Author, Author.user == bindparam('user_id'))
.where(
and_(
Shout.deleted_at.is_(None),
@@ -337,7 +371,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
q = add_stat_columns(q, aliased_reaction)
q = q.group_by(Shout.id).order_by(func.random()).limit(limit).offset(offset)
user_id = info.context.get("user_id")
user_id = info.context.get('user_id')
if user_id:
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
@@ -350,19 +384,24 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
async def get_shouts_from_query(q, author_id=None):
shouts = []
with local_session() as session:
for [shout,commented_stat, likes_stat, dislikes_stat, last_comment] in session.execute(
q, {"author_id": author_id}
).unique():
for [
shout,
commented_stat,
likes_stat,
dislikes_stat,
_last_comment,
] in session.execute(q, {'author_id': author_id}).unique():
shouts.append(shout)
shout.stat = {
"viewed": await ViewedStorage.get_shout(shout_slug=shout.slug),
"commented": commented_stat,
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
'viewed': await ViewedStorage.get_shout(shout_slug=shout.slug),
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
}
return shouts
@query.field("load_shouts_random_top")
@query.field('load_shouts_random_top')
async def load_shouts_random_top(_, _info, options):
"""
:param _
@@ -383,21 +422,22 @@ async def load_shouts_random_top(_, _info, options):
subquery = select(Shout.id).outerjoin(aliased_reaction).where(Shout.deleted_at.is_(None))
subquery = apply_filters(subquery, options.get("filters", {}))
subquery = subquery.group_by(Shout.id).order_by(desc(
subquery = apply_filters(subquery, options.get('filters', {}))
subquery = subquery.group_by(Shout.id).order_by(
desc(
func.sum(
case(
(Reaction.kind == ReactionKind.LIKE.value, 1),
(Reaction.kind == ReactionKind.AGREE.value, 1),
(Reaction.kind == ReactionKind.DISLIKE.value, -1),
(Reaction.kind == ReactionKind.DISAGREE.value, -1),
else_=0
else_=0,
)
)
)
)
random_limit = options.get("random_limit")
random_limit = options.get('random_limit')
if random_limit:
subquery = subquery.limit(random_limit)
@@ -412,7 +452,7 @@ async def load_shouts_random_top(_, _info, options):
aliased_reaction = aliased(Reaction)
q = add_stat_columns(q, aliased_reaction)
limit = options.get("limit", 10)
limit = options.get('limit', 10)
q = q.group_by(Shout.id).order_by(func.random()).limit(limit)
# print(q.compile(compile_kwargs={"literal_binds": True}))
@@ -420,7 +460,7 @@ async def load_shouts_random_top(_, _info, options):
return await get_shouts_from_query(q)
@query.field("load_shouts_random_topic")
@query.field('load_shouts_random_topic')
async def load_shouts_random_topic(_, info, limit: int = 10):
topic = get_random_topic()
shouts = []
@@ -431,7 +471,13 @@ async def load_shouts_random_topic(_, info, limit: int = 10):
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.filter(and_(Shout.deleted_at.is_(None), Shout.visibility == ShoutVisibility.PUBLIC.value, Shout.topics.any(slug=topic.slug)))
.filter(
and_(
Shout.deleted_at.is_(None),
Shout.visibility == ShoutVisibility.PUBLIC.value,
Shout.topics.any(slug=topic.slug),
)
)
)
aliased_reaction = aliased(Reaction)
@@ -441,4 +487,4 @@ async def load_shouts_random_topic(_, info, limit: int = 10):
shouts = get_shouts_from_query(q)
return {"topic": topic, "shouts": shouts}
return {'topic': topic, 'shouts': shouts}

View File

@@ -1,3 +1,5 @@
import logging
from sqlalchemy import and_, distinct, func, select
from sqlalchemy.orm import aliased
@@ -10,6 +12,10 @@ from services.schema import mutation, query
from services.viewed import ViewedStorage
logger = logging.getLogger('\t[resolvers.topic]\t')
logger.setLevel(logging.DEBUG)
async def followed_topics(follower_id):
q = select(Author)
q = add_topic_stat_columns(q)
@@ -17,17 +23,18 @@ async def followed_topics(follower_id):
# Pass the query to the get_topics_from_query function and return the results
return await get_topics_from_query(q)
def add_topic_stat_columns(q):
aliased_shout_author = aliased(ShoutAuthor)
aliased_topic_follower = aliased(TopicFollower)
q = (
q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic)
.add_columns(func.count(distinct(ShoutTopic.shout)).label("shouts_stat"))
.add_columns(func.count(distinct(ShoutTopic.shout)).label('shouts_stat'))
.outerjoin(aliased_shout_author, ShoutTopic.shout == aliased_shout_author.shout)
.add_columns(func.count(distinct(aliased_shout_author.author)).label("authors_stat"))
.add_columns(func.count(distinct(aliased_shout_author.author)).label('authors_stat'))
.outerjoin(aliased_topic_follower)
.add_columns(func.count(distinct(aliased_topic_follower.follower)).label("followers_stat"))
.add_columns(func.count(distinct(aliased_topic_follower.follower)).label('followers_stat'))
)
q = q.group_by(Topic.id)
@@ -40,17 +47,17 @@ async def get_topics_from_query(q):
with local_session() as session:
for [topic, shouts_stat, authors_stat, followers_stat] in session.execute(q):
topic.stat = {
"shouts": shouts_stat,
"authors": authors_stat,
"followers": followers_stat,
"viewed": await ViewedStorage.get_topic(topic.slug),
'shouts': shouts_stat,
'authors': authors_stat,
'followers': followers_stat,
'viewed': await ViewedStorage.get_topic(topic.slug),
}
topics.append(topic)
return topics
@query.field("get_topics_all")
@query.field('get_topics_all')
async def get_topics_all(_, _info):
q = select(Topic)
q = add_topic_stat_columns(q)
@@ -66,7 +73,7 @@ async def topics_followed_by(author_id):
return await get_topics_from_query(q)
@query.field("get_topics_by_community")
@query.field('get_topics_by_community')
async def get_topics_by_community(_, _info, community_id: int):
q = select(Topic).where(Topic.community == community_id)
q = add_topic_stat_columns(q)
@@ -74,8 +81,8 @@ async def get_topics_by_community(_, _info, community_id: int):
return await get_topics_from_query(q)
@query.field("get_topics_by_author")
async def get_topics_by_author(_, _info, author_id=None, slug="", user=""):
@query.field('get_topics_by_author')
async def get_topics_by_author(_, _info, author_id=None, slug='', user=''):
q = select(Topic)
q = add_topic_stat_columns(q)
if author_id:
@@ -88,7 +95,7 @@ async def get_topics_by_author(_, _info, author_id=None, slug="", user=""):
return await get_topics_from_query(q)
@query.field("get_topic")
@query.field('get_topic')
async def get_topic(_, _info, slug):
q = select(Topic).where(Topic.slug == slug)
q = add_topic_stat_columns(q)
@@ -97,7 +104,7 @@ async def get_topic(_, _info, slug):
return topics[0]
@mutation.field("create_topic")
@mutation.field('create_topic')
@login_required
async def create_topic(_, _info, inp):
with local_session() as session:
@@ -106,44 +113,44 @@ async def create_topic(_, _info, inp):
session.add(new_topic)
session.commit()
return {"topic": new_topic}
return {'topic': new_topic}
@mutation.field("update_topic")
@mutation.field('update_topic')
@login_required
async def update_topic(_, _info, inp):
slug = inp["slug"]
slug = inp['slug']
with local_session() as session:
topic = session.query(Topic).filter(Topic.slug == slug).first()
if not topic:
return {"error": "topic not found"}
return {'error': 'topic not found'}
else:
Topic.update(topic, inp)
session.add(topic)
session.commit()
return {"topic": topic}
return {'topic': topic}
@mutation.field("delete_topic")
@mutation.field('delete_topic')
@login_required
async def delete_topic(_, info, slug: str):
user_id = info.context["user_id"]
user_id = info.context['user_id']
with local_session() as session:
t: Topic = session.query(Topic).filter(Topic.slug == slug).first()
if not t:
return {"error": "invalid topic slug"}
return {'error': 'invalid topic slug'}
author = session.query(Author).filter(Author.user == user_id).first()
if author:
if t.created_by != author.id:
return {"error": "access denied"}
return {'error': 'access denied'}
session.delete(t)
session.commit()
return {}
else:
return {"error": "access denied"}
return {'error': 'access denied'}
def topic_follow(follower_id, slug):
@@ -169,12 +176,12 @@ def topic_unfollow(follower_id, slug):
session.delete(sub)
session.commit()
return True
except Exception:
pass
except Exception as ex:
logger.debug(ex)
return False
@query.field("get_topics_random")
@query.field('get_topics_random')
async def get_topics_random(_, info, amount=12):
q = select(Topic)
q = q.join(ShoutTopic)

View File

@@ -15,22 +15,22 @@ class WebhookEndpoint(HTTPEndpoint):
try:
data = await request.json()
if data:
auth = request.headers.get("Authorization")
auth = request.headers.get('Authorization')
if auth:
if auth == os.environ.get("WEBHOOK_SECRET"):
user_id: str = data["user"]["id"]
name: str = data["user"]["given_name"]
slug: str = data["user"]["email"].split("@")[0]
slug: str = re.sub("[^0-9a-z]+", "-", slug.lower())
if auth == os.environ.get('WEBHOOK_SECRET'):
user_id: str = data['user']['id']
name: str = data['user']['given_name']
slug: str = data['user']['email'].split('@')[0]
slug: str = re.sub('[^0-9a-z]+', '-', slug.lower())
with local_session() as session:
author = session.query(Author).filter(Author.slug == slug).first()
if author:
slug = slug + "-" + user_id.split("-").pop()
slug = slug + '-' + user_id.split('-').pop()
await create_author(user_id, slug, name)
return JSONResponse({"status": "success"})
return JSONResponse({'status': 'success'})
except Exception as e:
import traceback
traceback.print_exc()
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)