fixes-api

This commit is contained in:
2022-11-21 11:13:57 +03:00
parent 4c4f18147e
commit 5ad8328c03
17 changed files with 187 additions and 170 deletions

View File

@@ -0,0 +1,47 @@
from auth.authenticate import login_required
from base.resolvers import mutation
# from resolvers.community import community_follow, community_unfollow
from resolvers.zine.profile import author_follow, author_unfollow
from resolvers.zine.reactions import reactions_follow, reactions_unfollow
from resolvers.zine.topics import topic_follow, topic_unfollow
@mutation.field("follow")
@login_required
async def follow(_, info, what, slug):
user = info.context["request"].user
try:
if what == "AUTHOR":
author_follow(user, slug)
elif what == "TOPIC":
topic_follow(user, slug)
elif what == "COMMUNITY":
# community_follow(user, slug)
pass
elif what == "REACTIONS":
reactions_follow(user, slug)
except Exception as e:
return {"error": str(e)}
return {}
@mutation.field("unfollow")
@login_required
async def unfollow(_, info, what, slug):
user = info.context["request"].user
try:
if what == "AUTHOR":
author_unfollow(user, slug)
elif what == "TOPIC":
topic_unfollow(user, slug)
elif what == "COMMUNITY":
# community_unfollow(user, slug)
pass
elif what == "REACTIONS":
reactions_unfollow(user, slug)
except Exception as e:
return {"error": str(e)}
return {}

123
resolvers/zine/load.py Normal file
View File

@@ -0,0 +1,123 @@
from datetime import datetime, timedelta
import sqlalchemy as sa
from sqlalchemy.orm import selectinload
from sqlalchemy.sql.expression import desc, asc, select, case
from base.orm import local_session
from base.resolvers import query
from orm.shout import Shout
from orm.reaction import Reaction, ReactionKind
from services.zine.shoutauthor import ShoutAuthorStorage
from services.stat.reacted import ReactedStorage
def apply_filters(filters, q, user=None):
if filters.get("reacted") and user:
q.join(Reaction, Reaction.createdBy == user.slug)
if filters.get("visibility"):
q = q.filter(Shout.visibility == filters.get("visibility"))
if filters.get("layout"):
q = q.filter(Shout.layout == filters.get("layout"))
if filters.get("author"):
q = q.filter(Shout.authors.any(slug=filters.get("author")))
if filters.get("topic"):
q = q.filter(Shout.topics.any(slug=filters.get("topic")))
if filters.get("title"):
q = q.filter(Shout.title.ilike(f'%{filters.get("title")}%'))
if filters.get("body"):
q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s'))
if filters.get("days"):
before = datetime.now() - timedelta(days=int(filters.get("days")) or 30)
q = q.filter(Shout.createdAt > before)
return q
def extract_order(o, q):
if o:
q = q.add_columns(sa.func.count(Reaction.id).label(o))
if o == 'comments':
q = q.join(Reaction, Shout.slug == Reaction.shout)
q = q.filter(Reaction.body.is_not(None))
elif o == 'reacted':
q = q.join(
Reaction
).add_columns(
sa.func.max(Reaction.createdAt).label(o)
)
elif o == "rating":
q = q.join(Reaction).add_columns(sa.func.sum(case(
(Reaction.kind == ReactionKind.AGREE, 1),
(Reaction.kind == ReactionKind.DISAGREE, -1),
(Reaction.kind == ReactionKind.PROOF, 1),
(Reaction.kind == ReactionKind.DISPROOF, -1),
(Reaction.kind == ReactionKind.ACCEPT, 1),
(Reaction.kind == ReactionKind.REJECT, -1),
(Reaction.kind == ReactionKind.LIKE, 1),
(Reaction.kind == ReactionKind.DISLIKE, -1),
else_=0
)).label(o))
return o
else:
return 'createdAt'
@query.field("loadShout")
async def load_shout(_, info, slug):
with local_session() as session:
shout = session.query(Shout).options(
# TODO add cation
selectinload(Shout.authors),
selectinload(Shout.topics),
).filter(
Shout.slug == slug
).filter(
Shout.deletedAt.is_(None)
).one()
return shout
@query.field("loadShouts")
async def load_shouts_by(_, info, options):
"""
:param options: {
filters: {
layout: 'audio',
visibility: "public",
author: 'discours',
topic: 'culture',
title: 'something',
body: 'something else',
days: 30
}
offset: 0
limit: 50
order_by: 'createdAt'
order_by_desc: true
}
:return: Shout[]
"""
q = select(Shout).options(
# TODO add caption
selectinload(Shout.authors),
selectinload(Shout.topics),
).where(
Shout.deletedAt.is_(None)
)
user = info.context["request"].user
q = apply_filters(options.get("filters"), q, user)
order_by = extract_order(options.get("order_by"))
query_order_by = desc(order_by) if options.get("order_by_desc") else asc(order_by)
offset = options.get("offset", 0)
limit = options.get("limit", 10)
q = q.group_by(Shout.id).order_by(query_order_by).limit(limit).offset(offset)
with local_session() as session:
shouts = list(map(lambda r: r.Shout, session.execute(q)))
for s in shouts:
s.stat = await ReactedStorage.get_shout_stat(s.slug)
for a in s.authors:
a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug)
return shouts

