fmt
All checks were successful
Deploy on push / deploy (push) Successful in 3m45s

This commit is contained in:
Untone 2024-02-24 21:45:38 +03:00
parent 12137eccda
commit eaaace4d28
6 changed files with 263 additions and 215 deletions

View File

@ -7,9 +7,9 @@ from resolvers.author import (
get_author_id, get_author_id,
get_authors_all, get_authors_all,
load_authors_by, load_authors_by,
rate_author,
update_author, update_author,
) )
from resolvers.rating import rate_author
from resolvers.community import get_communities_all, get_community from resolvers.community import get_communities_all, get_community
from resolvers.editor import create_shout, delete_shout, update_shout from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.follower import ( from resolvers.follower import (

View File

@ -1,14 +1,19 @@
import json import json
import time import time
from sqlalchemy import and_, desc, select, or_, distinct, func from sqlalchemy import desc, select, or_, distinct, func
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from orm.author import Author, AuthorFollower, AuthorRating from orm.author import Author, AuthorFollower
from orm.shout import ShoutAuthor, ShoutTopic from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic from orm.topic import Topic
from resolvers.follower import query_follows from resolvers.follower import query_follows
from resolvers.stat import get_authors_with_stat, execute_with_ministat, author_follows_authors, author_follows_topics from resolvers.stat import (
get_authors_with_stat,
execute_with_ministat,
author_follows_authors,
author_follows_topics,
)
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.rediscache import redis from services.rediscache import redis
@ -47,7 +52,7 @@ def get_author(_, _info, slug='', author_id=None):
if author_id: if author_id:
q = select(Author).where(Author.id == author_id) q = select(Author).where(Author.id == author_id)
[author, ] = get_authors_with_stat(q, ratings=True) [author] = get_authors_with_stat(q, ratings=True)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.error(exc)
return author return author
@ -67,7 +72,7 @@ async def get_author_by_user_id(user_id: str, ratings=False):
logger.info(f'getting author id for {user_id}') logger.info(f'getting author id for {user_id}')
q = select(Author).filter(Author.user == user_id) q = select(Author).filter(Author.user == user_id)
[author, ] = get_authors_with_stat(q, ratings) [author] = get_authors_with_stat(q, ratings)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.error(exc)
return author return author
@ -115,7 +120,11 @@ def load_authors_by(_, _info, by, limit, offset):
def get_author_follows(_, _info, slug='', user=None, author_id=None): def get_author_follows(_, _info, slug='', user=None, author_id=None):
with local_session() as session: with local_session() as session:
if user or slug: if user or slug:
author_id_result = session.query(Author.id).filter(or_(Author.user == user, Author.slug == slug)).first() author_id_result = (
session.query(Author.id)
.filter(or_(Author.user == user, Author.slug == slug))
.first()
)
author_id = author_id_result[0] if author_id_result else None author_id = author_id_result[0] if author_id_result else None
if author_id: if author_id:
follows = query_follows(author_id) follows = query_follows(author_id)
@ -128,7 +137,11 @@ def get_author_follows(_, _info, slug='', user=None, author_id=None):
def get_author_follows_topics(_, _info, slug='', user=None, author_id=None): def get_author_follows_topics(_, _info, slug='', user=None, author_id=None):
with local_session() as session: with local_session() as session:
if user or slug: if user or slug:
author_id_result = session.query(Author.id).filter(or_(Author.user == user, Author.slug == slug)).first() author_id_result = (
session.query(Author.id)
.filter(or_(Author.user == user, Author.slug == slug))
.first()
)
author_id = author_id_result[0] if author_id_result else None author_id = author_id_result[0] if author_id_result else None
if author_id: if author_id:
follows = author_follows_authors(author_id) follows = author_follows_authors(author_id)
@ -141,7 +154,11 @@ def get_author_follows_topics(_, _info, slug='', user=None, author_id=None):
def get_author_follows_authors(_, _info, slug='', user=None, author_id=None): def get_author_follows_authors(_, _info, slug='', user=None, author_id=None):
with local_session() as session: with local_session() as session:
if user or slug: if user or slug:
author_id_result = session.query(Author.id).filter(or_(Author.user == user, Author.slug == slug)).first() author_id_result = (
session.query(Author.id)
.filter(or_(Author.user == user, Author.slug == slug))
.first()
)
author_id = author_id_result[0] if author_id_result else None author_id = author_id_result[0] if author_id_result else None
if author_id: if author_id:
follows = author_follows_topics(author_id) follows = author_follows_topics(author_id)
@ -150,42 +167,6 @@ def get_author_follows_authors(_, _info, slug='', user=None, author_id=None):
raise ValueError('Author not found') raise ValueError('Author not found')
@mutation.field('rate_author')
@login_required
def rate_author(_, info, rated_slug, value):
user_id = info.context['user_id']
with local_session() as session:
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
rater = session.query(Author).filter(Author.slug == user_id).first()
if rater and rated_author:
rating: AuthorRating = (
session.query(AuthorRating)
.filter(
and_(
AuthorRating.rater == rater.id,
AuthorRating.author == rated_author.id,
)
)
.first()
)
if rating:
rating.plus = value > 0
session.add(rating)
session.commit()
return {}
else:
try:
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 {}
def create_author(user_id: str, slug: str, name: str = ''): def create_author(user_id: str, slug: str, name: str = ''):
with local_session() as session: with local_session() as session:
new_author = Author(user=user_id, slug=slug, name=name) new_author = Author(user=user_id, slug=slug, name=name)

137
resolvers/rating.py Normal file
View File

@ -0,0 +1,137 @@
from sqlalchemy import and_
from sqlalchemy.orm import aliased
from orm.author import AuthorRating, Author
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout
from services.auth import login_required
from services.db import local_session
from services.schema import mutation
@mutation.field('rate_author')
@login_required
def rate_author(_, info, rated_slug, value):
user_id = info.context['user_id']
with local_session() as session:
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
rater = session.query(Author).filter(Author.slug == user_id).first()
if rater and rated_author:
rating: AuthorRating = (
session.query(AuthorRating)
.filter(
and_(
AuthorRating.rater == rater.id,
AuthorRating.author == rated_author.id,
)
)
.first()
)
if rating:
rating.plus = value > 0
session.add(rating)
session.commit()
return {}
else:
try:
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 {}
def count_author_comments_rating(session, author_id) -> int:
replied_alias = aliased(Reaction)
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,
)
)
.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,
)
)
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
.count()
) or 0
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,
)
)
.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,
)
)
.count()
or 0
)
return shouts_likes - shouts_dislikes
def load_author_ratings(author: Author):
with local_session() as session:
comments_count = (
session.query(Reaction)
.filter(
and_(
Reaction.created_by == author.id,
Reaction.kind == ReactionKind.COMMENT.value,
Reaction.deleted_at.is_(None),
)
)
.count()
)
likes_count = (
session.query(AuthorRating)
.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))
)
.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

