wip refactoring: reactions, storages isolated

This commit is contained in:
2022-07-21 14:58:50 +03:00
parent edcefadeab
commit 6cb5061ce5
43 changed files with 1674 additions and 1779 deletions

View File

@@ -1,14 +1,12 @@
from resolvers.auth import login, sign_out, is_email_used, register, confirm, auth_forget, auth_reset
from resolvers.zine import get_shout_by_slug, subscribe, unsubscribe, view_shout, rate_shout, \
from resolvers.zine import get_shout_by_slug, follow, unfollow, view_shout, \
top_month, top_overall, recent_published, recent_all, top_viewed, \
shouts_by_authors, shouts_by_topics, shouts_by_communities
from resolvers.profile import get_users_by_slugs, get_current_user, shouts_reviewed
from resolvers.topics import topic_subscribe, topic_unsubscribe, topics_by_author, \
topics_by_community, topics_by_slugs
from resolvers.comments import create_comment, delete_comment, update_comment, rate_comment
from resolvers.collab import get_shout_proposals, create_proposal, delete_proposal, \
update_proposal, rate_proposal, decline_proposal, disable_proposal, accept_proposal, \
invite_author, remove_author
from resolvers.profile import get_users_by_slugs, get_current_user, get_user_reacted_shouts, get_user_roles
from resolvers.topics import topic_follow, topic_unfollow, topics_by_author, topics_by_community, topics_by_slugs
# from resolvers.feed import shouts_for_feed, my_candidates
from resolvers.reactions import create_reaction, delete_reaction, update_reaction, get_all_reactions
from resolvers.collab import invite_author, remove_author
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.community import create_community, delete_community, get_community, get_communities
@@ -20,36 +18,43 @@ __all__ = [
"confirm",
"auth_forget",
"auth_reset"
"sign_out",
# profile
"get_current_user",
"get_users_by_slugs",
# zine
"shouts_for_feed",
"my_candidates",
"recent_published",
"recent_reacted",
"recent_all",
"shouts_by_topics",
"shouts_by_authors",
"shouts_by_communities",
"shouts_reviewed",
"get_user_reacted_shouts",
"top_month",
"top_overall",
"top_viewed",
"rate_shout",
"view_shout",
"view_reaction",
"get_shout_by_slug",
# editor
"create_shout",
"update_shout",
"delete_shout",
# collab
"invite_author",
"remove_author"
# topics
"topics_by_slugs",
"topics_by_community",
"topics_by_author",
"topic_subscribe",
"topic_unsubscribe",
"topic_follow",
"topic_unfollow",
# communities
"get_community",
@@ -57,22 +62,12 @@ __all__ = [
"create_community",
"delete_community",
# comments
"get_shout_comments",
"comments_subscribe",
"comments_unsubscribe",
"create_comment",
"update_comment",
"delete_comment",
# collab
"get_shout_proposals",
"create_proposal",
"update_proposal",
"disable_proposal",
"accept_proposal",
"decline_proposal",
"delete_proposal",
"invite_author",
"remove_author"
# reactions
"get_shout_reactions",
"reactions_follow",
"reactions_unfollow",
"create_reaction",
"update_reaction",
"delete_reaction",
"get_all_reactions",
]

View File

@@ -1,7 +1,6 @@
from graphql import GraphQLResolveInfo
from transliterate import translit
from urllib.parse import quote_plus
from auth.authenticate import login_required, ResetPassword
from auth.authorize import Authorize
from auth.identity import Identity
@@ -12,7 +11,6 @@ from orm.base import local_session
from resolvers.base import mutation, query
from resolvers.profile import get_user_info
from exceptions import InvalidPassword, InvalidToken
from settings import JWT_AUTH_HEADER
@mutation.field("confirmEmail")

View File

@@ -1,221 +1,9 @@
import asyncio
from orm import Proposal, ProposalRating, UserStorage
from datetime import datetime
from orm.base import local_session
from orm.shout import Shout
from sqlalchemy.orm import selectinload
from orm.user import User
from resolvers.base import mutation, query
from resolvers.base import mutation
from auth.authenticate import login_required
from datetime import datetime
class ProposalResult:
def __init__(self, status, proposal):
self.status = status
self.proposal = proposal
class ProposalStorage:
lock = asyncio.Lock()
subscriptions = []
@staticmethod
async def register_subscription(subs):
async with ProposalStorage.lock:
ProposalStorage.subscriptions.append(subs)
@staticmethod
async def del_subscription(subs):
async with ProposalStorage.lock:
ProposalStorage.subscriptions.remove(subs)
@staticmethod
async def put(message_result):
async with ProposalStorage.lock:
for subs in ProposalStorage.subscriptions:
if message_result.message["chatId"] == subs.chat_id:
subs.queue.put_nowait(message_result)
@query.field("getShoutProposals")
@login_required
async def get_shout_proposals(_, info, slug):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposals = session.query(Proposal).\
options(selectinload(Proposal.ratings)).\
filter(Proposal.shout == slug).\
group_by(Proposal.id).all()
shout = session.query(Shout).filter(Shout.slug == slug).first()
authors = [author.id for author in shout.authors]
if user_id not in authors:
return {"error": "access denied"}
for proposal in proposals:
proposal.createdBy = await UserStorage.get_user(proposal.createdBy)
return proposals
@mutation.field("createProposal")
@login_required
async def create_proposal(_, info, body, shout, range = None):
auth = info.context["request"].auth
user_id = auth.user_id
proposal = Proposal.create(
createdBy = user_id,
body = body,
shout = shout,
range = range
)
result = ProposalResult("NEW", proposal)
await ProposalStorage.put(result)
return {"proposal": proposal}
@mutation.field("updateProposal")
@login_required
async def update_proposal(_, info, id, body):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
shout = session.query(Shout).filter(Shout.sllug == proposal.shout).first()
authors = [author.id for author in shout.authors]
if not proposal:
return {"error": "invalid proposal id"}
if proposal.author in authors:
return {"error": "access denied"}
proposal.body = body
proposal.updatedAt = datetime.now()
session.commit()
result = ProposalResult("UPDATED", proposal)
await ProposalStorage.put(result)
return {"proposal": proposal}
@mutation.field("deleteProposal")
@login_required
async def delete_proposal(_, info, id):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
if not proposal:
return {"error": "invalid proposal id"}
if proposal.createdBy != user_id:
return {"error": "access denied"}
proposal.deletedAt = datetime.now()
session.commit()
result = ProposalResult("DELETED", proposal)
await ProposalStorage.put(result)
return {}
@mutation.field("disableProposal")
@login_required
async def disable_proposal(_, info, id):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
if not proposal:
return {"error": "invalid proposal id"}
if proposal.createdBy != user_id:
return {"error": "access denied"}
proposal.deletedAt = datetime.now()
session.commit()
result = ProposalResult("DISABLED", proposal)
await ProposalStorage.put(result)
return {}
@mutation.field("rateProposal")
@login_required
async def rate_proposal(_, info, id, value):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
if not proposal:
return {"error": "invalid proposal id"}
rating = session.query(ProposalRating).\
filter(ProposalRating.proposal_id == id and ProposalRating.createdBy == user_id).first()
if rating:
rating.value = value
session.commit()
if not rating:
ProposalRating.create(
proposal_id = id,
createdBy = user_id,
value = value)
result = ProposalResult("UPDATED_RATING", proposal)
await ProposalStorage.put(result)
return {}
@mutation.field("acceptProposal")
@login_required
async def accept_proposal(_, info, id):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
shout = session.query(Shout).filter(Shout.slug == proposal.shout).first()
authors = [author.id for author in shout.authors]
if not proposal:
return {"error": "invalid proposal id"}
if user_id not in authors:
return {"error": "access denied"}
proposal.acceptedAt = datetime.now()
proposal.acceptedBy = user_id
session.commit()
result = ProposalResult("ACCEPTED", proposal)
await ProposalStorage.put(result)
return {}
@mutation.field("declineProposal")
@login_required
async def decline_proposal(_, info, id):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
proposal = session.query(Proposal).filter(Proposal.id == id).first()
shout = session.query(Shout).filter(Shout.slug == proposal.shout).first()
authors = [author.id for author in shout.authors]
if not proposal:
return {"error": "invalid proposal id"}
if user_id not in authors:
return {"error": "access denied"}
proposal.acceptedAt = datetime.now()
proposal.acceptedBy = user_id
session.commit()
result = ProposalResult("DECLINED", proposal)
await ProposalStorage.put(result)
return {}
@mutation.field("inviteAuthor")
@login_required
@@ -234,11 +22,10 @@ async def invite_author(_, info, author, shout):
if author.id in authors:
return {"error": "already added"}
shout.authors.append(author)
shout.updated_at = datetime.now()
shout.save()
session.commit()
# result = Result("INVITED")
# FIXME: await ShoutStorage.put(result)
# TODO: email notify
return {}
@@ -260,6 +47,8 @@ async def remove_author(_, info, author, shout):
if author.id not in authors:
return {"error": "not in authors"}
shout.authors.remove(author)
shout.updated_at = datetime.now()
shout.save()
session.commit()
# result = Result("INVITED")

View File

@@ -1,136 +0,0 @@
from orm import Comment, CommentRating
from orm.base import local_session
from orm.shout import ShoutCommentsSubscription
from orm.user import User
from resolvers.base import mutation, query
from auth.authenticate import login_required
from datetime import datetime
def comments_subscribe(user, slug, auto = False):
with local_session() as session:
sub = session.query(ShoutCommentsSubscription).\
filter(ShoutCommentsSubscription.subscriber == user.slug, ShoutCommentsSubscription.shout == slug).\
first()
if auto and sub:
return
elif not auto and sub:
if not sub.deletedAt is None:
sub.deletedAt = None
sub.auto = False
session.commit()
return
raise Exception("subscription already exist")
ShoutCommentsSubscription.create(
subscriber = user.slug,
shout = slug,
auto = auto)
def comments_unsubscribe(user, slug):
with local_session() as session:
sub = session.query(ShoutCommentsSubscription).\
filter(ShoutCommentsSubscription.subscriber == user.slug, ShoutCommentsSubscription.shout == slug).\
first()
if not sub:
raise Exception("subscription not exist")
if sub.auto:
sub.deletedAt = datetime.now()
else:
session.delete(sub)
session.commit()
@mutation.field("createComment")
@login_required
async def create_comment(_, info, body, shout, replyTo = None):
user = info.context["request"].user
comment = Comment.create(
createdBy = user.slug,
body = body,
shout = shout,
replyTo = replyTo
)
try:
comments_subscribe(user, shout, True)
except Exception as e:
print(f"error on comment autosubscribe: {e}")
return {"comment": comment}
@mutation.field("updateComment")
@login_required
async def update_comment(_, info, id, body):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
comment = session.query(Comment).filter(Comment.id == id).first()
if not comment:
return {"error": "invalid comment id"}
if comment.createdBy != user_id:
return {"error": "access denied"}
comment.body = body
comment.updatedAt = datetime.now()
session.commit()
return {"comment": comment}
@mutation.field("deleteComment")
@login_required
async def delete_comment(_, info, id):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
comment = session.query(Comment).filter(Comment.id == id).first()
if not comment:
return {"error": "invalid comment id"}
if comment.createdBy != user_id:
return {"error": "access denied"}
comment.deletedAt = datetime.now()
session.commit()
return {}
@mutation.field("rateComment")
@login_required
async def rate_comment(_, info, id, value):
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()
comment = session.query(Comment).filter(Comment.id == id).first()
if not comment:
return {"error": "invalid comment id"}
rating = session.query(CommentRating).\
filter(CommentRating.comment_id == id, CommentRating.createdBy == user.slug).first()
if rating:
rating.value = value
session.commit()
if not rating:
CommentRating.create(
comment_id = id,
createdBy = user_id,
value = value)
return {}
def get_subscribed_shout_comments(slug):
with local_session() as session:
rows = session.query(ShoutCommentsSubscription.shout).\
filter(ShoutCommentsSubscription.subscriber == slug,\
ShoutCommentsSubscription.deletedAt == None).\
all()
slugs = [row.shout for row in rows]
return slugs
@query.field("commentsAll")
def get_top10_comments(_, info, page = 1, size = 10):
with local_session() as session:
rows = session.query(Comment).limit(size).all()

View File

@@ -1,5 +1,6 @@
from orm import Community, CommunitySubscription
from orm.community import Community, CommunityFollower
from orm.base import local_session
from orm.user import User
from resolvers.base import mutation, query
from auth.authenticate import login_required
from datetime import datetime
@@ -26,12 +27,15 @@ async def create_community(_, info, input):
async def update_community(_, info, input):
auth = info.context["request"].auth
user_id = auth.user_id
community_slug = input.get('slug', '')
with local_session() as session:
community = session.query(Community).filter(Community.slug == input.get('slug', '')).first()
owner = session.query(User).filter(User.id == user_id) # note list here
community = session.query(Community).filter(Community.slug == community_slug).first()
editors = [e.slug for e in community.editors]
if not community:
return {"error": "invalid community id"}
if community.createdBy != user_id:
if community.createdBy not in (owner + editors):
return {"error": "access denied"}
community.title = input.get('title', '')
community.desc = input.get('desc', '')
@@ -71,27 +75,28 @@ async def get_communities(_, info):
communities = session.query(Community)
return communities
def community_subscribe(user, slug):
CommunitySubscription.create(
subscriber = user.slug,
def community_follow(user, slug):
CommunityFollower.create(
follower = user.slug,
community = slug
)
def community_unsubscribe(user, slug):
def community_unfollow(user, slug):
with local_session() as session:
sub = session.query(CommunitySubscription).\
filter(and_(CommunitySubscription.subscriber == user.slug, CommunitySubscription.community == slug)).\
following = session.query(CommunityFollower).\
filter(and_(CommunityFollower.follower == user.slug, CommunityFollower.community == slug)).\
first()
if not sub:
raise Exception("subscription not exist")
session.delete(sub)
if not following:
raise Exception("[orm.community] following was not exist")
session.delete(following)
session.commit()
def get_subscribed_communities(user_slug):
@query.field("userFollowedCommunities")
def get_followed_communities(_, user_slug) -> list[Community]:
ccc = []
with local_session() as session:
rows = session.query(Community.slug).\
join(CommunitySubscription).\
where(CommunitySubscription.subscriber == user_slug).\
ccc = session.query(Community.slug).\
join(CommunityFollower).\
where(CommunityFollower.follower == user_slug).\
all()
slugs = [row.slug for row in rows]
return slugs
return ccc

View File

@@ -4,11 +4,10 @@ from orm.rbac import Resource
from orm.shout import ShoutAuthor, ShoutTopic
from orm.user import User
from resolvers.base import mutation
from resolvers.comments import comments_subscribe
from resolvers.reactions import reactions_follow, reactions_unfollow
from auth.authenticate import login_required
from datetime import datetime
from resolvers.zine import GitTask
from storages.gittask import GitTask
@mutation.field("createShout")
@@ -26,7 +25,7 @@ async def create_shout(_, info, input):
user = user.slug
)
comments_subscribe(user, new_shout.slug, True)
reactions_follow(user, new_shout.slug, True)
if "mainTopic" in input:
topic_slugs.append(input["mainTopic"])
@@ -110,8 +109,10 @@ async def delete_shout(_, info, slug):
return {"error": "invalid shout slug"}
if user_id not in authors:
return {"error": "access denied"}
for a in authors:
reactions_unfollow(a.slug, slug, True)
shout.deletedAt = datetime.now()
session.commit()
return {}

41
resolvers/feed.py Normal file
View File

@@ -0,0 +1,41 @@
from auth.authenticate import login_required
from orm.base import local_session
from sqlalchemy import and_, desc, query
from orm.reaction import Reaction
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import TopicFollower
from orm.user import AuthorFollower
@query.field("shoutsForFeed")
@login_required
def get_user_feed(_, info, page, size) -> list[Shout]:
user = info.context["request"].user
shouts = []
with local_session() as session:
shouts = session.query(Shout).\
join(ShoutAuthor).\
join(AuthorFollower).\
where(AuthorFollower.follower == user.slug).\
order_by(desc(Shout.createdAt))
topicrows = session.query(Shout).\
join(ShoutTopic).\
join(TopicFollower).\
where(TopicFollower.follower == user.slug).\
order_by(desc(Shout.createdAt))
shouts = shouts.union(topicrows).limit(size).offset(page * size).all()
return shouts
@query.field("myCandidates")
@login_required
async def user_unpublished_shouts(_, info, page = 1, size = 10) -> list[Shout]:
user = info.context["request"].user
shouts = []
with local_session() as session:
shouts = session.query(Shout).\
join(ShoutAuthor).\
where(and_(Shout.publishedAt == None, ShoutAuthor.user == user.slug)).\
order_by(desc(Shout.createdAt)).\
limit(size).\
offset( page * size).\
all()
return shouts

View File

@@ -1,224 +1,154 @@
from orm import User, UserRole, Role, UserRating
from orm.user import AuthorSubscription, UserStorage
from orm.comment import Comment
from orm.user import User, UserRole, Role, UserRating, AuthorFollower
from storages.users import UserStorage
from orm.shout import Shout
from orm.reaction import Reaction
from orm.base import local_session
from orm.topic import Topic, TopicSubscription
from resolvers.base import mutation, query, subscription
from resolvers.community import get_subscribed_communities
from resolvers.comments import get_subscribed_shout_comments
from orm.topic import Topic, TopicFollower
from resolvers.base import mutation, query
from resolvers.community import get_followed_communities
from resolvers.reactions import get_shout_reactions
from auth.authenticate import login_required
from inbox_resolvers.inbox import get_total_unread_messages_for_user
from sqlalchemy import func, and_, desc
from inbox_resolvers.inbox import get_inbox_counter
from sqlalchemy import and_, desc
from sqlalchemy.orm import selectinload
import asyncio
def _get_user_subscribed_topic_slugs(slug):
with local_session() as session:
rows = session.query(Topic.slug).\
join(TopicSubscription).\
where(TopicSubscription.subscriber == slug).\
all()
slugs = [row.slug for row in rows]
return slugs
def _get_user_subscribed_authors(slug):
with local_session() as session:
authors = session.query(User.slug).\
join(AuthorSubscription, User.slug == AuthorSubscription.author).\
where(AuthorSubscription.subscriber == slug)
return authors
@query.field("userReactedShouts")
async def get_user_reacted_shouts(_, info, slug, page, size) -> list[Shout]:
user = await UserStorage.get_user_by_slug(slug)
if not user: return {}
with local_session() as session:
shouts = session.query(Shout).\
join(Reaction).\
where(Reaction.createdBy == user.slug).\
order_by(desc(Reaction.createdAt)).\
limit(size).\
offset(page * size).all()
return shouts
@query.field("userFollowedTopics")
@login_required
def get_followed_topics(_, slug) -> list[Topic]:
rows = []
with local_session() as session:
rows = session.query(Topic).\
join(TopicFollower).\
where(TopicFollower.follower == slug).\
all()
return rows
@query.field("userFollowedAuthors")
def get_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()
return authors
@query.field("userFollowers")
async def user_followers(_, 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
# for query.field("getCurrentUser")
async def get_user_info(slug):
return {
"totalUnreadMessages" : await get_total_unread_messages_for_user(slug),
"userSubscribedTopics" : _get_user_subscribed_topic_slugs(slug),
"userSubscribedAuthors" : _get_user_subscribed_authors(slug),
"userSubscribedCommunities" : get_subscribed_communities(slug),
"userSubscribedShoutComments": get_subscribed_shout_comments(slug)
}
return {
"inbox": await get_inbox_counter(slug),
"topics": [t.slug for t in get_followed_topics(0, slug)],
"authors": [a.slug for a in get_followed_authors(0, slug)],
"reactions": [r.shout for r in get_shout_reactions(0, slug)],
"communities": [c.slug for c in get_followed_communities(0, slug)]
}
@query.field("getCurrentUser")
@login_required
async def get_current_user(_, info):
user = info.context["request"].user
return {
"user": user,
"info": await get_user_info(user.slug)
}
user = info.context["request"].user
return {
"user": user,
"info": await get_user_info(user.slug)
}
@query.field("getUsersBySlugs")
async def get_users_by_slugs(_, info, slugs):
with local_session() as session:
users = session.query(User).\
options(selectinload(User.ratings)).\
filter(User.slug.in_(slugs)).all()
return users
with local_session() as session:
users = session.query(User).\
options(selectinload(User.ratings)).\
filter(User.slug.in_(slugs)).all()
return users
@query.field("getUserRoles")
async def get_user_roles(_, info, 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
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
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()
user.update(profile)
session.commit()
return {}
with local_session() as session:
user = session.query(User).filter(User.id == user_id).first()
user.update(profile)
session.commit()
return {}
@query.field("userComments")
async def user_comments(_, info, slug, page, size):
user = await UserStorage.get_user_by_slug(slug)
if not user:
return
page = page - 1
with local_session() as session:
comments = session.query(Comment).\
filter(Comment.createdBy == user.id).\
order_by(desc(Comment.createdAt)).\
limit(size).\
offset(page * size)
return comments
@query.field("userSubscribedAuthors")
async def user_subscriptions(_, info, slug):
slugs = _get_user_subscribed_authors(slug)
return slugs
@query.field("userSubscribers")
async def user_subscribers(_, info, slug):
with local_session() as session:
slugs = session.query(User.slug).\
join(AuthorSubscription, User.slug == AuthorSubscription.subscriber).\
where(AuthorSubscription.author == slug)
return slugs
@query.field("userSubscribedTopics")
async def user_subscribed_topics(_, info, slug):
return _get_user_subscribed_topic_slugs(slug)
@mutation.field("rateUser")
@login_required
async def rate_user(_, info, slug, value):
user = info.context["request"].user
user = info.context["request"].user
with local_session() as session:
rating = session.query(UserRating).\
filter(and_(UserRating.rater == user.slug, UserRating.user == slug)).\
first()
if rating:
rating.value = value
session.commit()
return {}
try:
UserRating.create(
rater=user.slug,
user=slug,
value=value
)
except Exception as err:
return {"error": err}
return {}
with local_session() as session:
rating = session.query(UserRating).\
filter(and_(UserRating.rater == user.slug, UserRating.user == slug)).\
first()
if rating:
rating.value = value
session.commit()
return {}
UserRating.create(
rater = user.slug,
user = slug,
value = value
)
return {}
def author_subscribe(user, slug):
AuthorSubscription.create(
subscriber = user.slug,
author = slug
)
def author_unsubscribe(user, slug):
with local_session() as session:
sub = session.query(AuthorSubscription).\
filter(and_(AuthorSubscription.subscriber == user.slug, AuthorSubscription.author == slug)).\
first()
if not sub:
raise Exception("subscription not exist")
session.delete(sub)
session.commit()
@query.field("shoutsRatedByUser")
@login_required
async def shouts_rated_by_user(_, info, page, size):
user = info.context["request"].user
with local_session() as session:
shouts = session.query(Shout).\
join(ShoutRating).\
where(ShoutRating.rater == user.slug).\
order_by(desc(ShoutRating.ts)).\
limit(size).\
offset( (page - 1) * size)
return {
"shouts" : shouts
}
@query.field("userUnpublishedShouts")
@login_required
async def user_unpublished_shouts(_, info, page, size):
user = info.context["request"].user
with local_session() as session:
shouts = session.query(Shout).\
join(ShoutAuthor).\
where(and_(Shout.publishedAt == None, ShoutAuthor.user == user.slug)).\
order_by(desc(Shout.createdAt)).\
limit(size).\
offset( (page - 1) * size)
return {
"shouts" : shouts
}
@query.field("shoutsReviewed")
@login_required
async def shouts_reviewed(_, info, page, size):
user = info.context["request"].user
with local_session() as session:
shouts_by_rating = session.query(Shout).\
join(ShoutRating).\
where(and_(Shout.publishedAt != None, ShoutRating.rater == user.slug))
shouts_by_comment = session.query(Shout).\
join(Comment).\
where(and_(Shout.publishedAt != None, Comment.createdBy == user.id))
shouts = shouts_by_rating.union(shouts_by_comment).\
order_by(desc(Shout.publishedAt)).\
limit(size).\
offset( (page - 1) * size)
return shouts
@query.field("shoutsCommentedByUser")
async def shouts_commented_by_user(_, info, slug, page, size):
user = await UserStorage.get_user_by_slug(slug)
if not user:
return {}
with local_session() as session:
shouts = session.query(Shout).\
join(Comment).\
where(Comment.createdBy == user.id).\
order_by(desc(Comment.createdAt)).\
limit(size).\
offset( (page - 1) * size)
return shouts
# for mutation.field("follow")
def author_follow(user, slug):
AuthorFollower.create(
follower=user.slug,
author=slug
)
# 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()

150
resolvers/reactions.py Normal file
View File

@@ -0,0 +1,150 @@
from sqlalchemy import and_
from sqlalchemy.orm import selectinload, joinedload
from orm.reaction import Reaction
from orm.base import local_session
from orm.shout import Shout, ShoutReactionsFollower
from orm.user import User
from resolvers.base import mutation, query
from auth.authenticate import login_required
from datetime import datetime
from storages.reactions import ReactionsStorage
from storages.viewed import ViewedStorage
def reactions_follow(user, slug, auto=False):
with local_session() as session:
fw = session.query(ShoutReactionsFollower).\
filter(ShoutReactionsFollower.follower == user.slug, ShoutReactionsFollower.shout == slug).\
first()
if auto and fw:
return
elif not auto and fw:
if not fw.deletedAt is None:
fw.deletedAt = None
fw.auto = False
session.commit()
return
# print("[resolvers.reactions] was followed before")
ShoutReactionsFollower.create(
follower=user.slug,
shout=slug,
auto=auto)
def reactions_unfollow(user, slug):
with local_session() as session:
following = session.query(ShoutReactionsFollower).\
filter(ShoutReactionsFollower.follower == user.slug, ShoutReactionsFollower.shout == slug).\
first()
if not following:
# print("[resolvers.reactions] was not followed", slug)
return
if following.auto:
following.deletedAt = datetime.now()
else:
session.delete(following)
session.commit()
@mutation.field("createReaction")
@login_required
async def create_reaction(_, info, inp):
user = info.context["request"].user
reaction = Reaction.create(**inp)
try:
reactions_follow(user, inp['shout'], True)
except Exception as e:
print(f"[resolvers.reactions] error on reactions autofollowing: {e}")
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).filter(User.id == user_id).first()
reaction = session.query(Reaction).filter(Reaction.id == 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']:
# TODO: change mind detection
pass
if inp.get('range'):
reaction.range = inp.get('range')
session.commit()
return {"reaction": reaction}
@mutation.field("deleteReaction")
@login_required
async def delete_reaction(_, info, id):
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()
reaction = session.query(Reaction).filter(Reaction.id == id).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("reactionsByShout")
def get_shout_reactions(_, info, slug) -> list[Shout]:
shouts = []
with local_session() as session:
shoutslugs = session.query(ShoutReactionsFollower.shout).\
join(User).where(Reaction.createdBy == User.slug).\
filter(ShoutReactionsFollower.follower == slug,
ShoutReactionsFollower.deletedAt == None).all()
shoutslugs = list(set(shoutslugs))
shouts = session.query(Shout).filter(Shout.slug in shoutslugs).all()
return shouts
@query.field("reactionsAll")
def get_all_reactions(_, info, page=1, size=10) -> list[Reaction]:
reactions = []
with local_session() as session:
q = session.query(Reaction).\
options(
joinedload(User),
joinedload(Shout)
).\
join( User, Reaction.createdBy == User.slug ).\
join( Shout, Reaction.shout == Shout.slug ).\
filter( Reaction.deletedAt == None ).\
limit(size).offset(page * size).all()
# print(reactions[0].dict())
return reactions
@query.field("reactionsByAuthor")
def get_reactions_by_author(_, info, slug, page=1, size=50) -> list[Reaction]:
reactions = []
with local_session() as session:
reactions = session.query(Reaction).\
join(Shout).where(Reaction.shout == Shout.slug).\
filter(Reaction.deletedAt == None, Reaction.createdBy == slug).\
limit(size).offset(page * size).all() # pagination
return reactions
@mutation.field("viewReaction")
async def view_reaction(_, info, reaction):
await ViewedStorage.inc_reaction(reaction)
return {"error" : ""}

View File

@@ -1,17 +1,16 @@
from orm import Topic, TopicSubscription, TopicStorage, Shout, User
from orm.shout import TopicStat, ShoutAuthorStorage
from orm.user import UserStorage
from orm.topic import Topic, TopicFollower
from storages.topics import TopicStorage
from orm.shout import Shout
from orm.user import User
from storages.topicstat import TopicStat
from orm.base import local_session
from resolvers.base import mutation, query
from auth.authenticate import login_required
import asyncio
from sqlalchemy import func, and_
from sqlalchemy import and_
@query.field("topicsAll")
async def topics_by_slugs(_, info, slugs = None):
with local_session() as session:
topics = await TopicStorage.get_topics(slugs)
async def topics_by_slugs(_, info, page = 1, size = 50):
topics = await TopicStorage.get_topics_all(page, size)
all_fields = [node.name.value for node in info.field_nodes[0].selection_set.selections]
if "stat" in all_fields:
for topic in topics:
@@ -20,8 +19,7 @@ async def topics_by_slugs(_, info, slugs = None):
@query.field("topicsByCommunity")
async def topics_by_community(_, info, community):
with local_session() as session:
topics = await TopicStorage.get_topics_by_community(community)
topics = await TopicStorage.get_topics_by_community(community)
all_fields = [node.name.value for node in info.field_nodes[0].selection_set.selections]
if "stat" in all_fields:
for topic in topics:
@@ -65,17 +63,17 @@ async def update_topic(_, info, input):
return { "topic" : topic }
def topic_subscribe(user, slug):
TopicSubscription.create(
subscriber = user.slug,
def topic_follow(user, slug):
TopicFollower.create(
follower = user.slug,
topic = slug)
def topic_unsubscribe(user, slug):
def topic_unfollow(user, slug):
with local_session() as session:
sub = session.query(TopicSubscription).\
filter(and_(TopicSubscription.subscriber == user.slug, TopicSubscription.topic == slug)).\
sub = session.query(TopicFollower).\
filter(and_(TopicFollower.follower == user.slug, TopicFollower.topic == slug)).\
first()
if not sub:
raise Exception("subscription not exist")
raise Exception("[resolvers.topics] follower not exist")
session.delete(sub)
session.commit()

View File

@@ -1,216 +1,18 @@
from orm import Shout, ShoutAuthor, ShoutTopic, ShoutRating, ShoutViewByDay, \
User, Community, Resource, ShoutRatingStorage, ShoutViewStorage, \
Comment, CommentRating, Topic, ShoutCommentsSubscription
from orm.community import CommunitySubscription
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from orm.base import local_session
from orm.user import UserStorage, AuthorSubscription
from orm.topic import TopicSubscription
from resolvers.base import mutation, query
from resolvers.profile import author_subscribe, author_unsubscribe
from resolvers.topics import topic_subscribe, topic_unsubscribe
from resolvers.community import community_subscribe, community_unsubscribe
from resolvers.comments import comments_subscribe, comments_unsubscribe
from storages.shoutscache import ShoutsCache
from storages.viewed import ViewedStorage
from resolvers.profile import author_follow, author_unfollow
from resolvers.topics import topic_follow, topic_unfollow
from resolvers.community import community_follow, community_unfollow
from resolvers.reactions import reactions_follow, reactions_unfollow
from auth.authenticate import login_required
from settings import SHOUTS_REPO
import subprocess
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
from sqlalchemy import select, func, desc, and_
from sqlalchemy import select, desc, and_
from sqlalchemy.orm import selectinload
class GitTask:
queue = asyncio.Queue()
def __init__(self, input, username, user_email, comment):
self.slug = input["slug"]
self.shout_body = input["body"]
self.username = username
self.user_email = user_email
self.comment = comment
GitTask.queue.put_nowait(self)
def init_repo(self):
repo_path = "%s" % (SHOUTS_REPO)
Path(repo_path).mkdir()
cmd = "cd %s && git init && " \
"git config user.name 'discours' && " \
"git config user.email 'discours@discours.io' && " \
"touch initial && git add initial && " \
"git commit -m 'init repo'" \
% (repo_path)
output = subprocess.check_output(cmd, shell=True)
print(output)
def execute(self):
repo_path = "%s" % (SHOUTS_REPO)
if not Path(repo_path).exists():
self.init_repo()
#cmd = "cd %s && git checkout master" % (repo_path)
#output = subprocess.check_output(cmd, shell=True)
#print(output)
shout_filename = "%s.mdx" % (self.slug)
shout_full_filename = "%s/%s" % (repo_path, shout_filename)
with open(shout_full_filename, mode='w', encoding='utf-8') as shout_file:
shout_file.write(bytes(self.shout_body,'utf-8').decode('utf-8','ignore'))
author = "%s <%s>" % (self.username, self.user_email)
cmd = "cd %s && git add %s && git commit -m '%s' --author='%s'" % \
(repo_path, shout_filename, self.comment, author)
output = subprocess.check_output(cmd, shell=True)
print(output)
@staticmethod
async def git_task_worker():
print("[git.task] worker start")
while True:
task = await GitTask.queue.get()
try:
task.execute()
except Exception as err:
print("[git.task] worker error = %s" % (err))
class ShoutsCache:
limit = 200
period = 60*60 #1 hour
lock = asyncio.Lock()
@staticmethod
async def prepare_recent_published():
with local_session() as session:
stmt = select(Shout).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
where(Shout.publishedAt != None).\
order_by(desc("publishedAt")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.recent_published = shouts
@staticmethod
async def prepare_recent_all():
with local_session() as session:
stmt = select(Shout).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
order_by(desc("createdAt")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.recent_all = shouts
@staticmethod
async def prepare_recent_commented():
with local_session() as session:
stmt = select(Shout, func.max(Comment.createdAt).label("commentCreatedAt")).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
join(Comment).\
where(and_(Shout.publishedAt != None, Comment.deletedAt == None)).\
group_by(Shout.slug).\
order_by(desc("commentCreatedAt")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.recent_commented = shouts
@staticmethod
async def prepare_top_overall():
with local_session() as session:
stmt = select(Shout, func.sum(ShoutRating.value).label("rating")).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
join(ShoutRating).\
where(Shout.publishedAt != None).\
group_by(Shout.slug).\
order_by(desc("rating")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.top_overall = shouts
@staticmethod
async def prepare_top_month():
month_ago = datetime.now() - timedelta(days = 30)
with local_session() as session:
stmt = select(Shout, func.sum(ShoutRating.value).label("rating")).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
join(ShoutRating).\
where(and_(Shout.createdAt > month_ago, Shout.publishedAt != None)).\
group_by(Shout.slug).\
order_by(desc("rating")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.top_month = shouts
@staticmethod
async def prepare_top_viewed():
month_ago = datetime.now() - timedelta(days = 30)
with local_session() as session:
stmt = select(Shout, func.sum(ShoutViewByDay.value).label("views")).\
options(selectinload(Shout.authors), selectinload(Shout.topics)).\
join(ShoutViewByDay).\
where(and_(ShoutViewByDay.day > month_ago, Shout.publishedAt != None)).\
group_by(Shout.slug).\
order_by(desc("views")).\
limit(ShoutsCache.limit)
shouts = []
for row in session.execute(stmt):
shout = row.Shout
shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug)
shout.views = row.views
shouts.append(shout)
async with ShoutsCache.lock:
ShoutsCache.top_viewed = shouts
@staticmethod
async def worker():
print("[shouts.cache] worker start")
while True:
try:
print("[shouts.cache] updating...")
await ShoutsCache.prepare_top_month()
await ShoutsCache.prepare_top_overall()
await ShoutsCache.prepare_top_viewed()
await ShoutsCache.prepare_recent_published()
await ShoutsCache.prepare_recent_all()
await ShoutsCache.prepare_recent_commented()
print("[shouts.cache] update finished")
except Exception as err:
print("[shouts.cache] worker error: %s" % (err))
await asyncio.sleep(ShoutsCache.period)
@query.field("topViewed")
async def top_viewed(_, info, page, size):
async with ShoutsCache.lock:
@@ -236,20 +38,20 @@ async def recent_all(_, info, page, size):
async with ShoutsCache.lock:
return ShoutsCache.recent_all[(page - 1) * size : page * size]
@query.field("recentCommented")
async def recent_commented(_, info, page, size):
@query.field("recentReacted")
async def recent_reacted(_, info, page, size):
async with ShoutsCache.lock:
return ShoutsCache.recent_commented[(page - 1) * size : page * size]
return ShoutsCache.recent_reacted[(page - 1) * size : page * size]
@mutation.field("viewShout")
async def view_shout(_, info, slug):
await ShoutViewStorage.inc_view(slug)
await ViewedStorage.inc_shout(slug)
return {"error" : ""}
@query.field("getShoutBySlug")
async def get_shout_by_slug(_, info, slug):
all_fields = [node.name.value for node in info.field_nodes[0].selection_set.selections]
selected_fields = set(["authors", "topics"]).intersection(all_fields)
selected_fields = set(["authors", "topics", "reactions"]).intersection(all_fields)
select_options = [selectinload(getattr(Shout, field)) for field in selected_fields]
with local_session() as session:
@@ -258,23 +60,11 @@ async def get_shout_by_slug(_, info, slug):
filter(Shout.slug == slug).first()
if not shout:
print(f"shout with slug {slug} not exist")
print(f"[resolvers.zine] error: shout with slug {slug} not exist")
return {} #TODO return error field
shout.ratings = await ShoutRatingStorage.get_ratings(slug)
return shout
@query.field("getShoutComments")
async def get_shout_comments(_, info, slug):
with local_session() as session:
comments = session.query(Comment).\
options(selectinload(Comment.ratings)).\
filter(Comment.shout == slug).\
group_by(Comment.id).all()
for comment in comments:
comment.createdBy = await UserStorage.get_user(comment.createdBy)
return comments
@query.field("shoutsByTopics")
async def shouts_by_topics(_, info, slugs, page, size):
page = page - 1
@@ -316,65 +106,39 @@ async def shouts_by_communities(_, info, slugs, page, size):
offset(page * size)
return shouts
@mutation.field("subscribe")
@mutation.field("follow")
@login_required
async def subscribe(_, info, what, slug):
async def follow(_, info, what, slug):
user = info.context["request"].user
try:
if what == "AUTHOR":
author_subscribe(user, slug)
author_follow(user, slug)
elif what == "TOPIC":
topic_subscribe(user, slug)
topic_follow(user, slug)
elif what == "COMMUNITY":
community_subscribe(user, slug)
elif what == "COMMENTS":
comments_subscribe(user, slug)
community_follow(user, slug)
elif what == "REACTIONS":
reactions_follow(user, slug)
except Exception as e:
return {"error" : str(e)}
return {}
@mutation.field("unsubscribe")
@mutation.field("unfollow")
@login_required
async def unsubscribe(_, info, what, slug):
async def unfollow(_, info, what, slug):
user = info.context["request"].user
try:
if what == "AUTHOR":
author_unsubscribe(user, slug)
author_unfollow(user, slug)
elif what == "TOPIC":
topic_unsubscribe(user, slug)
topic_unfollow(user, slug)
elif what == "COMMUNITY":
community_unsubscribe(user, slug)
elif what == "COMMENTS":
comments_unsubscribe(user, slug)
community_unfollow(user, slug)
elif what == "REACTIONS":
reactions_unfollow(user, slug)
except Exception as e:
return {"error" : str(e)}
return {}
@mutation.field("rateShout")
@login_required
async def rate_shout(_, info, slug, value):
auth = info.context["request"].auth
user = info.context["request"].user
with local_session() as session:
rating = session.query(ShoutRating).\
filter(and_(ShoutRating.rater == user.slug, ShoutRating.shout == slug)).first()
if rating:
rating.value = value;
rating.ts = datetime.now()
session.commit()
else:
rating = ShoutRating.create(
rater = user.slug,
shout = slug,
value = value
)
await ShoutRatingStorage.update_rating(rating)
return {"error" : ""}