223
resolvers/zine/profile.py Normal file
View File

@@ -0,0 +1,223 @@
from typing import List
from datetime import datetime, timedelta
from sqlalchemy import and_, func
from sqlalchemy.orm import selectinload
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction
from orm.topic import Topic, TopicFollower
from orm.user import AuthorFollower, Role, User, UserRating, UserRole
from services.stat.reacted import ReactedStorage
from services.stat.topicstat import TopicStat
from services.zine.shoutauthor import ShoutAuthor
# from .community import followed_communities
from resolvers.inbox.unread import get_total_unread_counter
from .topics import get_topic_stat
async def user_subscriptions(slug: str):
return {
"unread": await get_total_unread_counter(slug), # unread inbox messages counter
"topics": [t.slug for t in await followed_topics(slug)], # followed topics slugs
"authors": [a.slug for a in await followed_authors(slug)], # followed authors slugs
"reactions": await ReactedStorage.get_shouts_by_author(slug),
# "communities": [c.slug for c in followed_communities(slug)], # communities
}
async def get_author_stat(slug):
# TODO: implement author stat
with local_session() as session:
return {
"shouts": session.query(ShoutAuthor).where(ShoutAuthor.user == slug).count(),
"followers": session.query(AuthorFollower).where(AuthorFollower.author == slug).count(),
"followings": session.query(AuthorFollower).where(AuthorFollower.follower == slug).count(),
"rating": session.query(func.sum(UserRating.value)).where(UserRating.user == slug).first(),
"commented": session.query(
Reaction.id
).where(
Reaction.createdBy == slug
).filter(
func.length(Reaction.body) > 0
).count()
}
@query.field("userFollowedTopics")
@login_required
async def get_followed_topics(_, info, slug) -> List[Topic]:
return await followed_topics(slug)
async def followed_topics(slug):
topics = []
with local_session() as session:
topics = (
session.query(Topic)
.join(TopicFollower)
.where(TopicFollower.follower == slug)
.all()
)
for topic in topics:
topic.stat = await get_topic_stat(topic.slug)
return topics
@query.field("userFollowedAuthors")
async def get_followed_authors(_, _info, slug) -> List[User]:
return await followed_authors(slug)
async def followed_authors(slug) -> List[User]:
authors = []
with local_session() as session:
authors = (
session.query(User)
.join(AuthorFollower, User.slug == AuthorFollower.author)
.where(AuthorFollower.follower == slug)
.all()
)
for author in authors:
author.stat = await get_author_stat(author.slug)
return authors
@query.field("userFollowers")
async def user_followers(_, _info, slug) -> List[User]:
with local_session() as session:
users = (
session.query(User)
.join(AuthorFollower, User.slug == AuthorFollower.follower)
.where(AuthorFollower.author == slug)
.all()
)
return users
async def get_user_roles(slug):
with local_session() as session:
user = session.query(User).where(User.slug == slug).first()
roles = (
session.query(Role)
.options(selectinload(Role.permissions))
.join(UserRole)
.where(UserRole.user_id == user.id)
.all()
)
return roles
@mutation.field("updateProfile")
@login_required
async def update_profile(_, info, profile):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
user = session.query(User).filter(User.id == user_id).first()
if user:
User.update(user, **profile)
session.add(user)
session.commit()
return {}
@mutation.field("rateUser")
@login_required
async def rate_user(_, info, rated_userslug, value):
user = info.context["request"].user
with local_session() as session:
rating = (
session.query(UserRating)
.filter(and_(UserRating.rater == user.slug, UserRating.user == rated_userslug))
.first()
)
if rating:
rating.value = value
session.commit()
return {}
try:
UserRating.create(rater=user.slug, user=rated_userslug, value=value)
except Exception as err:
return {"error": err}
return {}
# for mutation.field("follow")
def author_follow(user, slug):
with local_session() as session:
af = AuthorFollower.create(follower=user.slug, author=slug)
session.add(af)
session.commit()
# for mutation.field("unfollow")
def author_unfollow(user, slug):
with local_session() as session:
flw = (
session.query(AuthorFollower)
.filter(
and_(
AuthorFollower.follower == user.slug, AuthorFollower.author == slug
)
)
.first()
)
if not flw:
raise Exception("[resolvers.profile] follower not exist, cant unfollow")
else:
session.delete(flw)
session.commit()
@query.field("authorsAll")
async def get_authors_all(_, _info):
with local_session() as session:
authors = session.query(User).join(ShoutAuthor).all()
for author in authors:
author.stat = await get_author_stat(author.slug)
return authors
@query.field("getAuthor")
async def get_author(_, _info, slug):
with local_session() as session:
author = session.query(User).join(ShoutAuthor).where(User.slug == slug).first()
for author in author:
author.stat = await get_author_stat(author.slug)
return author
@query.field("loadAuthorsBy")
async def load_authors_by(_, info, by, limit, offset):
authors = []
with local_session() as session:
aq = session.query(User)
if by.get("slug"):
aq = aq.filter(User.slug.ilike(f"%{by['slug']}%"))
elif by.get("name"):
aq = aq.filter(User.name.ilike(f"%{by['name']}%"))
elif by.get("topic"):
aaa = list(map(lambda a: a.slug, TopicStat.authors_by_topic.get(by["topic"])))
aq = aq.filter(User.name._in(aaa))
if by.get("lastSeen"): # in days
days_before = datetime.now() - timedelta(days=by["lastSeen"])
aq = aq.filter(User.lastSeen > days_before)
elif by.get("createdAt"): # in days
days_before = datetime.now() - timedelta(days=by["createdAt"])
aq = aq.filter(User.createdAt > days_before)
aq = aq.group_by(
User.id
).order_by(
by.get("order") or "createdAt"
).limit(limit).offset(offset)
print(aq)
authors = list(map(lambda r: r.User, session.execute(aq)))
if by.get("stat"):
for a in authors:
a.stat = await get_author_stat(a.slug)
authors = list(set(authors))
# authors = sorted(authors, key=lambda a: a["stat"].get(by.get("stat")))
return authors

