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]
- login_accepted decorator added
- `docs` added
- optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions`
- `load_shouts_bookmarked` resolver fixed

View File

@ -54,12 +54,8 @@ class JWTAuthenticate(AuthenticationBackend):
def login_required(func):
@wraps(func)
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
# print(auth)
if not auth or not auth.logged_in:
# raise Unauthorized(auth.error_message or "Please login")
return {"error": "Please login first"}
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 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)
roles = relationship(lambda: Role, secondary=UserRole.__tablename__)
def get_permission(self):
scope = {}
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()
if not shout:
return {"error": "Shout not found"}
rdict['shout'] = shout.dict()
rdict["shout"] = shout.dict()
rdict["created_by"] = author_dict
return {"reaction": rdict}
except Exception as e:

View File

@ -9,6 +9,7 @@ from orm.author import Author
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from services.auth import login_accepted
from services.db import json_array_builder, json_builder, local_session
from services.schema import query
from services.search import search_text
@ -161,8 +162,28 @@ def query_with_stat(info):
.group_by(Reaction.shout)
.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)
if user_reaction_subquery:
q = q.outerjoin(user_reaction_subquery, user_reaction_subquery.c.shout == Shout.id)
# Добавляем поле my_rate в JSON
q = q.add_columns(
json_builder(
"comments_count",
@ -171,6 +192,8 @@ def query_with_stat(info):
stats_subquery.c.rating,
"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")
)
@ -337,6 +360,7 @@ def apply_sorting(q, options):
@query.field("load_shouts_by")
@login_accepted
async def load_shouts_by(_, info, options):
"""
Загрузка публикаций с фильтрацией, сортировкой и пагинацией.
@ -346,7 +370,7 @@ async def load_shouts_by(_, info, options):
:param options: Опции фильтрации и сортировки.
:return: Список публикаций, удовлетворяющих критериям.
"""
# Базовый запрос: если запрашиваются статистические данные, используем специальный запрос с статистикой
# Базовый запрос: используем специальный запрос с статистикой
q = query_with_stat(info)
q, limit, offset = apply_options(q, options)

View File

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

View File

@ -94,3 +94,39 @@ def login_required(f):
return await f(*args, **kwargs)
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