my_rate-stat
All checks were successful
Deploy on push / deploy (push) Successful in 6s

This commit is contained in:
Untone 2024-11-12 17:56:20 +03:00
parent 34511a8edf
commit 8116160b4d
7 changed files with 83 additions and 7 deletions

View File

@ -1,4 +1,5 @@
#### [0.4.6] #### [0.4.6]
- login_accepted decorator added
- `docs` added - `docs` added
- optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions` - optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions`
- `load_shouts_bookmarked` resolver fixed - `load_shouts_bookmarked` resolver fixed

View File

@ -54,12 +54,8 @@ class JWTAuthenticate(AuthenticationBackend):
def login_required(func): def login_required(func):
@wraps(func) @wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs): async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
# debug only
# print('[auth.authenticate] login required for %r with info %r' % (func, info))
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
# print(auth)
if not auth or not auth.logged_in: if not auth or not auth.logged_in:
# raise Unauthorized(auth.error_message or "Please login")
return {"error": "Please login first"} return {"error": "Please login first"}
return await func(parent, info, *args, **kwargs) return await func(parent, info, *args, **kwargs)
@ -79,3 +75,22 @@ def permission_required(resource, operation, func):
return await func(parent, info, *args, **kwargs) return await func(parent, info, *args, **kwargs)
return wrap return wrap
def login_accepted(func):
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
auth: AuthCredentials = info.context["request"].auth
# Если есть авторизация, добавляем данные автора в контекст
if auth and auth.logged_in:
# Существующие данные auth остаются
pass
else:
# Очищаем данные автора из контекста если авторизация отсутствует
info.context["author"] = None
info.context["user_id"] = None
return await func(parent, info, *args, **kwargs)
return wrap

View File

@ -95,7 +95,6 @@ class User(Base):
ratings = relationship(UserRating, foreign_keys=UserRating.user) ratings = relationship(UserRating, foreign_keys=UserRating.user)
roles = relationship(lambda: Role, secondary=UserRole.__tablename__) roles = relationship(lambda: Role, secondary=UserRole.__tablename__)
def get_permission(self): def get_permission(self):
scope = {} scope = {}
for role in self.roles: for role in self.roles:

View File

@ -314,7 +314,7 @@ async def create_reaction(_, info, reaction):
shout = session.query(Shout).filter(Shout.id == shout_id).first() shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout: if not shout:
return {"error": "Shout not found"} return {"error": "Shout not found"}
rdict['shout'] = shout.dict() rdict["shout"] = shout.dict()
rdict["created_by"] = author_dict rdict["created_by"] = author_dict
return {"reaction": rdict} return {"reaction": rdict}
except Exception as e: except Exception as e:

View File

@ -9,6 +9,7 @@ from orm.author import Author
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic from orm.topic import Topic
from services.auth import login_accepted
from services.db import json_array_builder, json_builder, local_session from services.db import json_array_builder, json_builder, local_session
from services.schema import query from services.schema import query
from services.search import search_text from services.search import search_text
@ -161,8 +162,28 @@ def query_with_stat(info):
.group_by(Reaction.shout) .group_by(Reaction.shout)
.subquery() .subquery()
) )
author_id = info.context.get("author", {}).get("id")
user_reaction_subquery = None
if author_id:
user_reaction_subquery = (
select(Reaction.shout, Reaction.kind.label("my_rate"))
.where(
and_(
Reaction.created_by == author_id,
Reaction.deleted_at.is_(None),
Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
Reaction.reply_to.is_(None),
)
)
.distinct(Reaction.shout)
.subquery()
)
q = q.outerjoin(stats_subquery, stats_subquery.c.shout == Shout.id) q = q.outerjoin(stats_subquery, stats_subquery.c.shout == Shout.id)
if user_reaction_subquery:
q = q.outerjoin(user_reaction_subquery, user_reaction_subquery.c.shout == Shout.id)
# Добавляем поле my_rate в JSON
q = q.add_columns( q = q.add_columns(
json_builder( json_builder(
"comments_count", "comments_count",
@ -171,6 +192,8 @@ def query_with_stat(info):
stats_subquery.c.rating, stats_subquery.c.rating,
"last_reacted_at", "last_reacted_at",
stats_subquery.c.last_reacted_at, stats_subquery.c.last_reacted_at,
"my_rate",
user_reaction_subquery.c.my_rate if user_reaction_subquery else None,
).label("stat") ).label("stat")
) )
@ -337,6 +360,7 @@ def apply_sorting(q, options):
@query.field("load_shouts_by") @query.field("load_shouts_by")
@login_accepted
async def load_shouts_by(_, info, options): async def load_shouts_by(_, info, options):
""" """
Загрузка публикаций с фильтрацией, сортировкой и пагинацией. Загрузка публикаций с фильтрацией, сортировкой и пагинацией.
@ -346,7 +370,7 @@ async def load_shouts_by(_, info, options):
:param options: Опции фильтрации и сортировки. :param options: Опции фильтрации и сортировки.
:return: Список публикаций, удовлетворяющих критериям. :return: Список публикаций, удовлетворяющих критериям.
""" """
# Базовый запрос: если запрашиваются статистические данные, используем специальный запрос с статистикой # Базовый запрос: используем специальный запрос с статистикой
q = query_with_stat(info) q = query_with_stat(info)
q, limit, offset = apply_options(q, options) q, limit, offset = apply_options(q, options)

View File

@ -112,6 +112,7 @@ type Stat {
viewed: Int viewed: Int
# followed: Int # followed: Int
last_reacted_at: Int last_reacted_at: Int
my_rate: ReactionKind
} }
type CommunityStat { type CommunityStat {

View File

@ -94,3 +94,39 @@ def login_required(f):
return await f(*args, **kwargs) return await f(*args, **kwargs)
return decorated_function return decorated_function
def login_accepted(f):
"""
Декоратор, который добавляет данные авторизации в контекст, если они доступны,
но не блокирует доступ для неавторизованных пользователей.
"""
@wraps(f)
async def decorated_function(*args, **kwargs):
info = args[1]
req = info.context.get("request")
# Пробуем получить данные авторизации
user_id, user_roles = await check_auth(req)
if user_id and user_roles:
# Если пользователь авторизован, добавляем его данные в контекст
logger.info(f" got {user_id} roles: {user_roles}")
info.context["user_id"] = user_id.strip()
info.context["roles"] = user_roles
# Пробуем получить профиль автора
author = await get_cached_author_by_user_id(user_id, get_with_stat)
if not author:
logger.warning(f"author profile not found for user {user_id}")
info.context["author"] = author
else:
# Для неавторизованных пользователей очищаем контекст
info.context["user_id"] = None
info.context["roles"] = None
info.context["author"] = None
return await f(*args, **kwargs)
return decorated_function