featured-id-patch
All checks were successful
Deploy to core / deploy (push) Successful in 1m40s

This commit is contained in:
2024-02-02 15:03:44 +03:00
parent bd5f910f8c
commit c00361b2ec
19 changed files with 640 additions and 798 deletions

View File

@@ -82,35 +82,6 @@ async def update_profile(_, info, profile):
return {'error': None, 'author': author}
# for mutation.field("follow")
def author_follow(follower_id, slug):
try:
with local_session() as session:
author = session.query(Author).where(Author.slug == slug).one()
af = AuthorFollower(follower=follower_id, author=author.id)
session.add(af)
session.commit()
return True
except Exception:
return False
# for mutation.field("unfollow")
def author_unfollow(follower_id, slug):
with local_session() as session:
flw = (
session.query(AuthorFollower)
.join(Author, Author.id == AuthorFollower.author)
.filter(and_(AuthorFollower.follower == follower_id, Author.slug == slug))
.first()
)
if flw:
session.delete(flw)
session.commit()
return True
return False
# TODO: caching query
@query.field('get_authors_all')
async def get_authors_all(_, _info):

View File

@@ -4,11 +4,14 @@ from sqlalchemy import and_, select
from sqlalchemy.orm import joinedload
from orm.author import Author
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
from orm.topic import Topic
from resolvers.reaction import reactions_follow, reactions_unfollow
from resolvers.follower import reactions_follow, reactions_unfollow
from resolvers.rater import is_negative, is_positive
from services.auth import login_required
from services.db import local_session
from services.diff import apply_diff, get_diff
from services.notify import notify_shout
from services.schema import mutation, query
from services.search import search_service
@@ -187,7 +190,8 @@ async def update_shout( # noqa: C901
if not publish:
await notify_shout(shout_dict, 'update')
if shout.visibility is ShoutVisibility.COMMUNITY.value or shout.visibility is ShoutVisibility.PUBLIC.value:
else:
await notify_shout(shout_dict, 'published')
# search service indexing
search_service.index(shout)
@@ -217,3 +221,30 @@ async def delete_shout(_, info, shout_id):
session.commit()
await notify_shout(shout_dict, 'delete')
return {}
def handle_proposing(session, r, shout):
if is_positive(r.kind):
# Proposal accepting logic
replied_reaction = session.query(Reaction).filter(Reaction.id == r.reply_to).first()
if replied_reaction and replied_reaction.kind is ReactionKind.PROPOSE.value and replied_reaction.quote:
# patch all the proposals' quotes
proposals = session.query(Reaction).filter(and_(Reaction.shout == r.shout, Reaction.kind == ReactionKind.PROPOSE.value)).all()
for proposal in proposals:
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)
Reaction.update(proposal, proposal_dict)
session.add(proposal)
# patch shout's body
shout_dict = shout.dict()
shout_dict['body'] = replied_reaction.quote
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()
if is_negative(r.kind):
# TODO: rejection logic
pass

View File

@@ -2,15 +2,14 @@ import logging
from typing import List
from sqlalchemy.orm import aliased
from sqlalchemy.sql import and_
from orm.author import Author, AuthorFollower
from orm.community import Community
from orm.reaction import Reaction
from orm.shout import Shout
from orm.shout import Shout, ShoutReactionsFollower
from orm.topic import Topic, TopicFollower
from resolvers.author import author_follow, author_unfollow
from resolvers.community import community_follow, community_unfollow
from resolvers.reaction import reactions_follow, reactions_unfollow
from resolvers.topic import topic_follow, topic_unfollow
from services.auth import login_required
from services.db import local_session
@@ -140,3 +139,83 @@ def get_shout_followers(_, _info, slug: str = '', shout_id: int | None = None) -
followers.append(r.created_by)
return followers
def reactions_follow(author_id, shout_id, auto=False):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
following = (
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
.first()
)
if not following:
following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
session.add(following)
session.commit()
return True
except Exception:
return False
def reactions_unfollow(author_id, shout_id: int):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
following = (
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
.first()
)
if following:
session.delete(following)
session.commit()
return True
except Exception as ex:
logger.debug(ex)
return False
# for mutation.field("follow")
def author_follow(follower_id, slug):
try:
with local_session() as session:
author = session.query(Author).where(Author.slug == slug).one()
af = AuthorFollower(follower=follower_id, author=author.id)
session.add(af)
session.commit()
return True
except Exception:
return False
# for mutation.field("unfollow")
def author_unfollow(follower_id, slug):
with local_session() as session:
flw = (
session.query(AuthorFollower)
.join(Author, Author.id == AuthorFollower.author)
.filter(and_(AuthorFollower.follower == follower_id, Author.slug == slug))
.first()
)
if flw:
session.delete(flw)
session.commit()
return True
return False

