diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 77cef009..da613afa 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,14 @@ +[0.4.5] +- bookmark_shout mutation resolver added +- load_bookmarked_shouts resolver fix +- community stats in orm +- get_communities_by_author resolver added +- get_communities_all resolver fix +- reaction filter by kinds +- reaction sort enum added + [0.4.4] -- followers_stat removed +- followers_stat removed for shout - sqlite3 support added - rating_stat and commented_stat fix diff --git a/cache/precache.py b/cache/precache.py index 046f206a..9f4731e6 100644 --- a/cache/precache.py +++ b/cache/precache.py @@ -10,7 +10,6 @@ from orm.topic import Topic, TopicFollower from resolvers.stat import get_with_stat from services.db import local_session from services.redis import redis -from settings import REDIS_URL from utils.encoders import CustomJSONEncoder from utils.logger import root_logger as logger diff --git a/orm/author.py b/orm/author.py index c96fbcf5..c83b9d4f 100644 --- a/orm/author.py +++ b/orm/author.py @@ -26,6 +26,14 @@ class AuthorFollower(Base): auto = Column(Boolean, nullable=False, default=False) +class AuthorBookmark(Base): + __tablename__ = "author_bookmark" + + id = None # type: ignore + author = Column(ForeignKey("author.id"), primary_key=True) + shout = Column(ForeignKey("shout.id"), primary_key=True) + + class Author(Base): __tablename__ = "author" diff --git a/orm/community.py b/orm/community.py index b98a0650..3e1aa1ab 100644 --- a/orm/community.py +++ b/orm/community.py @@ -1,6 +1,8 @@ import time -from sqlalchemy import Column, ForeignKey, Integer, String +from requests import Session +from sqlalchemy import Column, ForeignKey, Integer, String, func +from sqlalchemy.ext.hybrid import hybrid_method from sqlalchemy.orm import relationship from orm.author import Author @@ -27,3 +29,17 @@ class Community(Base): created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) authors = relationship(Author, secondary="community_author") + + @hybrid_method + def get_stats(self, session: Session): + from orm.shout import ShoutCommunity # Импорт здесь во избежание циклических зависимостей + + shouts_count = ( + session.query(func.count(ShoutCommunity.shout_id)).filter(ShoutCommunity.community_id == self.id).scalar() + ) + + followers_count = ( + session.query(func.count(CommunityFollower.author)).filter(CommunityFollower.community == self.id).scalar() + ) + + return {"shouts": shouts_count, "followers": followers_count} diff --git a/orm/notification.py b/orm/notification.py index d07c5f59..59a16e0f 100644 --- a/orm/notification.py +++ b/orm/notification.py @@ -5,8 +5,7 @@ from sqlalchemy import JSON, Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from orm.author import Author -from services.db import Base, create_table_if_not_exists, engine -from utils.logger import root_logger as logger +from services.db import Base class NotificationEntity(Enumeration): diff --git a/resolvers/bookmark.py b/resolvers/bookmark.py new file mode 100644 index 00000000..2a728387 --- /dev/null +++ b/resolvers/bookmark.py @@ -0,0 +1,70 @@ +from graphql import GraphQLError +from sqlalchemy import delete, insert + +from orm.author import AuthorBookmark +from orm.shout import Shout +from services.common_result import CommonResult +from services.db import local_session +from services.schema import mutation, query + + +@query.field("load_shouts_bookmarked") +def load_shouts_bookmarked(_, info, limit=50, offset=0): + """ + Load bookmarked shouts for the authenticated user. + + Args: + limit (int): Maximum number of shouts to return. + offset (int): Number of shouts to skip. + + Returns: + list: List of bookmarked shouts. + """ + author_dict = info.context.get("author", {}) + author_id = author_dict.get("id") + if not author_id: + raise GraphQLError("User not authenticated") + result = [] + with local_session() as db: + result = db.query(AuthorBookmark).where(AuthorBookmark.author == author_id).offset(offset).limit(limit).all() + return result + + +@mutation.field("toggle_bookmark_shout") +def toggle_bookmark_shout(_, info, slug: str) -> CommonResult: + """ + Toggle bookmark status for a specific shout. + + Args: + slug (str): Unique identifier of the shout. + + Returns: + CommonResult: Result of the operation with bookmark status. + """ + author_dict = info.context.get("author", {}) + author_id = author_dict.get("id") + if not author_id: + raise GraphQLError("User not authenticated") + + with local_session() as db: + shout = db.query(Shout).filter(Shout.slug == slug).first() + if not shout: + raise GraphQLError("Shout not found") + + existing_bookmark = ( + db.query(AuthorBookmark) + .filter(AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id) + .first() + ) + + if existing_bookmark: + db.execute( + delete(AuthorBookmark).where(AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id) + ) + result = False + else: + db.execute(insert(AuthorBookmark).values(author=author_id, shout=shout.id)) + result = True + + db.commit() + return result diff --git a/resolvers/community.py b/resolvers/community.py index ffc51316..6d633ec1 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -1,36 +1,31 @@ -from sqlalchemy import select - from orm.author import Author -from orm.community import Community +from orm.community import Community, CommunityFollower from services.db import local_session from services.schema import query -def get_communities_from_query(q): - ccc = [] - with local_session() as session: - for [c, shouts_stat, followers_stat] in session.execute(q): - c.stat = { - "shouts": shouts_stat, - "followers": followers_stat, - # "authors": session.execute(select(func.count(distinct(ShoutCommunity.shout))).filter(ShoutCommunity.community == c.id)), - # "commented": commented_stat, - } - ccc.append(c) - - return ccc - - @query.field("get_communities_all") async def get_communities_all(_, _info): - q = select(Author) - - return get_communities_from_query(q) + return local_session().query(Community).all() @query.field("get_community") async def get_community(_, _info, slug: str): - q = select(Community).where(Community.slug == slug) + q = local_session().query(Community).where(Community.slug == slug) + return q.first() - communities = get_communities_from_query(q) - return communities[0] + +@query.field("get_communities_by_author") +async def get_communities_by_author(_, _info, slug="", user="", author_id=0): + with local_session() as session: + q = session.query(Community).join(CommunityFollower) + if slug: + author_id = session.query(Author).where(Author.slug == slug).first().id + q = q.where(CommunityFollower.author == author_id) + if user: + author_id = session.query(Author).where(Author.user == user).first().id + q = q.where(CommunityFollower.author == author_id) + if author_id: + q = q.where(CommunityFollower.author == author_id) + return q.all() + return [] diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 672d169b..c2fbfc1b 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -430,11 +430,12 @@ def apply_reaction_filters(by, q): if isinstance(topic, int): q = q.filter(Shout.topics.any(id=topic)) - if by.get("comment"): - q = q.filter(Reaction.kind == ReactionKind.COMMENT.value) + kinds = by.get("kinds") + if isinstance(kinds, list): + q = q.filter(Reaction.kind.in_(kinds)) - if by.get("rating"): - q = q.filter(Reaction.kind.in_(RATING_REACTIONS)) + if by.get("reply_to"): + q = q.filter(Reaction.reply_to == by.get("reply_to")) by_search = by.get("search", "") if len(by_search) > 2: diff --git a/schema/input.graphql b/schema/input.graphql index 027cfe57..a794969d 100644 --- a/schema/input.graphql +++ b/schema/input.graphql @@ -72,13 +72,13 @@ input ReactionBy { shout: String shouts: [String] search: String - comment: Boolean - rating: Boolean + kinds: [ReactionKind] + reply_to: Int # filter topic: String created_by: Int author: String after: Int - sort: ReactionSort + sort: ReactionSort # sort } input NotificationSeenInput { diff --git a/schema/mutation.graphql b/schema/mutation.graphql index e3336e1d..4bb05c15 100644 --- a/schema/mutation.graphql +++ b/schema/mutation.graphql @@ -29,6 +29,9 @@ type Mutation { accept_invite(invite_id: Int!): CommonResult! reject_invite(invite_id: Int!): CommonResult! + # bookmark + toggle_bookmark_shout(slug: String!): CommonResult! + # notifier notification_mark_seen(notification_id: Int!, seen: Boolean): CommonResult! notifications_seen_after(after: Int!, seen: Boolean): CommonResult! diff --git a/schema/query.graphql b/schema/query.graphql index 87511d02..e52a7acb 100644 --- a/schema/query.graphql +++ b/schema/query.graphql @@ -37,6 +37,7 @@ type Query { load_shouts_discussed(limit: Int, offset: Int): [Shout] load_shouts_random_top(options: LoadShoutsOptions): [Shout] load_shouts_random_topic(limit: Int!): CommonResult! # { topic shouts } + load_shouts_bookmarked(limit: Int, offset: Int): [Shout] # editor get_my_shout(shout_id: Int!): CommonResult! diff --git a/services/common_result.py b/services/common_result.py new file mode 100644 index 00000000..354dfc23 --- /dev/null +++ b/services/common_result.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +from orm.author import Author +from orm.community import Community +from orm.reaction import Reaction +from orm.shout import Shout +from orm.topic import Topic + + +@dataclass +class CommonResult: + error: Optional[str] = None + slugs: Optional[List[str]] = None + shout: Optional[Shout] = None + shouts: Optional[List[Shout]] = None + author: Optional[Author] = None + authors: Optional[List[Author]] = None + reaction: Optional[Reaction] = None + reactions: Optional[List[Reaction]] = None + topic: Optional[Topic] = None + topics: Optional[List[Topic]] = None + community: Optional[Community] = None + communities: Optional[List[Community]] = None + + @classmethod + def ok(cls, data: Dict[str, Any]) -> "CommonResult": + """ + Создает успешный результат. + + Args: + data: Словарь с данными для включения в результат. + + Returns: + CommonResult: Экземпляр с предоставленными данными. + """ + result = cls() + for key, value in data.items(): + if hasattr(result, key): + setattr(result, key, value) + return result + + @classmethod + def error(cls, message: str): + """ + Create an error result. + + Args: + message: The error message. + + Returns: + CommonResult: An instance with the error message. + """ + return cls(error=message) diff --git a/services/sentry.py b/services/sentry.py index 4a35fee7..5cf87f46 100644 --- a/services/sentry.py +++ b/services/sentry.py @@ -26,5 +26,5 @@ def start_sentry(): send_default_pii=True, # Отправка информации о пользователе (PII) ) logger.info("[services.sentry] Sentry initialized successfully.") - except Exception as e: + except Exception as _e: logger.warning("[services.sentry] Failed to initialize Sentry", exc_info=True)