254
resolvers/zine/reactions.py Normal file
View File

@@ -0,0 +1,254 @@
from datetime import datetime, timedelta
from sqlalchemy import and_, desc, select, text, func
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutReactionsFollower
from orm.user import User
from services.stat.reacted import ReactedStorage
async def get_reaction_stat(reaction_id):
return {
# "viewed": await ViewedStorage.get_reaction(reaction_id),
"reacted": len(await ReactedStorage.get_reaction(reaction_id)),
"rating": await ReactedStorage.get_reaction_rating(reaction_id),
"commented": len(await ReactedStorage.get_reaction_comments(reaction_id)),
}
def reactions_follow(user: User, slug: str, auto=False):
with local_session() as session:
following = (
session.query(ShoutReactionsFollower)
.where(and_(
ShoutReactionsFollower.follower == user.slug,
ShoutReactionsFollower.shout == slug
))
.first()
)
if not following:
following = ShoutReactionsFollower.create(
follower=user.slug,
shout=slug,
auto=auto
)
session.add(following)
session.commit()
def reactions_unfollow(user, slug):
with local_session() as session:
following = (
session.query(ShoutReactionsFollower)
.where(and_(
ShoutReactionsFollower.follower == user.slug,
ShoutReactionsFollower.shout == slug
))
.first()
)
if following:
session.delete(following)
session.commit()
def is_published_author(session, userslug):
''' checks if user has at least one publication '''
return session.query(
Shout
).where(
Shout.authors.contains(userslug)
).filter(
and_(
Shout.publishedAt.is_not(None),
Shout.deletedAt.is_(None)
)
).count() > 0
def check_to_publish(session, user, reaction):
''' set shout to public if publicated approvers amount > 4 '''
if not reaction.replyTo and reaction.kind in [
ReactionKind.ACCEPT,
ReactionKind.LIKE,
ReactionKind.PROOF
]:
if is_published_author(user):
# now count how many approvers are voted already
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
approvers = [user.slug, ]
for ar in approvers_reactions:
a = ar.createdBy
if is_published_author(session, a):
approvers.append(a)
if len(approvers) > 4:
return True
return False
def check_to_hide(session, user, reaction):
''' hides any shout if 20% of reactions are negative '''
if not reaction.replyTo and reaction.kind in [
ReactionKind.DECLINE,
ReactionKind.UNLIKE,
ReactionKind.UNPROOF
]:
# if is_published_author(user):
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
declines = 0
for r in approvers_reactions:
if r.kind in [
ReactionKind.DECLINE,
ReactionKind.UNLIKE,
ReactionKind.UNPROOF
]:
declines += 1
if len(approvers_reactions) / declines < 5:
return True
return False
def set_published(session, slug, publisher):
s = session.query(Shout).where(Shout.slug == slug).first()
s.publishedAt = datetime.now()
s.publishedBy = publisher
s.visibility = text('public')
session.add(s)
session.commit()
def set_hidden(session, slug):
s = session.query(Shout).where(Shout.slug == slug).first()
s.visibility = text('authors')
s.publishedAt = None # TODO: discuss
s.publishedBy = None # TODO: store changes history in git
session.add(s)
session.commit()
@mutation.field("createReaction")
@login_required
async def create_reaction(_, info, inp):
user = info.context["request"].user
with local_session() as session:
reaction = Reaction.create(**inp)
session.add(reaction)
session.commit()
# self-regulation mechanics
if check_to_hide(session, user, reaction):
set_hidden(session, reaction.shout)
elif check_to_publish(session, user, reaction):
set_published(session, reaction.shout, reaction.createdBy)
ReactedStorage.react(reaction)
try:
reactions_follow(user, inp["shout"], True)
except Exception as e:
print(f"[resolvers.reactions] error on reactions autofollowing: {e}")
reaction.stat = await get_reaction_stat(reaction.id)
return {"reaction": reaction}
@mutation.field("updateReaction")
@login_required
async def update_reaction(_, info, inp):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
reaction = session.query(Reaction).filter(Reaction.id == inp.id).first()
if not reaction:
return {"error": "invalid reaction id"}
if reaction.createdBy != user.slug:
return {"error": "access denied"}
reaction.body = inp["body"]
reaction.updatedAt = datetime.now()
if reaction.kind != inp["kind"]:
# NOTE: change mind detection can be here
pass
if inp.get("range"):
reaction.range = inp.get("range")
session.commit()
reaction.stat = await get_reaction_stat(reaction.id)
return {"reaction": reaction}
@mutation.field("deleteReaction")
@login_required
async def delete_reaction(_, info, rid):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
reaction = session.query(Reaction).filter(Reaction.id == rid).first()
if not reaction:
return {"error": "invalid reaction id"}
if reaction.createdBy != user.slug:
return {"error": "access denied"}
reaction.deletedAt = datetime.now()
session.commit()
return {}
@query.field("loadReactionsBy")
async def load_reactions_by(_, info, by, limit=50, offset=0):
"""
:param by: {
shout: 'some-slug'
author: 'discours',
topic: 'culture',
body: 'something else',
stat: 'rating' | 'comments' | 'reacted' | 'views',
days: 30
}
:param limit: int amount of shouts
:param offset: int offset in this order
:return: Reaction[]
"""
q = select(Reaction).join(
Shout
).where(
Reaction.deletedAt.is_(None)
)
if by.get("slug"):
q = q.filter(Shout.slug == by["slug"])
else:
if by.get("reacted"):
user = info.context["request"].user
q = q.filter(Reaction.createdBy == user.slug)
if by.get("author"):
q = q.filter(Reaction.createdBy == by["author"])
if by.get("topic"):
q = q.filter(Shout.topics.contains(by["topic"]))
if by.get("body"):
if by["body"] is True:
q = q.filter(func.length(Reaction.body) > 0)
else:
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
if by.get("days"):
before = datetime.now() - timedelta(days=int(by["days"]) or 30)
q = q.filter(Reaction.createdAt > before)
q = q.group_by(Reaction.id).order_by(
desc(by.get("order") or Reaction.createdAt)
).limit(limit).offset(offset)
rrr = []
with local_session() as session:
# post query stats and author's captions
for r in list(map(lambda r: r.Reaction, session.execute(q))):
r.stat = await get_reaction_stat(r.id)
rrr.append(r)
if by.get("stat"):
rrr.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)
return rrr