28
resolvers/rater.py Normal file
View File

@@ -0,0 +1,28 @@
from orm.reaction import ReactionKind
RATING_REACTIONS = [
ReactionKind.LIKE.value,
ReactionKind.ACCEPT.value,
ReactionKind.AGREE.value,
ReactionKind.DISLIKE.value,
ReactionKind.REJECT.value,
ReactionKind.DISAGREE.value]
def is_negative(x):
return x in [
ReactionKind.ACCEPT.value,
ReactionKind.LIKE.value,
ReactionKind.PROOF.value,
]
def is_positive(x):
return x in [
ReactionKind.ACCEPT.value,
ReactionKind.LIKE.value,
ReactionKind.PROOF.value,
]

View File

@@ -8,7 +8,10 @@ from sqlalchemy.sql import union
from orm.author import Author
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutReactionsFollower, ShoutVisibility
from orm.shout import Shout, ShoutVisibility
from resolvers.editor import handle_proposing
from resolvers.follower import reactions_follow
from resolvers.rater import RATING_REACTIONS, is_negative, is_positive
from services.auth import add_user_role, login_required
from services.db import local_session
from services.notify import notify_reaction
@@ -37,120 +40,52 @@ def add_stat_columns(q, aliased_reaction):
return q
def reactions_follow(author_id, shout_id, auto=False):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
following = (
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
.first()
)
if not following:
following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
session.add(following)
session.commit()
return True
except Exception:
return False
def reactions_unfollow(author_id, shout_id: int):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
following = (
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
.first()
)
if following:
session.delete(following)
session.commit()
return True
except Exception as ex:
logger.debug(ex)
return False
def is_published_author(session, author_id):
"""checks if author has at least one publication"""
def is_featured_author(session, author_id):
"""checks if author has at least one featured publication"""
return (
session.query(Shout)
.where(Shout.authors.any(id=author_id))
.filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
.filter(and_(Shout.featured_at.is_not(None), Shout.deleted_at.is_(None)))
.count()
> 0
)
def is_negative(x):
return x in [
ReactionKind.ACCEPT.value,
ReactionKind.LIKE.value,
ReactionKind.PROOF.value,
]
def is_positive(x):
return x in [
ReactionKind.ACCEPT.value,
ReactionKind.LIKE.value,
ReactionKind.PROOF.value,
]
def check_to_publish(session, approver_id, reaction):
def check_to_feature(session, approver_id, reaction):
"""set shout to public if publicated approvers amount > 4"""
if not reaction.reply_to and is_positive(reaction.kind):
if is_published_author(session, approver_id):
if is_featured_author(session, approver_id):
approvers = []
approvers.append(approver_id)
# now count how many approvers are voted already
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
approvers = [
approver_id,
]
for ar in approvers_reactions:
a = ar.created_by
if is_published_author(session, a):
approvers.append(a)
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)
if len(approvers) > 4:
return True
return False
def check_to_hide(session, reaction):
"""hides any shout if 20% of reactions are negative"""
def check_to_unfeature(session, rejecter_id, reaction):
"""unfeature any shout if 20% of reactions are negative"""
if not reaction.reply_to and is_negative(reaction.kind):
# if is_published_author(author_id):
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
rejects = 0
for r in approvers_reactions:
if is_negative(r.kind):
rejects += 1
if len(approvers_reactions) / rejects < 5:
return True
if is_featured_author(session, rejecter_id):
reactions = session.query(Reaction).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()
if is_featured_author(session, approver):
if is_negative(r.kind):
rejects += 1
if len(reactions) / rejects < 5:
return True
return False
async def set_published(session, shout_id, approver_id):
async def set_featured(session, shout_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})
s.featured_at = int(time.time())
Shout.update(s, {'visibility': ShoutVisibility.FEATURED.value})
author = session.query(Author).filter(Author.id == s.created_by).first()
if author:
await add_user_role(str(author.user))
@@ -158,7 +93,7 @@ async def set_published(session, shout_id, approver_id):
session.commit()
def set_hidden(session, shout_id):
def set_unfeatured(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
Shout.update(s, {'visibility': ShoutVisibility.COMMUNITY.value})
session.add(s)
@@ -171,38 +106,24 @@ async def _create_reaction(session, shout, author, reaction):
session.commit()
rdict = r.dict()
# Proposal accepting logic
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 = 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.update(shout, shout_dict)
session.add(shout)
session.commit()
# collaborative editing
if rdict.get('reply_to') and r.kind in RATING_REACTIONS and author.id in shout.authors:
handle_proposing(session, r, shout)
# Self-regulation mechanics
if check_to_hide(session, r):
set_hidden(session, shout.id)
elif check_to_publish(session, author.id, r):
await set_published(session, shout.id, author.id)
# self-regultaion mechanics
if check_to_unfeature(session, author.id, r):
set_unfeatured(session, shout.id)
elif check_to_feature(session, author.id, r):
await set_featured(session, shout.id)
# Reactions auto-following
# reactions auto-following
reactions_follow(author.id, reaction['shout'], True)
rdict['shout'] = shout.dict()
rdict['created_by'] = author.dict()
rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0}
# Notifications call
# notifications call
await notify_reaction(rdict, 'create')
return rdict
@@ -220,14 +141,14 @@ async def create_reaction(_, info, reaction):
try:
with local_session() as session:
shout = session.query(Shout).filter(Shout.id == shout_id).one()
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 reaction.get('body'):
if not kind and isinstance(reaction.get('body'), str):
kind = ReactionKind.COMMENT.value
if not kind:

