diff --git a/Pipfile b/Pipfile index 90fed477..8f16daad 100644 --- a/Pipfile +++ b/Pipfile @@ -13,7 +13,7 @@ passlib = "*" PyJWT = "*" SQLAlchemy = "*" itsdangerous = "*" -httpx = "<0.18.2" +httpx = ">=0.23.0" psycopg2-binary = "*" Authlib = "*" bson = "*" diff --git a/orm/__init__.py b/orm/__init__.py index 0cafbcb4..5aa2c2ee 100644 --- a/orm/__init__.py +++ b/orm/__init__.py @@ -1,5 +1,5 @@ from orm.rbac import Operation, Resource, Permission, Role, RoleStorage -from orm.community import Community +from orm.community import Community, CommunitySubscription from orm.user import User, UserRating, UserRole, UserStorage from orm.topic import Topic, TopicSubscription, TopicStorage from orm.notification import Notification diff --git a/orm/community.py b/orm/community.py index ca5459fc..3ffdad1d 100644 --- a/orm/community.py +++ b/orm/community.py @@ -3,6 +3,14 @@ from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy.orm import relationship, backref from orm.base import Base, local_session +class CommunitySubscription(Base): + __tablename__ = 'community_subscription' + + id = None + subscriber = Column(ForeignKey('user.slug'), primary_key = True) + community = Column(ForeignKey('community.slug'), primary_key = True) + createdAt: str = Column(DateTime, nullable=False, default = datetime.now, comment="Created at") + class Community(Base): __tablename__ = 'community' diff --git a/requirements.txt b/requirements.txt index 1a97e2d9..e369a526 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,19 @@ -aioredis -ariadne -pyjwt>=2.0.0 -starlette -sqlalchemy -uvicorn -pydantic -passlib -itsdangerous -authlib==0.15.5 -httpx==0.20.0 -psycopg2-binary -bson -python-frontmatter -transliterate -requests -bcrypt -bs4 -websockets +aioredis +ariadne +pyjwt>=2.0.0 +starlette +sqlalchemy +uvicorn +pydantic +passlib +itsdangerous +authlib==0.15.5 +httpx>=0.23.0 +psycopg2-binary +bson +python-frontmatter +transliterate +requests +bcrypt +bs4 +websockets diff --git a/resolvers/__init__.py b/resolvers/__init__.py index 658ae999..86b097a1 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -1,6 +1,6 @@ from resolvers.auth import login, sign_out, is_email_free, register, confirm from resolvers.zine import create_shout, get_shout_by_slug, \ - top_month, top_overall, recent_shouts, recent_all, top_viewed, shouts_by_authors, shouts_by_topics, shouts_by_communities, \ + top_month, top_overall, recent_published, recent_all, top_viewed, shouts_by_authors, shouts_by_topics, shouts_by_communities, \ shouts_candidates, shouts_reviewed, shouts_subscribed from resolvers.profile import get_users_by_slugs, get_current_user from resolvers.topics import topic_subscribe, topic_unsubscribe, topics_by_author, \ @@ -19,7 +19,7 @@ __all__ = [ "get_current_user", "get_users_by_slugs", "get_shout_by_slug", - "recent_shouts", + "recent_published", "recent_all", "shouts_by_topics", "shouts_by_authors", diff --git a/resolvers/community.py b/resolvers/community.py index 0a2778e4..f3ce65b1 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -1,10 +1,12 @@ -from orm import Community +from orm import Community, CommunitySubscription from orm.base import local_session from resolvers.base import mutation, query, subscription from auth.authenticate import login_required import asyncio from datetime import datetime +from sqlalchemy import and_ + @mutation.field("createCommunity") @login_required async def create_community(_, info, title, desc): @@ -68,3 +70,28 @@ async def get_communities(_, info): with local_session() as session: communities = session.query(Community) return communities + +def community_subscribe(user, slug): + CommunitySubscription.create( + subscriber = user.slug, + community = slug + ) + +def community_unsubscribe(user, slug): + with local_session() as session: + sub = session.query(CommunitySubscription).\ + filter(and_(CommunitySubscription.subscriber == user.slug, CommunitySubscription.community == slug)).\ + first() + if not sub: + raise Exception("subscription not exist") + session.delete(sub) + session.commit() + +def get_subscribed_communities(user_slug): + with local_session() as session: + rows = session.query(Community.slug).\ + join(CommunitySubscription).\ + where(CommunitySubscription.subscriber == user_slug).\ + all() + slugs = [row.slug for row in rows] + return slugs diff --git a/resolvers/profile.py b/resolvers/profile.py index 86184da3..9e7ed6b2 100644 --- a/resolvers/profile.py +++ b/resolvers/profile.py @@ -4,6 +4,8 @@ from orm.comment import Comment from orm.base import local_session from orm.topic import Topic, TopicSubscription from resolvers.base import mutation, query, subscription +from resolvers.topics import topic_subscribe, topic_unsubscribe +from resolvers.community import community_subscribe, community_unsubscribe, get_subscribed_communities from auth.authenticate import login_required from inbox_resolvers.inbox import get_total_unread_messages_for_user @@ -30,9 +32,10 @@ def _get_user_subscribed_authors(slug): 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) + "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) } @query.field("getCurrentUser") @@ -133,30 +136,53 @@ async def rate_user(_, info, slug, value): return {} -@mutation.field("authorSubscribe") -@login_required -async def author_subscribe(_, info, slug): - user = info.context["request"].user +def author_subscribe(user, slug): AuthorSubscription.create( subscriber = user.slug, author = slug ) - return {} - -@mutation.field("authorUnsubscribe") -@login_required -async def author_unsubscribe(_, info, slug): - user = info.context["request"].user - +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: - return { "error" : "subscription not exist" } + raise Exception("subscription not exist") session.delete(sub) session.commit() +@mutation.field("subscribe") +@login_required +async def subscribe(_, info, subscription, slug): + user = info.context["request"].user + + try: + if subscription == "AUTHOR": + author_subscribe(user, slug) + elif subscription == "TOPIC": + topic_subscribe(user, slug) + elif subscription == "COMMUNITY": + community_subscribe(user, slug) + except Exception as e: + return {"error" : e} + + return {} + +@mutation.field("unsubscribe") +@login_required +async def unsubscribe(_, info, subscription, slug): + user = info.context["request"].user + + try: + if subscription == "AUTHOR": + author_unsubscribe(user, slug) + elif subscription == "TOPIC": + topic_unsubscribe(user, slug) + elif subscription == "COMMUNITY": + community_unsubscribe(user, slug) + except Exception as e: + return {"error" : e} + return {} diff --git a/resolvers/topics.py b/resolvers/topics.py index 9e2e4d14..1d372286 100644 --- a/resolvers/topics.py +++ b/resolvers/topics.py @@ -60,29 +60,17 @@ async def update_topic(_, info, input): return { "topic" : topic } -@mutation.field("topicSubscribe") -@login_required -async def topic_subscribe(_, info, slug): - user = info.context["request"].user - +def topic_subscribe(user, slug): TopicSubscription.create( subscriber = user.slug, topic = slug) - return {} - -@mutation.field("topicUnsubscribe") -@login_required -async def topic_unsubscribe(_, info, slug): - user = info.context["request"].user - +def topic_unsubscribe(user, slug): with local_session() as session: sub = session.query(TopicSubscription).\ filter(and_(TopicSubscription.subscriber == user.slug, TopicSubscription.topic == slug)).\ first() if not sub: - return { "error" : "subscription not exist" } + raise Exception("subscription not exist") session.delete(sub) session.commit() - - return {} diff --git a/resolvers/zine.py b/resolvers/zine.py index f8e1dcf9..ac044378 100644 --- a/resolvers/zine.py +++ b/resolvers/zine.py @@ -1,5 +1,6 @@ from orm import Shout, ShoutAuthor, ShoutTopic, ShoutRating, ShoutViewByDay, User, Community, Resource,\ ShoutRatingStorage, ShoutViewStorage, Comment, CommentRating, Topic +from orm.community import CommunitySubscription from orm.base import local_session from orm.user import UserStorage, AuthorSubscription from orm.topic import TopicSubscription @@ -83,7 +84,7 @@ class ShoutsCache: lock = asyncio.Lock() @staticmethod - async def prepare_recent_shouts(): + async def prepare_recent_published(): with local_session() as session: stmt = select(Shout).\ options(selectinload(Shout.authors), selectinload(Shout.topics)).\ @@ -96,14 +97,14 @@ class ShoutsCache: shout.ratings = await ShoutRatingStorage.get_ratings(shout.slug) shouts.append(shout) async with ShoutsCache.lock: - ShoutsCache.recent_shouts = shouts + 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)).\ - where(Shout.publishedAt != None).\ + order_by(desc("createdAt")).\ limit(ShoutsCache.limit) shouts = [] for row in session.execute(stmt): @@ -198,7 +199,7 @@ class ShoutsCache: await ShoutsCache.prepare_top_month() await ShoutsCache.prepare_top_overall() await ShoutsCache.prepare_top_viewed() - await ShoutsCache.prepare_recent_shouts() + await ShoutsCache.prepare_recent_published() await ShoutsCache.prepare_recent_all() await ShoutsCache.prepare_recent_commented() print("shouts cache update finished") @@ -242,9 +243,9 @@ async def top_overall(_, info, page, size): return ShoutsCache.top_overall[(page - 1) * size : page * size] @query.field("recentPublished") -async def recent_shouts(_, info, page, size): +async def recent_published(_, info, page, size): async with ShoutsCache.lock: - return ShoutsCache.recent_shouts[(page - 1) * size : page * size] + return ShoutsCache.recent_published[(page - 1) * size : page * size] @query.field("recentAll") async def recent_all(_, info, page, size): @@ -446,13 +447,18 @@ async def shouts_subscribed(_, info, page, size): shouts_by_topic = session.query(Shout).\ join(ShoutTopic).\ join(TopicSubscription, ShoutTopic.topic == TopicSubscription.topic).\ - where(and_(Shout.publishedAt != None, TopicSubscription.subscriber == user.slug)) + where(TopicSubscription.subscriber == user.slug) shouts_by_author = session.query(Shout).\ join(ShoutAuthor).\ join(AuthorSubscription, ShoutAuthor.user == AuthorSubscription.author).\ - where(and_(Shout.publishedAt != None, AuthorSubscription.subscriber == user.slug)) + where(AuthorSubscription.subscriber == user.slug) + shouts_by_community = session.query(Shout).\ + join(Community).\ + join(CommunitySubscription).\ + where(CommunitySubscription.subscriber == user.slug) shouts = shouts_by_topic.union(shouts_by_author).\ - order_by(desc(Shout.publishedAt)).\ + union(shouts_by_community).\ + order_by(desc(Shout.createdAt)).\ limit(size).\ offset( (page - 1) * size) diff --git a/schema.graphql b/schema.graphql index dccda6a2..96c40139 100644 --- a/schema.graphql +++ b/schema.graphql @@ -7,9 +7,10 @@ type Result { } type CurrentUserInfo { - totalUnreadMessages : Int - userSubscribedTopics : [String]! - userSubscribedAuthors: [User]! + totalUnreadMessages : Int + userSubscribedTopics : [String]! + userSubscribedAuthors : [User]! + userSubscribedCommunities : [String]! } type AuthResult { @@ -93,6 +94,12 @@ type CommentUpdatedResult { comment: Comment } +enum SubscriptionType { + TOPIC + AUTHOR + COMMUNITY +} + ################################### Mutation type Mutation { @@ -118,8 +125,6 @@ type Mutation { # topics createTopic(input: TopicInput!): TopicResult! updateTopic(input: TopicInput!): TopicResult! - topicSubscribe(slug: String!): Result! - topicUnsubscribe(slug: String!): Result! createComment(body: String!, shout: String!, replyTo: Int): CommentResult! updateComment(id: Int!, body: String!): CommentResult! @@ -130,8 +135,8 @@ type Mutation { updateCommunity(community: CommunityInput!): Community! deleteCommunity(id: Int!): Result! - authorSubscribe(slug: String!): Result! - authorUnsubscribe(slug: String!): Result! + subscribe(subscription : SubscriptionType!, slug: String!): Result! + unsubscribe(subscription : SubscriptionType!, slug: String!): Result! } ################################### Query