fmt
All checks were successful
Deploy to core / deploy (push) Successful in 2m0s

This commit is contained in:
2024-02-21 10:27:16 +03:00
parent 4f26812340
commit 7cf702eb98
35 changed files with 1059 additions and 825 deletions

View File

@@ -1,6 +1,6 @@
from resolvers.author import (
get_author,
get_author_followed,
get_author_follows,
get_author_followers,
get_author_id,
get_authors_all,
@@ -27,46 +27,52 @@ from resolvers.reader import (
load_shouts_search,
load_shouts_unrated,
)
from resolvers.topic import get_topic, get_topics_all, get_topics_by_author, get_topics_by_community
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_author',
"get_author",
"get_author_id",
"get_authors_all",
"get_author_followers",
"get_author_follows",
"load_authors_by",
"rate_author",
"update_author",
# 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,21 +1,18 @@
import time
from typing import List
from sqlalchemy import and_, desc, distinct, func, select
from sqlalchemy import and_, desc, distinct, func, select, or_
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 Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.community import followed_communities
from resolvers.reaction import reacted_shouts_updates as followed_reactions
from resolvers.topic import followed_topics
from resolvers.follower import get_follows_by_user_id
from services.auth import login_required
from services.db import local_session
from services.rediscache import redis
from services.schema import mutation, query
from services.unread import get_total_unread_counter
from services.viewed import ViewedStorage
from services.logger import root_logger as logger
@@ -23,18 +20,18 @@ from services.logger import root_logger as logger
def add_author_stat_columns(q):
shout_author_aliased = aliased(ShoutAuthor)
q = q.outerjoin(shout_author_aliased).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')
)
q = q.outerjoin(
followings_table, followings_table.follower == Author.id
).add_columns(func.count(distinct(followers_table.author)).label("followings_stat"))
q = q.group_by(Author.id)
return q
@@ -43,42 +40,33 @@ def add_author_stat_columns(q):
async def get_authors_from_query(q):
authors = []
with local_session() as session:
for [author, shouts_stat, followers_stat, followings_stat] in session.execute(q):
for [author, shouts_stat, followers_stat, followings_stat] in session.execute(
q
):
author.stat = {
'shouts': shouts_stat,
'viewed': await ViewedStorage.get_author(author.slug),
'followers': followers_stat,
'followings': followings_stat,
"shouts": shouts_stat,
"viewed": await ViewedStorage.get_author(author.slug),
"followers": followers_stat,
"followings": followings_stat,
}
authors.append(author)
return authors
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)],
}
@mutation.field('update_author')
@mutation.field("update_author")
@login_required
async def update_author(_, 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}
# TODO: caching query
@query.field('get_authors_all')
@query.field("get_authors_all")
async def get_authors_all(_, _info):
authors = []
with local_session() as session:
@@ -165,23 +153,33 @@ async def load_author_with_stats(q):
)
likes_count = (
session.query(AuthorRating)
.filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True)))
.filter(
and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True))
)
.count()
)
dislikes_count = (
session.query(AuthorRating)
.filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_not(True)))
.filter(
and_(
AuthorRating.author == author.id, AuthorRating.plus.is_not(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
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):
@@ -192,34 +190,51 @@ async def get_author(_, _info, slug='', author_id=None):
return await load_author_with_stats(q)
@query.field('get_author_id')
async def get_author_by_user_id(user_id: str):
redis_key = f"user:{user_id}:author"
res = await redis.execute("HGET", redis_key)
if res:
return res
logger.info(f"getting author id for {user_id}")
q = select(Author).filter(Author.user == user_id)
author = await load_author_with_stats(q)
await redis.execute("HSET", redis_key, author.dict())
return author
@query.field("get_author_id")
async def get_author_id(_, _info, user: str):
logger.info(f'getting author id for {user}')
q = select(Author).filter(Author.user == user)
return await load_author_with_stats(q)
return get_author_by_user_id(user)
@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 unix time
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 unix time
before = int(time.time()) - by["created_at"]
q = q.filter(Author.created_at > before)
order = by.get('order')
if order == 'followers' or order == 'shouts':
q = q.order_by(desc(f'{order}_stat'))
order = by.get("order")
if order == "followers" or order == "shouts":
q = q.order_by(desc(f"{order}_stat"))
q = q.limit(limit).offset(offset)
@@ -228,24 +243,28 @@ async def load_authors_by(_, _info, by, limit, offset):
return authors
@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)
elif user:
author_id_query = select(Author.id).where(Author.user == user)
if author_id_query is not None and not author_id:
@query.field("get_author_follows")
async def get_author_follows(
_, _info, slug="", user=None, author_id=None
) -> List[Author]:
user_id = user
if author_id or slug:
with local_session() as session:
author_id = session.execute(author_id_query).scalar()
author = (
session.query(Author)
.where(or_(Author.id == author_id, Author.slug == slug))
.first()
)
user_id = author.user
if author_id is None:
raise ValueError('Author not found')
if user_id:
follows = await get_follows_by_user_id(user)
return follows
else:
return await followed_authors(author_id) # Author[]
raise ValueError("Author not found")
@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)
@@ -260,18 +279,10 @@ async def get_author_followers(_, _info, slug) -> List[Author]:
return await get_authors_from_query(q)
async def followed_authors(follower_id):
q = select(Author)
q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where(AuthorFollower.follower == follower_id)
# Pass the query to the get_authors_from_query function and return the results
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()
@@ -294,17 +305,19 @@ async def rate_author(_, info, rated_slug, value):
return {}
else:
try:
rating = AuthorRating(rater=rater.id, author=rated_author.id, plus=value > 0)
rating = AuthorRating(
rater=rater.id, author=rated_author.id, plus=value > 0
)
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:
@@ -17,7 +17,11 @@ async def accept_invite(_, info, invite_id: int):
if author:
# Check if the invite exists
invite = session.query(Invite).filter(Invite.id == invite_id).first()
if invite and invite.author_id is author.id and invite.status is InviteStatus.PENDING.value:
if (
invite
and invite.author_id is author.id
and invite.status is InviteStatus.PENDING.value
):
# Add the user to the shout authors
shout = session.query(Shout).filter(Shout.id == invite.shout_id).first()
if shout:
@@ -26,19 +30,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:
@@ -46,21 +50,25 @@ async def reject_invite(_, info, invite_id: int):
if author:
# Check if the invite exists
invite = session.query(Invite).filter(Invite.id == invite_id).first()
if invite and invite.author_id is author.id and invite.status is InviteStatus.PENDING.value:
if (
invite
and invite.author_id is author.id
and invite.status is InviteStatus.PENDING.value
):
# 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,42 +90,47 @@ 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(
inviter_id=user_id, author_id=author_id, shout_id=shout.id, status=InviteStatus.PENDING.value
inviter_id=user_id,
author_id=author_id,
shout_id=shout.id,
status=InviteStatus.PENDING.value,
)
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:
shout = session.query(Shout).filter(Shout.slug == slug).first()
# NOTE: owner should be first in a list
if shout and author.id is shout.created_by:
shout.authors = [author for author in shout.authors if author.id != author_id]
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 +148,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