View File

@@ -26,9 +26,9 @@ 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')
if by_published:
q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC.value)
by_featured = filters.get('featured')
if by_featured:
q = q.filter(Shout.visibility == ShoutVisibility.FEATURED.value)
by_layouts = filters.get('layouts')
if by_layouts:
q = q.filter(Shout.layout.in_(by_layouts))
@@ -114,7 +114,7 @@ async def load_shouts_by(_, _info, options):
filters: {
layouts: ['audio', 'video', ..],
reacted: True,
published: True, // filter published-only
featured: True, // filter featured-only
author: 'discours',
topic: 'culture',
after: 1234567 // unixtime
@@ -143,13 +143,14 @@ async def load_shouts_by(_, _info, options):
q = add_stat_columns(q, aliased_reaction)
# filters
q = apply_filters(q, 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.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)
q = q.order_by(nulls_last(query_order_by))
@@ -274,9 +275,10 @@ async def load_shouts_feed(_, info, options):
aliased_reaction = aliased(Reaction)
q = add_stat_columns(q, aliased_reaction)
q = apply_filters(q, options.get('filters', {}), reader.id)
filters = options.get('filters', {})
q = apply_filters(q, filters, reader.id)
order_by = options.get('order_by', 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)
@@ -464,7 +466,7 @@ async def load_shouts_random_topic(_, info, limit: int = 10):
.filter(
and_(
Shout.deleted_at.is_(None),
Shout.visibility == ShoutVisibility.PUBLIC.value,
Shout.visibility == ShoutVisibility.FEATURED.value,
Shout.topics.any(slug=topic.slug),
)
)

View File

@@ -1,36 +0,0 @@
import os
import re
from starlette.endpoints import HTTPEndpoint
from starlette.requests import Request
from starlette.responses import JSONResponse
from orm.author import Author
from resolvers.author import create_author
from services.db import local_session
class WebhookEndpoint(HTTPEndpoint):
async def post(self, request: Request) -> JSONResponse:
try:
data = await request.json()
if data:
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())
with local_session() as session:
author = session.query(Author).filter(Author.slug == slug).first()
if author:
slug = slug + '-' + user_id.split('-').pop()
await create_author(user_id, slug, name)
return JSONResponse({'status': 'success'})
except Exception as e:
import traceback
traceback.print_exc()
return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)