0.4.5-api-update
All checks were successful
Deploy on push / deploy (push) Successful in 1m49s

This commit is contained in:
Untone 2024-10-21 10:52:23 +03:00
parent 045d2ddadf
commit 160f02e67f
13 changed files with 192 additions and 37 deletions

View File

@ -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

1
cache/precache.py vendored
View File

@ -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

View File

@ -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"

View File

@ -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}

View File

@ -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):

70
resolvers/bookmark.py Normal file
View File

@ -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

View File

@ -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 []

View File

@ -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:

View File

@ -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 {

View File

@ -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!

View File

@ -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!

54
services/common_result.py Normal file
View File

@ -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)

View File

@ -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)