@@ -14,10 +14,12 @@ def add_community_stat_columns(q):
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')
q = q.outerjoin(
community_followers, community_followers.author == Author.id
).add_columns(
func.count(distinct(community_followers.follower)).label("followers_stat")
)
q = q.group_by(Author.id)
@@ -30,8 +32,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)
@@ -39,26 +41,6 @@ def get_communities_from_query(q):
return ccc
SINGLE_COMMUNITY = True
def followed_communities(follower_id):
if SINGLE_COMMUNITY:
with local_session() as session:
c = session.query(Community).first()
return [
c,
]
else:
q = select(Community)
q = add_community_stat_columns(q)
q = q.join(CommunityAuthor, CommunityAuthor.community == Community.id).where(
CommunityAuthor.author == follower_id
)
# 3. Pass the query to the get_authors_from_query function and return the results
return get_communities_from_query(q)
# for mutation.field("follow")
def community_follow(follower_id, slug):
try:
@@ -90,7 +72,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 +80,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

@@ -18,10 +18,10 @@ from services.search import search_service
from services.logger import root_logger as logger
@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"]
shouts = []
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
@@ -40,28 +40,28 @@ 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()
if isinstance(author, 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', []),
'published_at': None,
'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", []),
"published_at": None,
"created_at": current_time, # Set created_at as Unix timestamp
}
new_shout = Shout(**shout_dict)
@@ -75,7 +75,11 @@ 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)
@@ -85,9 +89,9 @@ async def create_shout(_, info, inp):
# notifier
# await notify_shout(shout_dict, 'create')
return { 'shout': shout.dict() }
return {"shout": shout.dict()}
return {'error': 'cant create shout'}
return {"error": "cant create shout"}
def patch_main_topic(session, main_topic, shout):
@@ -117,15 +121,17 @@ def patch_main_topic(session, main_topic, shout):
)
if old_main_topic and new_main_topic and old_main_topic is not new_main_topic:
ShoutTopic.update(old_main_topic, {'main': False})
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)
def patch_topics(session, shout, topics_input):
new_topics_to_link = [Topic(**new_topic) for new_topic in topics_input if new_topic['id'] < 0]
new_topics_to_link = [
Topic(**new_topic) for new_topic in topics_input if new_topic["id"] < 0
]
if new_topics_to_link:
session.add_all(new_topics_to_link)
session.commit()
@@ -134,21 +140,25 @@ def patch_topics(session, shout, topics_input):
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)
created_unlinked_topic = ShoutTopic(
shout=shout.id, topic=existing_topic_to_link_id
)
session.add(created_unlinked_topic)
topic_to_unlink_ids = [
topic.id
for topic in shout.topics
if topic.id not in [topic_input['id'] for topic_input in existing_topics_input]
if topic.id not in [topic_input["id"] for topic_input in existing_topics_input]
]
session.query(ShoutTopic).filter(
@@ -159,16 +169,16 @@ def patch_topics(session, shout, topics_input):
).delete(synchronize_session=False)
@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']
roles = info.context['roles']
user_id = info.context["user_id"]
roles = info.context["roles"]
shout_input = shout_input or {}
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
current_time = int(time.time())
shout_id = shout_id or shout_input.get('id')
shout_id = shout_id or shout_input.get("id")
if isinstance(author, Author) and isinstance(shout_id, int):
shout = (
session.query(Shout)
@@ -181,23 +191,27 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
)
if not shout:
return {'error': 'shout not found'}
if shout.created_by is not author.id and author.id not in shout.authors and 'editor' not in roles:
return {'error': 'access denied'}
return {"error": "shout not found"}
if (
shout.created_by is not author.id
and author.id not in shout.authors
and "editor" not in roles
):
return {"error": "access denied"}
# topics patch
topics_input = shout_input.get('topics')
topics_input = shout_input.get("topics")
if topics_input:
patch_topics(session, shout, topics_input)
del shout_input['topics']
del shout_input["topics"]
# main topic
main_topic = shout_input.get('main_topic')
main_topic = shout_input.get("main_topic")
if main_topic:
patch_main_topic(session, main_topic, shout)
shout_input['updated_at'] = current_time
shout_input['published_at'] = current_time if publish else None
shout_input["updated_at"] = current_time
shout_input["published_at"] = current_time if publish else None
Shout.update(shout, shout_input)
session.add(shout)
session.commit()
@@ -205,50 +219,61 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
shout_dict = shout.dict()
if not publish:
await notify_shout(shout_dict, 'update')
await notify_shout(shout_dict, "update")
else:
await notify_shout(shout_dict, 'published')
await notify_shout(shout_dict, "published")
# search service indexing
search_service.index(shout)
return {'shout': shout_dict}
logger.debug(f' cannot update with data: {shout_input}')
return { 'error': 'not enough data' }
return {'error': 'cannot update'}
return {"shout": shout_dict}
logger.debug(f" cannot update with data: {shout_input}")
return {"error": "cant update shout"}
@mutation.field('delete_shout')
@mutation.field("delete_shout")
@login_required
async def delete_shout(_, info, shout_id):
user_id = info.context['user_id']
roles = info.context['roles']
user_id = info.context["user_id"]
roles = info.context["roles"]
with local_session() as session:
author = session.query(Author).filter(Author.user == 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 author and shout:
if shout.created_by is not author.id and author.id not in shout.authors and 'editor' not in roles:
return {'error': 'access denied'}
if (
shout.created_by is not author.id
and author.id not in shout.authors
and "editor" not in roles
):
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 {}
def handle_proposing(session, r, shout):
if is_positive(r.kind):
replied_reaction = session.query(Reaction).filter(Reaction.id == r.reply_to, Reaction.shout == r.shout).first()
replied_reaction = (
session.query(Reaction)
.filter(Reaction.id == r.reply_to, Reaction.shout == r.shout)
.first()
)
if replied_reaction and replied_reaction.kind is ReactionKind.PROPOSE.value and replied_reaction.quote:
if (
replied_reaction
and replied_reaction.kind is ReactionKind.PROPOSE.value
and replied_reaction.quote
):
# patch all the proposals' quotes
proposals = (
session.query(Reaction)
@@ -265,13 +290,15 @@ def handle_proposing(session, r, shout):
if proposal.quote:
proposal_diff = get_diff(shout.body, proposal.quote)
proposal_dict = proposal.dict()
proposal_dict['quote'] = apply_diff(replied_reaction.quote, proposal_diff)
proposal_dict["quote"] = apply_diff(
replied_reaction.quote, proposal_diff
)
Reaction.update(proposal, proposal_dict)
session.add(proposal)
# patch shout's body
shout_dict = shout.dict()
shout_dict['body'] = replied_reaction.quote
shout_dict["body"] = replied_reaction.quote
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()

View File

@@ -15,66 +15,72 @@ from services.db import local_session
from services.notify import notify_follower
from services.schema import mutation, query
from services.logger import root_logger as logger
from services.rediscache import redis
@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):
author = session.query(Author.id).where(Author.slug == slug).one()
follower = session.query(Author).where(Author.id == follower_id).one()
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":
topic_follow(follower_id, slug)
elif what == 'COMMUNITY':
elif what == "COMMUNITY":
community_follow(follower_id, slug)
elif what == 'REACTIONS':
elif what == "REACTIONS":
reactions_follow(follower_id, slug)
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):
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':
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":
topic_unfollow(follower_id, slug)
elif what == 'COMMUNITY':
elif what == "COMMUNITY":
community_unfollow(follower_id, slug)
elif what == 'REACTIONS':
elif what == "REACTIONS":
reactions_unfollow(follower_id, slug)
except Exception as e:
return {'error': str(e)}
return {"error": str(e)}
return {}
@query.field('get_my_followed')
@login_required
async def get_my_followed(_, info):
user_id = info.context['user_id']
def query_follows(user_id: str):
topics = set()
authors = set()
communities = []
@@ -99,11 +105,37 @@ async def get_my_followed(_, info):
topics = set(session.execute(topics_query).scalars())
communities = session.query(Community).all()
return {'topics': list(topics), 'authors': list(authors), 'communities': communities}
return {
"topics": list(topics),
"authors": list(authors),
"communities": communities,
}
@query.field('get_shout_followers')
def get_shout_followers(_, _info, slug: str = '', shout_id: int | None = None) -> List[Author]:
async def get_follows_by_user_id(user_id: str):
redis_key = f"user:{user_id}:follows"
res = await redis.execute("HGET", redis_key)
if res:
return res
logger.info(f"getting follows for {user_id}")
follows = query_follows(user_id)
await redis.execute("HSET", redis_key, follows)
return follows
@query.field("get_my_followed")
@login_required
async def get_my_followed(_, info):
user_id = info.context["user_id"]
return await get_follows_by_user_id(user_id)
@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
@@ -136,7 +168,9 @@ def reactions_follow(author_id, shout_id, auto=False):
)
if not following:
following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
following = ShoutReactionsFollower(
follower=author_id, shout=shout.id, auto=auto
)
session.add(following)
session.commit()
return True