123
resolvers/zine/topics.py Normal file
View File

@@ -0,0 +1,123 @@
import random
from sqlalchemy import and_
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.topic import Topic, TopicFollower
from services.zine.topics import TopicStorage
from services.stat.reacted import ReactedStorage
from services.stat.topicstat import TopicStat
# from services.stat.viewed import ViewedStorage
async def get_topic_stat(slug):
return {
"shouts": len(TopicStat.shouts_by_topic.get(slug, {}).keys()),
"authors": len(TopicStat.authors_by_topic.get(slug, {}).keys()),
"followers": len(TopicStat.followers_by_topic.get(slug, {}).keys()),
# "viewed": await ViewedStorage.get_topic(slug),
"reacted": len(await ReactedStorage.get_topic(slug)),
"commented": len(await ReactedStorage.get_topic_comments(slug)),
"rating": await ReactedStorage.get_topic_rating(slug)
}
@query.field("topicsAll")
async def topics_all(_, _info):
topics = await TopicStorage.get_topics_all()
for topic in topics:
topic.stat = await get_topic_stat(topic.slug)
return topics
@query.field("topicsByCommunity")
async def topics_by_community(_, info, community):
topics = await TopicStorage.get_topics_by_community(community)
for topic in topics:
topic.stat = await get_topic_stat(topic.slug)
return topics
@query.field("topicsByAuthor")
async def topics_by_author(_, _info, author):
shouts = TopicStorage.get_topics_by_author(author)
author_topics = set()
for s in shouts:
for tpc in s.topics:
tpc = await TopicStorage.topics[tpc.slug]
tpc.stat = await get_topic_stat(tpc.slug)
author_topics.add(tpc)
return list(author_topics)
@query.field("getTopic")
async def get_topic(_, _info, slug):
t = TopicStorage.topics[slug]
t.stat = await get_topic_stat(slug)
return t
@mutation.field("createTopic")
@login_required
async def create_topic(_, _info, inp):
with local_session() as session:
# TODO: check user permissions to create topic for exact community
new_topic = Topic.create(**inp)
session.add(new_topic)
session.commit()
await TopicStorage.update_topic(new_topic)
return {"topic": new_topic}
@mutation.field("updateTopic")
@login_required
async def update_topic(_, _info, inp):
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"}
else:
topic.update(**inp)
session.commit()
await TopicStorage.update_topic(topic.slug)
return {"topic": topic}
async def topic_follow(user, slug):
with local_session() as session:
following = TopicFollower.create(topic=slug, follower=user.slug)
session.add(following)
session.commit()
await TopicStorage.update_topic(slug)
async def topic_unfollow(user, slug):
with local_session() as session:
sub = (
session.query(TopicFollower)
.filter(
and_(TopicFollower.follower == user.slug, TopicFollower.topic == slug)
)
.first()
)
if not sub:
raise Exception("[resolvers.topics] follower not exist")
else:
session.delete(sub)
session.commit()
await TopicStorage.update_topic(slug)
@query.field("topicsRandom")
async def topics_random(_, info, amount=12):
topics = await TopicStorage.get_topics_all()
normalized_topics = []
for topic in topics:
topic.stat = await get_topic_stat(topic.slug)
if topic.stat["shouts"] > 2:
normalized_topics.append(topic)
sample_length = min(len(normalized_topics), amount)
return random.sample(normalized_topics, sample_length)