View File

@ -57,7 +57,7 @@ def check_to_feature(session, approver_id, reaction):
"""set shout to public if publicated approvers amount > 4""" """set shout to public if publicated approvers amount > 4"""
if not reaction.reply_to and is_positive(reaction.kind): if not reaction.reply_to and is_positive(reaction.kind):
if is_featured_author(session, approver_id): if is_featured_author(session, approver_id):
approvers = [approver_id, ] approvers = [approver_id]
# now count how many approvers are voted already # now count how many approvers are voted already
reacted_readers = ( reacted_readers = (
session.query(Reaction).where(Reaction.shout == reaction.shout).all() session.query(Reaction).where(Reaction.shout == reaction.shout).all()
@ -148,36 +148,10 @@ async def _create_reaction(session, shout, author, reaction):
return rdict return rdict
@mutation.field('create_reaction') def check_rating(reaction: dict, shout_id: int, session, author: Author):
@login_required
async def create_reaction(_, info, reaction):
user_id = info.context['user_id']
shout_id = reaction.get('shout')
if not shout_id:
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') kind = reaction.get('kind')
shout_id = shout.id
if not kind and isinstance(reaction.get('body'), str):
kind = ReactionKind.COMMENT.value
if not kind:
return {'error': 'cannot create reaction without a kind'}
if kind in RATING_REACTIONS:
opposite_kind = ( opposite_kind = (
ReactionKind.DISLIKE.value ReactionKind.DISLIKE.value if is_positive(kind) else ReactionKind.LIKE.value
if is_positive(kind)
else ReactionKind.LIKE.value
) )
q = select(Reaction).filter( q = select(Reaction).filter(
@ -205,6 +179,38 @@ async def create_reaction(_, info, reaction):
return {'error': 'Remove opposite vote first'} return {'error': 'Remove opposite vote first'}
elif filter(lambda r: r.created_by == author.id, rating_reactions): 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"}
return
@mutation.field('create_reaction')
@login_required
async def create_reaction(_, info, reaction):
user_id = info.context['user_id']
shout_id = reaction.get('shout')
if not shout_id:
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')
shout_id = shout.id
if not kind and isinstance(reaction.get('body'), str):
kind = ReactionKind.COMMENT.value
if not kind:
return {'error': 'cannot create reaction without a kind'}
if kind in RATING_REACTIONS:
result = check_rating(reaction, shout_id, session, author)
if result:
return result
rdict = await _create_reaction(session, shout, author, reaction) rdict = await _create_reaction(session, shout, author, reaction)
return {'reaction': rdict} return {'reaction': rdict}

View File

@ -1,11 +1,11 @@
from sqlalchemy import func, distinct, select, join, and_ from sqlalchemy import func, distinct, select, join
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from orm.reaction import Reaction, ReactionKind
from orm.topic import TopicFollower, Topic from orm.topic import TopicFollower, Topic
from resolvers.rating import load_author_ratings
from services.db import local_session from services.db import local_session
from orm.author import AuthorFollower, Author, AuthorRating from orm.author import AuthorFollower, Author
from orm.shout import ShoutTopic, ShoutAuthor, Shout from orm.shout import ShoutTopic, ShoutAuthor
from services.logger import root_logger as logger from services.logger import root_logger as logger
@ -16,11 +16,22 @@ def add_topic_stat_columns(q):
q = ( q = (
q.outerjoin(aliased_shout_topic, aliased_shout_topic.topic == Topic.id) q.outerjoin(aliased_shout_topic, aliased_shout_topic.topic == Topic.id)
.add_columns(func.count(distinct(aliased_shout_topic.shout)).label("shouts_stat")) .add_columns(
.outerjoin(aliased_shout_author, aliased_shout_topic.shout == aliased_shout_author.shout) func.count(distinct(aliased_shout_topic.shout)).label('shouts_stat')
.add_columns(func.count(distinct(aliased_shout_author.author)).label("authors_stat")) )
.outerjoin(
aliased_shout_author,
aliased_shout_topic.shout == aliased_shout_author.shout,
)
.add_columns(
func.count(distinct(aliased_shout_author.author)).label('authors_stat')
)
.outerjoin(aliased_topic_follower) .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) q = q.group_by(Topic.id)
@ -35,11 +46,21 @@ def add_author_stat_columns(q):
q = ( q = (
q.outerjoin(aliased_shout_author, aliased_shout_author.author == Author.id) q.outerjoin(aliased_shout_author, aliased_shout_author.author == Author.id)
.add_columns(func.count(distinct(aliased_shout_author.shout)).label("shouts_stat")) .add_columns(
func.count(distinct(aliased_shout_author.shout)).label('shouts_stat')
)
.outerjoin(aliased_author_authors, aliased_author_authors.follower == Author.id) .outerjoin(aliased_author_authors, aliased_author_authors.follower == Author.id)
.add_columns(func.count(distinct(aliased_shout_author.author)).label("authors_stat")) .add_columns(
.outerjoin(aliased_author_followers, aliased_author_followers.author == Author.id) func.count(distinct(aliased_shout_author.author)).label('authors_stat')
.add_columns(func.count(distinct(aliased_author_followers.follower)).label("followers_stat")) )
.outerjoin(
aliased_author_followers, aliased_author_followers.author == Author.id
)
.add_columns(
func.count(distinct(aliased_author_followers.follower)).label(
'followers_stat'
)
)
) )
q = q.group_by(Author.id) q = q.group_by(Author.id)
@ -47,104 +68,6 @@ def add_author_stat_columns(q):
return q return q
def count_author_comments_rating(session, author_id) -> int:
replied_alias = aliased(Reaction)
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,
)
)
.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,
)
)
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
.count()
) or 0
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,
)
)
.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,
)
)
.count()
or 0
)
return shouts_likes - shouts_dislikes
def load_author_ratings(author: Author):
with local_session() as session:
comments_count = (
session.query(Reaction)
.filter(
and_(
Reaction.created_by == author.id,
Reaction.kind == ReactionKind.COMMENT.value,
Reaction.deleted_at.is_(None),
)
)
.count()
)
likes_count = (
session.query(AuthorRating)
.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)
)
)
.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
def execute_with_ministat(q): def execute_with_ministat(q):
records = [] records = []
with local_session() as session: with local_session() as session:
@ -176,11 +99,11 @@ def get_topics_with_stat(q):
def author_follows_authors(author_id: int): def author_follows_authors(author_id: int):
af = aliased(AuthorFollower, name="af") af = aliased(AuthorFollower, name='af')
q = ( q = (
select(Author).select_from( select(Author)
join(Author, af, Author.id == int(af.author)) .select_from(join(Author, af, Author.id == int(af.author)))
).where(af.follower == author_id) .where(af.follower == author_id)
) )
q = add_author_stat_columns(q) q = add_author_stat_columns(q)
return execute_with_ministat(q) return execute_with_ministat(q)
@ -188,9 +111,9 @@ def author_follows_authors(author_id: int):
def author_follows_topics(author_id: int): def author_follows_topics(author_id: int):
q = ( q = (
select(Topic).select_from( select(Topic)
join(Topic, TopicFollower, Topic.id == TopicFollower.topic) .select_from(join(Topic, TopicFollower, Topic.id == TopicFollower.topic))
).where(TopicFollower.follower == author_id) .where(TopicFollower.follower == author_id)
) )
q = add_topic_stat_columns(q) q = add_topic_stat_columns(q)
@ -207,5 +130,5 @@ def query_follows(author_id: int):
'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}],
} }
except Exception as e: except Exception as e:
logger.exception(f"An error occurred while executing query_follows: {e}") logger.exception(f'An error occurred while executing query_follows: {e}')
raise Exception("An error occurred while executing query_follows") from e raise Exception('An error occurred while executing query_follows') from e