View File

@@ -21,16 +21,22 @@ from services.logger import root_logger as logger
def add_stat_columns(q, aliased_reaction):
q = q.outerjoin(aliased_reaction).add_columns(
func.sum(aliased_reaction.id).label('reacted_stat'),
func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT.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.sum(aliased_reaction.id).label("reacted_stat"),
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'),
).label("last_comment"),
)
return q
@@ -54,7 +60,9 @@ def check_to_feature(session, approver_id, reaction):
approvers = []
approvers.append(approver_id)
# now count how many approvers are voted already
reacted_readers = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
reacted_readers = (
session.query(Reaction).where(Reaction.shout == reaction.shout).all()
)
for reacted_reader in reacted_readers:
if is_featured_author(session, reacted_reader.id):
approvers.append(reacted_reader.id)
@@ -69,12 +77,19 @@ def check_to_unfeature(session, rejecter_id, reaction):
if is_featured_author(session, rejecter_id):
reactions = (
session.query(Reaction)
.where(and_(Reaction.shout == reaction.shout, Reaction.kind.in_(RATING_REACTIONS)))
.where(
and_(
Reaction.shout == reaction.shout,
Reaction.kind.in_(RATING_REACTIONS),
)
)
.all()
)
rejects = 0
for r in reactions:
approver = session.query(Author).filter(Author.id == r.created_by).first()
approver = (
session.query(Author).filter(Author.id == r.created_by).first()
)
if is_featured_author(session, approver):
if is_negative(r.kind):
rejects += 1
@@ -86,7 +101,7 @@ def check_to_unfeature(session, rejecter_id, reaction):
async def set_featured(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
s.featured_at = int(time.time())
Shout.update(s, {'featured_at': int(time.time())})
Shout.update(s, {"featured_at": int(time.time())})
author = session.query(Author).filter(Author.id == s.created_by).first()
if author:
await add_user_role(str(author.user))
@@ -96,7 +111,7 @@ async def set_featured(session, shout_id):
def set_unfeatured(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
Shout.update(s, {'featured_at': None})
Shout.update(s, {"featured_at": None})
session.add(s)
session.commit()
@@ -108,7 +123,11 @@ async def _create_reaction(session, shout, author, reaction):
rdict = r.dict()
# collaborative editing
if rdict.get('reply_to') and r.kind in RATING_REACTIONS and author.id in shout.authors:
if (
rdict.get("reply_to")
and r.kind in RATING_REACTIONS
and author.id in shout.authors
):
handle_proposing(session, r, shout)
# self-regultaion mechanics
@@ -118,92 +137,95 @@ async def _create_reaction(session, shout, author, reaction):
await set_featured(session, shout.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).first()
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 isinstance(reaction.get('body'), str):
if not kind and isinstance(reaction.get("body"), str):
kind = ReactionKind.COMMENT.value
if not kind:
return {'error': 'cannot create reaction without a kind'}
return {"error": "cannot create reaction without a kind"}
if kind in RATING_REACTIONS:
opposite_kind = ( ReactionKind.DISLIKE.value
opposite_kind = (
ReactionKind.DISLIKE.value
if is_positive(kind)
else ReactionKind.LIKE.value
)
q = (
select(Reaction)
.filter(
and_(
Reaction.shout == shout_id,
Reaction.created_by == author.id,
Reaction.kind.in_(RATING_REACTIONS),
)
q = select(Reaction).filter(
and_(
Reaction.shout == shout_id,
Reaction.created_by == author.id,
Reaction.kind.in_(RATING_REACTIONS),
)
)
reply_to = reaction.get('reply_to')
reply_to = reaction.get("reply_to")
if reply_to:
q = q.filter(Reaction.reply_to == reply_to)
rating_reactions = session.execute(q).all()
same_rating = filter(lambda r: r.created_by == author.id and r.kind == opposite_kind, rating_reactions)
opposite_rating = filter(lambda r: r.created_by == author.id and r.kind == opposite_kind, rating_reactions)
same_rating = filter(
lambda r: r.created_by == author.id and r.kind == opposite_kind,
rating_reactions,
)
opposite_rating = filter(
lambda r: r.created_by == author.id and r.kind == opposite_kind,
rating_reactions,
)
if same_rating:
return {'error': "You can't rate the same thing twice"}
return {"error": "You can't rate the same thing twice"}
elif opposite_rating:
return {'error': 'Remove opposite vote first'}
return {"error": "Remove opposite vote first"}
elif filter(lambda r: r.created_by == author.id, rating_reactions):
return {'error': "You can't rate your own thing"}
return {"error": "You can't rate your own thing"}
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, reaction):
user_id = info.context.get('user_id')
roles = info.context.get('roles')
rid = reaction.get('id')
user_id = info.context.get("user_id")
roles = info.context.get("roles")
rid = reaction.get("id")
if rid and user_id and roles:
del reaction['id']
del reaction["id"]
with local_session() as session:
reaction_query = select(Reaction).filter(Reaction.id == int(rid))
aliased_reaction = aliased(Reaction)
@@ -211,22 +233,24 @@ async def update_reaction(_, info, reaction):
reaction_query = reaction_query.group_by(Reaction.id)
try:
[r, reacted_stat, commented_stat, likes_stat, dislikes_stat, _l] = session.execute(reaction_query).unique().first()
[r, reacted_stat, commented_stat, likes_stat, dislikes_stat, _l] = (
session.execute(reaction_query).unique().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 != author.id and 'editor' not in roles:
return {'error': 'access denied'}
if r.created_by != author.id and "editor" not in roles:
return {"error": "access denied"}
body = reaction.get('body')
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"]:
# Определение изменения мнения может быть реализовано здесь
pass
@@ -235,79 +259,79 @@ async def update_reaction(_, info, reaction):
session.commit()
r.stat = {
'reacted': reacted_stat,
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
"reacted": reacted_stat,
"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": "not authorized"}
except Exception:
import traceback
traceback.print_exc()
return {'error': 'cannot create reaction'}
return {"error": "cannot create reaction"}
@mutation.field('delete_reaction')
@mutation.field("delete_reaction")
@login_required
async def delete_reaction(_, info, reaction_id: int):
user_id = info.context['user_id']
roles = info.context['roles']
user_id = info.context["user_id"]
roles = info.context["roles"]
if isinstance(reaction_id, int) and user_id and isinstance(roles, list):
with local_session() as session:
try:
author = session.query(Author).filter(Author.user == user_id).one()
r = session.query(Reaction).filter(Reaction.id == reaction_id).one()
if r and author:
if r.created_by is author.id and 'editor' not in roles:
return {'error': 'access denied'}
if r.created_by is author.id and "editor" not in roles:
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")
except Exception as exc:
return {'error': f'cannot delete reaction: {exc}'}
return {'error': 'cannot delete reaction'}
return {"error": f"cannot delete reaction: {exc}"}
return {"error": "cannot delete reaction"}
def apply_reaction_filters(by, q):
shout_slug = by.get('shout', None)
shout_slug = by.get("shout", None)
if shout_slug:
q = q.filter(Shout.slug == shout_slug)
elif by.get('shouts'):
q = q.filter(Shout.slug.in_(by.get('shouts', [])))
elif by.get("shouts"):
q = q.filter(Shout.slug.in_(by.get("shouts", [])))
created_by = by.get('created_by', None)
created_by = by.get("created_by", None)
if created_by:
q = q.filter(Author.id == created_by)
topic = by.get('topic', None)
topic = by.get("topic", None)
if topic:
q = q.filter(Shout.topics.contains(topic))
if by.get('comment', False):
if by.get("comment", False):
q = q.filter(Reaction.kind == ReactionKind.COMMENT.value)
if by.get('rating', False):
if by.get("rating", False):
q = q.filter(Reaction.kind.in_(RATING_REACTIONS))
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}%"))
after = by.get('after', None)
after = by.get("after", None)
if isinstance(after, int):
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
@@ -344,7 +368,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)
@@ -365,19 +389,19 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
reaction.created_by = author
reaction.shout = shout
reaction.stat = {
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
'reacted': reacted_stat,
'commented': commented_stat,
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
"reacted": reacted_stat,
"commented": commented_stat,
}
reactions.add(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('-'),
reverse=stat_sort.startswith("-"),
)
return reactions
@@ -415,7 +439,9 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S
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 [
@@ -427,26 +453,26 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S
last_comment,
] in results:
shout.stat = {
'viewed': await ViewedStorage.get_shout(shout.slug),
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
'reacted': reacted_stat,
'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),
"reacted": reacted_stat,
"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

@@ -18,22 +18,22 @@ from services.logger import root_logger as logger
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.created_by == author_id)
by_featured = filters.get('featured')
by_featured = filters.get("featured")
if by_featured:
q = q.filter(Shout.featured_at.is_not(None))
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)
@@ -41,7 +41,7 @@ def apply_filters(q, filters, author_id=None):
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(
@@ -72,13 +72,15 @@ async def get_shout(_, _info, slug=None, shout_id=None):
] = results
shout.stat = {
'viewed': await ViewedStorage.get_shout(shout.slug),
'reacted': reacted_stat,
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
"viewed": await ViewedStorage.get_shout(shout.slug),
"reacted": reacted_stat,
"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):
for author_caption in (
session.query(ShoutAuthor).join(Shout).where(Shout.slug == slug)
):
for author in shout.authors:
if author.id == author_caption.author:
author.caption = author_caption.caption
@@ -99,10 +101,12 @@ 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: {
@@ -138,20 +142,24 @@ async def load_shouts_by(_, _info, options):
q = add_stat_columns(q, aliased_reaction)
# filters
filters = options.get('filters', {})
filters = options.get("filters", {})
q = apply_filters(q, filters)
# group
q = q.group_by(Shout.id)
# order
order_by = options.get('order_by', Shout.featured_at if filters.get('featured') else 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.featured_at if filters.get("featured") else 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 = []
@@ -180,20 +188,20 @@ 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),
'reacted': reacted_stat,
'commented': commented_stat,
'rating': int(likes_stat) - int(dislikes_stat),
"viewed": await ViewedStorage.get_shout(shout.slug),
"reacted": reacted_stat,
"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)
@@ -231,24 +239,29 @@ 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:
reader = session.query(Author).filter(Author.user == user_id).first()
if reader:
reader_followed_authors = select(AuthorFollower.author).where(AuthorFollower.follower == reader.id)
reader_followed_topics = select(TopicFollower.topic).where(TopicFollower.follower == reader.id)
reader_followed_authors = select(AuthorFollower.author).where(
AuthorFollower.follower == reader.id
)
reader_followed_topics = select(TopicFollower.topic).where(
TopicFollower.follower == reader.id
)
subquery = (
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))
(ShoutAuthor.author.in_(reader_followed_authors))
| (ShoutTopic.topic.in_(reader_followed_topics))
)
)
@@ -269,22 +282,37 @@ async def load_shouts_feed(_, info, options):
aliased_reaction = aliased(Reaction)
q = add_stat_columns(q, aliased_reaction)
filters = options.get('filters', {})
filters = options.get("filters", {})
q = apply_filters(q, filters, reader.id)
order_by = options.get('order_by', Shout.featured_at if filters.get('featured') else Shout.published_at)
order_by = options.get(
"order_by",
Shout.featured_at if filters.get("featured") else 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)
q = (
q.group_by(Shout.id)
.order_by(nulls_last(query_order_by))
.limit(limit)
.offset(offset)
)
# print(q.compile(compile_kwargs={"literal_binds": True}))
for [shout, reacted_stat, commented_stat, likes_stat, dislikes_stat, _last_comment] in session.execute(
q
).unique():
for [
shout,
reacted_stat,
commented_stat,
likes_stat,
dislikes_stat,
_last_comment,
] in session.execute(q).unique():
main_topic = (
session.query(Topic.slug)
.join(
@@ -301,17 +329,17 @@ 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,
'rating': likes_stat - dislikes_stat,
"viewed": await ViewedStorage.get_shout(shout.slug),
"reacted": reacted_stat,
"commented": commented_stat,
"rating": likes_stat - dislikes_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 isinstance(text, str) and len(text) > 2:
results = await search_text(text, limit, offset)
@@ -321,7 +349,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)
@@ -334,10 +362,12 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
and_(
Reaction.shout == Shout.id,
Reaction.replyTo.is_(None),
Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
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),
@@ -354,7 +384,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()
@@ -374,20 +404,20 @@ async def get_shouts_from_query(q, author_id=None):
likes_stat,
dislikes_stat,
last_comment,
] in session.execute(q, {'author_id': author_id}).unique():
] in session.execute(q, {"author_id": author_id}).unique():
shouts.append(shout)
shout.stat = {
'viewed': await ViewedStorage.get_shout(shout_slug=shout.slug),
'reacted': reacted_stat,
'commented': commented_stat,
'rating': int(likes_stat or 0) - int(dislikes_stat or 0),
'last_comment': last_comment,
"viewed": await ViewedStorage.get_shout(shout_slug=shout.slug),
"reacted": reacted_stat,
"commented": commented_stat,
"rating": int(likes_stat or 0) - int(dislikes_stat or 0),
"last_comment": last_comment,
}
return shouts
@query.field('load_shouts_random_top')
@query.field("load_shouts_random_top")
async def load_shouts_random_top(_, _info, options):
"""
:param _
@@ -406,9 +436,11 @@ async def load_shouts_random_top(_, _info, options):
aliased_reaction = aliased(Reaction)
subquery = select(Shout.id).outerjoin(aliased_reaction).where(Shout.deleted_at.is_(None))
subquery = (
select(Shout.id).outerjoin(aliased_reaction).where(Shout.deleted_at.is_(None))
)
subquery = apply_filters(subquery, options.get('filters', {}))
subquery = apply_filters(subquery, options.get("filters", {}))
subquery = subquery.group_by(Shout.id).order_by(
desc(
func.sum(
@@ -423,7 +455,7 @@ async def load_shouts_random_top(_, _info, options):
)
)
random_limit = options.get('random_limit')
random_limit = options.get("random_limit")
if random_limit:
subquery = subquery.limit(random_limit)
@@ -438,20 +470,24 @@ 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)
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()
if topic:
shouts = fetch_shouts_by_topic(topic, limit)
if shouts:
return {'topic': topic, 'shouts': shouts}
return { 'error': 'failed to get random topic after few retries', shouts: [], topic: {} }
return {"topic": topic, "shouts": shouts}
return {
"error": "failed to get random topic after few retries",
shouts: [],
topic: {},
}
def fetch_shouts_by_topic(topic, limit):

View File

@@ -11,25 +11,23 @@ from services.viewed import ViewedStorage
from services.logger import root_logger as logger
async def followed_topics(follower_id):
q = select(Author)
q = add_topic_stat_columns(q)
q = q.join(TopicFollower, TopicFollower.author == Author.id).where(TopicFollower.follower == 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)
@@ -42,17 +40,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)
@@ -68,7 +66,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)
@@ -76,8 +74,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:
@@ -90,7 +88,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)
@@ -100,7 +98,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:
@@ -110,45 +108,43 @@ async def create_topic(_, _info, inp):
session.add(new_topic)
session.commit()
return {'topic': new_topic}
return {'error': 'cannot create 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 {'error': 'cannot update'}
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 {}
return {'error': 'access denied'}
return {"error": "access denied"}
def topic_follow(follower_id, slug):
@@ -179,7 +175,7 @@ def topic_unfollow(follower_id, slug):
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)