View File

@ -28,16 +28,15 @@ async def update_author_cache(author: Author, ttl=25 * 60 * 60):
@event.listens_for(Shout, 'after_update') @event.listens_for(Shout, 'after_update')
def after_shouts_update(mapper, connection, shout: Shout): def after_shouts_update(mapper, connection, shout: Shout):
# Создаем подзапрос для проверки наличия авторов в списке shout.authors # Создаем подзапрос для проверки наличия авторов в списке shout.authors
subquery = ( subquery = select(1).where(
select(1) or_(
.where(or_(
Author.id == shout.created_by, Author.id == shout.created_by,
and_( and_(
Shout.id == shout.id, Shout.id == shout.id,
ShoutAuthor.shout == Shout.id, ShoutAuthor.shout == Shout.id,
ShoutAuthor.author == Author.id ShoutAuthor.author == Author.id,
),
) )
))
) )
# Основной запрос с использованием объединения и подзапроса exists # Основной запрос с использованием объединения и подзапроса exists
@ -45,10 +44,7 @@ def after_shouts_update(mapper, connection, shout: Shout):
select(Author) select(Author)
.join(ShoutAuthor, Author.id == ShoutAuthor.author) .join(ShoutAuthor, Author.id == ShoutAuthor.author)
.where(ShoutAuthor.shout == shout.id) .where(ShoutAuthor.shout == shout.id)
.union( .union(select(Author).where(exists(subquery)))
select(Author)
.where(exists(subquery))
)
) )
authors = get_authors_with_stat(authors_query, ratings=True) authors = get_authors_with_stat(authors_query, ratings=True)
for author in authors: for author in authors:
@ -57,10 +53,7 @@ def after_shouts_update(mapper, connection, shout: Shout):
@event.listens_for(Reaction, 'after_insert') @event.listens_for(Reaction, 'after_insert')
def after_reaction_insert(mapper, connection, reaction: Reaction): def after_reaction_insert(mapper, connection, reaction: Reaction):
author_subquery = ( author_subquery = select(Author).where(Author.id == reaction.created_by)
select(Author)
.where(Author.id == reaction.created_by)
)
replied_author_subquery = ( replied_author_subquery = (
select(Author) select(Author)
.join(Reaction, Author.id == Reaction.created_by) .join(Reaction, Author.id == Reaction.created_by)
@ -112,7 +105,9 @@ def after_author_follower_delete(mapper, connection, target: AuthorFollower):
) )
async def update_follows_for_user(connection, user_id, entity_type, entity: dict, is_insert): async def update_follows_for_user(
connection, user_id, entity_type, entity: dict, is_insert
):
redis_key = f'user:{user_id}:follows' redis_key = f'user:{user_id}:follows'
follows_str = await redis.get(redis_key) follows_str = await redis.get(redis_key)
if follows_str: if follows_str:
@ -123,13 +118,17 @@ async def update_follows_for_user(connection, user_id, entity_type, entity: dict
follows[f'{entity_type}s'].append(entity) follows[f'{entity_type}s'].append(entity)
else: else:
# Remove the entity from follows # Remove the entity from follows
follows[f'{entity_type}s'] = [e for e in follows[f'{entity_type}s'] if e['id'] != entity['id']] follows[f'{entity_type}s'] = [
e for e in follows[f'{entity_type}s'] if e['id'] != entity['id']
]
await redis.execute('SET', redis_key, json.dumps(follows)) await redis.execute('SET', redis_key, json.dumps(follows))
async def handle_author_follower_change(connection, author_id: int, follower_id: int, is_insert: bool): async def handle_author_follower_change(
connection, author_id: int, follower_id: int, is_insert: bool
):
author_query = select(Author).filter(Author.id == author_id) author_query = select(Author).filter(Author.id == author_id)
[author, ] = get_authors_with_stat(author_query, ratings=True) [author] = get_authors_with_stat(author_query, ratings=True)
follower_query = select(Author).filter(Author.id == follower_id) follower_query = select(Author).filter(Author.id == follower_id)
follower = get_authors_with_stat(follower_query, ratings=True) follower = get_authors_with_stat(follower_query, ratings=True)
if follower and author: if follower and author:
@ -151,7 +150,9 @@ async def handle_author_follower_change(connection, author_id: int, follower_id:
) )
async def handle_topic_follower_change(connection, topic_id: int, follower_id: int, is_insert: bool): async def handle_topic_follower_change(
connection, topic_id: int, follower_id: int, is_insert: bool
):
q = select(Topic).filter(Topic.id == topic_id) q = select(Topic).filter(Topic.id == topic_id)
topics = get_topics_with_stat(q) topics = get_topics_with_stat(q)
topic = topics[0] topic = topics[0]