This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
from cache.triggers import events_register
|
||||
from resolvers.admin import (
|
||||
admin_get_roles,
|
||||
admin_get_users,
|
||||
)
|
||||
from resolvers.auth import (
|
||||
confirm_email,
|
||||
get_current_user,
|
||||
login,
|
||||
register_by_email,
|
||||
send_link,
|
||||
)
|
||||
from resolvers.author import ( # search_authors,
|
||||
get_author,
|
||||
get_author_followers,
|
||||
@@ -16,8 +27,8 @@ from resolvers.draft import (
|
||||
delete_draft,
|
||||
load_drafts,
|
||||
publish_draft,
|
||||
update_draft,
|
||||
unpublish_draft,
|
||||
update_draft,
|
||||
)
|
||||
from resolvers.editor import (
|
||||
unpublish_shout,
|
||||
@@ -62,19 +73,6 @@ from resolvers.topic import (
|
||||
get_topics_by_community,
|
||||
)
|
||||
|
||||
from resolvers.auth import (
|
||||
get_current_user,
|
||||
confirm_email,
|
||||
register_by_email,
|
||||
send_link,
|
||||
login,
|
||||
)
|
||||
|
||||
from resolvers.admin import (
|
||||
admin_get_users,
|
||||
admin_get_roles,
|
||||
)
|
||||
|
||||
events_register()
|
||||
|
||||
__all__ = [
|
||||
@@ -84,11 +82,9 @@ __all__ = [
|
||||
"register_by_email",
|
||||
"send_link",
|
||||
"login",
|
||||
|
||||
# admin
|
||||
"admin_get_users",
|
||||
"admin_get_roles",
|
||||
|
||||
# author
|
||||
"get_author",
|
||||
"get_author_followers",
|
||||
@@ -100,11 +96,9 @@ __all__ = [
|
||||
"load_authors_search",
|
||||
"update_author",
|
||||
# "search_authors",
|
||||
|
||||
# community
|
||||
"get_community",
|
||||
"get_communities_all",
|
||||
|
||||
# topic
|
||||
"get_topic",
|
||||
"get_topics_all",
|
||||
@@ -112,14 +106,12 @@ __all__ = [
|
||||
"get_topics_by_author",
|
||||
"get_topic_followers",
|
||||
"get_topic_authors",
|
||||
|
||||
# reader
|
||||
"get_shout",
|
||||
"load_shouts_by",
|
||||
"load_shouts_random_top",
|
||||
"load_shouts_search",
|
||||
"load_shouts_unrated",
|
||||
|
||||
# feed
|
||||
"load_shouts_feed",
|
||||
"load_shouts_coauthored",
|
||||
@@ -127,12 +119,10 @@ __all__ = [
|
||||
"load_shouts_with_topic",
|
||||
"load_shouts_followed_by",
|
||||
"load_shouts_authored_by",
|
||||
|
||||
# follower
|
||||
"follow",
|
||||
"unfollow",
|
||||
"get_shout_followers",
|
||||
|
||||
# reaction
|
||||
"create_reaction",
|
||||
"update_reaction",
|
||||
@@ -142,18 +132,15 @@ __all__ = [
|
||||
"load_shout_ratings",
|
||||
"load_comment_ratings",
|
||||
"load_comments_branch",
|
||||
|
||||
# notifier
|
||||
"load_notifications",
|
||||
"notifications_seen_thread",
|
||||
"notifications_seen_after",
|
||||
"notification_mark_seen",
|
||||
|
||||
# rating
|
||||
"rate_author",
|
||||
"get_my_rates_comments",
|
||||
"get_my_rates_shouts",
|
||||
|
||||
# draft
|
||||
"load_drafts",
|
||||
"create_draft",
|
||||
|
@@ -1,12 +1,13 @@
|
||||
from math import ceil
|
||||
from sqlalchemy import or_, cast, String
|
||||
|
||||
from graphql.error import GraphQLError
|
||||
from sqlalchemy import String, cast, or_
|
||||
|
||||
from auth.decorators import admin_auth_required
|
||||
from auth.orm import Author, AuthorRole, Role
|
||||
from services.db import local_session
|
||||
from services.schema import query, mutation
|
||||
from auth.orm import Author, Role, AuthorRole
|
||||
from services.env import EnvManager, EnvVariable
|
||||
from services.schema import mutation, query
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@@ -64,11 +65,9 @@ async def admin_get_users(_, info, limit=10, offset=0, search=None):
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"slug": user.slug,
|
||||
"roles": [role.id for role in user.roles]
|
||||
if hasattr(user, "roles") and user.roles
|
||||
else [],
|
||||
"roles": [role.id for role in user.roles] if hasattr(user, "roles") and user.roles else [],
|
||||
"created_at": user.created_at,
|
||||
"last_seen": user.last_seen
|
||||
"last_seen": user.last_seen,
|
||||
}
|
||||
for user in users
|
||||
],
|
||||
@@ -81,6 +80,7 @@ async def admin_get_users(_, info, limit=10, offset=0, search=None):
|
||||
return result
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
logger.error(f"Ошибка при получении списка пользователей: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise GraphQLError(f"Не удалось получить список пользователей: {str(e)}")
|
||||
@@ -126,20 +126,20 @@ async def admin_get_roles(_, info):
|
||||
async def get_env_variables(_, info):
|
||||
"""
|
||||
Получает список переменных окружения, сгруппированных по секциям
|
||||
|
||||
|
||||
Args:
|
||||
info: Контекст GraphQL запроса
|
||||
|
||||
|
||||
Returns:
|
||||
Список секций с переменными окружения
|
||||
"""
|
||||
try:
|
||||
# Создаем экземпляр менеджера переменных окружения
|
||||
env_manager = EnvManager()
|
||||
|
||||
|
||||
# Получаем все переменные
|
||||
sections = env_manager.get_all_variables()
|
||||
|
||||
|
||||
# Преобразуем к формату GraphQL API
|
||||
result = [
|
||||
{
|
||||
@@ -154,11 +154,11 @@ async def get_env_variables(_, info):
|
||||
"isSecret": var.is_secret,
|
||||
}
|
||||
for var in section.variables
|
||||
]
|
||||
],
|
||||
}
|
||||
for section in sections
|
||||
]
|
||||
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении переменных окружения: {str(e)}")
|
||||
@@ -170,27 +170,27 @@ async def get_env_variables(_, info):
|
||||
async def update_env_variable(_, info, key, value):
|
||||
"""
|
||||
Обновляет значение переменной окружения
|
||||
|
||||
|
||||
Args:
|
||||
info: Контекст GraphQL запроса
|
||||
key: Ключ переменной
|
||||
value: Новое значение
|
||||
|
||||
|
||||
Returns:
|
||||
Boolean: результат операции
|
||||
"""
|
||||
try:
|
||||
# Создаем экземпляр менеджера переменных окружения
|
||||
env_manager = EnvManager()
|
||||
|
||||
|
||||
# Обновляем переменную
|
||||
result = env_manager.update_variable(key, value)
|
||||
|
||||
|
||||
if result:
|
||||
logger.info(f"Переменная окружения '{key}' успешно обновлена")
|
||||
else:
|
||||
logger.error(f"Не удалось обновить переменную окружения '{key}'")
|
||||
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении переменной окружения: {str(e)}")
|
||||
@@ -202,36 +202,32 @@ async def update_env_variable(_, info, key, value):
|
||||
async def update_env_variables(_, info, variables):
|
||||
"""
|
||||
Массовое обновление переменных окружения
|
||||
|
||||
|
||||
Args:
|
||||
info: Контекст GraphQL запроса
|
||||
variables: Список переменных для обновления
|
||||
|
||||
|
||||
Returns:
|
||||
Boolean: результат операции
|
||||
"""
|
||||
try:
|
||||
# Создаем экземпляр менеджера переменных окружения
|
||||
env_manager = EnvManager()
|
||||
|
||||
|
||||
# Преобразуем входные данные в формат для менеджера
|
||||
env_variables = [
|
||||
EnvVariable(
|
||||
key=var.get("key", ""),
|
||||
value=var.get("value", ""),
|
||||
type=var.get("type", "string")
|
||||
)
|
||||
EnvVariable(key=var.get("key", ""), value=var.get("value", ""), type=var.get("type", "string"))
|
||||
for var in variables
|
||||
]
|
||||
|
||||
|
||||
# Обновляем переменные
|
||||
result = env_manager.update_variables(env_variables)
|
||||
|
||||
|
||||
if result:
|
||||
logger.info(f"Переменные окружения успешно обновлены ({len(variables)} шт.)")
|
||||
else:
|
||||
logger.error(f"Не удалось обновить переменные окружения")
|
||||
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при массовом обновлении переменных окружения: {str(e)}")
|
||||
@@ -243,90 +239,78 @@ async def update_env_variables(_, info, variables):
|
||||
async def admin_update_user(_, info, user):
|
||||
"""
|
||||
Обновляет роли пользователя
|
||||
|
||||
|
||||
Args:
|
||||
info: Контекст GraphQL запроса
|
||||
user: Данные для обновления пользователя (содержит id и roles)
|
||||
|
||||
|
||||
Returns:
|
||||
Boolean: результат операции или объект с ошибкой
|
||||
"""
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
roles = user.get("roles", [])
|
||||
|
||||
|
||||
if not roles:
|
||||
logger.warning(f"Пользователю {user_id} не назначено ни одной роли. Доступ в систему будет заблокирован.")
|
||||
|
||||
|
||||
with local_session() as session:
|
||||
# Получаем пользователя из базы данных
|
||||
author = session.query(Author).filter(Author.id == user_id).first()
|
||||
|
||||
|
||||
if not author:
|
||||
error_msg = f"Пользователь с ID {user_id} не найден"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
# Получаем ID сообщества по умолчанию
|
||||
default_community_id = 1 # Используем значение по умолчанию из модели AuthorRole
|
||||
|
||||
|
||||
try:
|
||||
# Очищаем текущие роли пользователя через ORM
|
||||
session.query(AuthorRole).filter(AuthorRole.author == user_id).delete()
|
||||
session.flush()
|
||||
|
||||
|
||||
# Получаем все существующие роли, которые указаны для обновления
|
||||
role_objects = session.query(Role).filter(Role.id.in_(roles)).all()
|
||||
|
||||
|
||||
# Проверяем, все ли запрошенные роли найдены
|
||||
found_role_ids = [role.id for role in role_objects]
|
||||
missing_roles = set(roles) - set(found_role_ids)
|
||||
|
||||
|
||||
if missing_roles:
|
||||
warning_msg = f"Некоторые роли не найдены в базе: {', '.join(missing_roles)}"
|
||||
logger.warning(warning_msg)
|
||||
|
||||
|
||||
# Создаем новые записи в таблице author_role с указанием community
|
||||
for role in role_objects:
|
||||
# Используем ORM для создания новых записей
|
||||
author_role = AuthorRole(
|
||||
community=default_community_id,
|
||||
author=user_id,
|
||||
role=role.id
|
||||
)
|
||||
author_role = AuthorRole(community=default_community_id, author=user_id, role=role.id)
|
||||
session.add(author_role)
|
||||
|
||||
|
||||
# Сохраняем изменения в базе данных
|
||||
session.commit()
|
||||
|
||||
|
||||
# Проверяем, добавлена ли пользователю роль reader
|
||||
has_reader = 'reader' in [role.id for role in role_objects]
|
||||
has_reader = "reader" in [role.id for role in role_objects]
|
||||
if not has_reader:
|
||||
logger.warning(f"Пользователю {author.email or author.id} не назначена роль 'reader'. Доступ в систему будет ограничен.")
|
||||
|
||||
logger.warning(
|
||||
f"Пользователю {author.email or author.id} не назначена роль 'reader'. Доступ в систему будет ограничен."
|
||||
)
|
||||
|
||||
logger.info(f"Роли пользователя {author.email or author.id} обновлены: {', '.join(found_role_ids)}")
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
# Обработка вложенных исключений
|
||||
session.rollback()
|
||||
error_msg = f"Ошибка при изменении ролей: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
return {"success": False, "error": error_msg}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_msg = f"Ошибка при обновлении ролей пользователя: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
return {"success": False, "error": error_msg}
|
||||
|
@@ -1,46 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import traceback
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from graphql.type import GraphQLResolveInfo
|
||||
# import asyncio # Убираем, так как резолвер будет синхронным
|
||||
|
||||
from services.auth import login_required
|
||||
from auth.credentials import AuthCredentials
|
||||
from auth.email import send_auth_email
|
||||
from auth.exceptions import InvalidToken, ObjectNotExist
|
||||
from auth.identity import Identity, Password
|
||||
from auth.internal import verify_internal_auth
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.tokenstorage import TokenStorage
|
||||
from auth.orm import Author, Role
|
||||
from auth.sessions import SessionManager
|
||||
from auth.tokenstorage import TokenStorage
|
||||
|
||||
# import asyncio # Убираем, так как резолвер будет синхронным
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
from services.schema import mutation, query
|
||||
from settings import (
|
||||
ADMIN_EMAILS,
|
||||
SESSION_TOKEN_HEADER,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SECURE,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE,
|
||||
SESSION_TOKEN_HEADER,
|
||||
)
|
||||
from utils.generate_slug import generate_unique_slug
|
||||
from auth.sessions import SessionManager
|
||||
from auth.internal import verify_internal_auth
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@mutation.field("getSession")
|
||||
@login_required
|
||||
async def get_current_user(_, info):
|
||||
"""
|
||||
Получает информацию о текущем пользователе.
|
||||
|
||||
|
||||
Требует авторизации через декоратор login_required.
|
||||
|
||||
|
||||
Args:
|
||||
_: Родительский объект (не используется)
|
||||
info: Контекст GraphQL запроса
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Объект с токеном и данными автора с добавленной статистикой
|
||||
"""
|
||||
@@ -49,68 +51,73 @@ async def get_current_user(_, info):
|
||||
if not author_id:
|
||||
logger.error("[getSession] Пользователь не авторизован")
|
||||
from graphql.error import GraphQLError
|
||||
|
||||
raise GraphQLError("Требуется авторизация")
|
||||
|
||||
|
||||
# Получаем токен из заголовка
|
||||
req = info.context.get("request")
|
||||
token = req.headers.get(SESSION_TOKEN_HEADER)
|
||||
if token and token.startswith("Bearer "):
|
||||
token = token.split("Bearer ")[-1].strip()
|
||||
|
||||
|
||||
# Получаем данные автора
|
||||
author = info.context.get("author")
|
||||
|
||||
|
||||
# Если автор не найден в контексте, пробуем получить из БД с добавлением статистики
|
||||
if not author:
|
||||
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {user_id}, получаем из БД")
|
||||
|
||||
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {author_id}, получаем из БД")
|
||||
|
||||
try:
|
||||
# Используем функцию get_with_stat для получения автора со статистикой
|
||||
from sqlalchemy import select
|
||||
|
||||
from resolvers.stat import get_with_stat
|
||||
|
||||
q = select(Author).where(Author.id == user_id)
|
||||
|
||||
q = select(Author).where(Author.id == author_id)
|
||||
authors_with_stat = get_with_stat(q)
|
||||
|
||||
|
||||
if authors_with_stat and len(authors_with_stat) > 0:
|
||||
author = authors_with_stat[0]
|
||||
|
||||
|
||||
# Обновляем last_seen отдельной транзакцией
|
||||
with local_session() as session:
|
||||
author_db = session.query(Author).filter(Author.id == user_id).first()
|
||||
author_db = session.query(Author).filter(Author.id == author_id).first()
|
||||
if author_db:
|
||||
author_db.last_seen = int(time.time())
|
||||
session.commit()
|
||||
else:
|
||||
logger.error(f"[getSession] Автор с ID {user_id} не найден в БД")
|
||||
logger.error(f"[getSession] Автор с ID {author_id} не найден в БД")
|
||||
from graphql.error import GraphQLError
|
||||
|
||||
raise GraphQLError("Пользователь не найден")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[getSession] Ошибка при получении автора из БД: {e}", exc_info=True)
|
||||
from graphql.error import GraphQLError
|
||||
|
||||
raise GraphQLError("Ошибка при получении данных пользователя")
|
||||
else:
|
||||
# Если автор уже есть в контексте, добавляем статистику
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
from resolvers.stat import get_with_stat
|
||||
|
||||
q = select(Author).where(Author.id == user_id)
|
||||
|
||||
q = select(Author).where(Author.id == author_id)
|
||||
authors_with_stat = get_with_stat(q)
|
||||
|
||||
|
||||
if authors_with_stat and len(authors_with_stat) > 0:
|
||||
# Обновляем только статистику
|
||||
author.stat = authors_with_stat[0].stat
|
||||
except Exception as e:
|
||||
logger.warning(f"[getSession] Не удалось добавить статистику к автору: {e}")
|
||||
|
||||
|
||||
# Возвращаем данные сессии
|
||||
logger.info(f"[getSession] Успешно получена сессия для пользователя {user_id}")
|
||||
return {"token": token or '', "author": author}
|
||||
logger.info(f"[getSession] Успешно получена сессия для пользователя {author_id}")
|
||||
return {"token": token or "", "author": author}
|
||||
|
||||
|
||||
@mutation.field("confirmEmail")
|
||||
@mutation.field("confirmEmail")
|
||||
async def confirm_email(_, info, token):
|
||||
"""confirm owning email address"""
|
||||
try:
|
||||
@@ -118,26 +125,26 @@ async def confirm_email(_, info, token):
|
||||
payload = JWTCodec.decode(token)
|
||||
user_id = payload.user_id
|
||||
username = payload.username
|
||||
|
||||
|
||||
# Если TokenStorage.get асинхронный, это нужно будет переделать или вызывать синхронно
|
||||
# Для теста пока оставим, но это потенциальная точка отказа в синхронном резолвере
|
||||
token_key = f"{user_id}-{username}-{token}"
|
||||
await TokenStorage.get(token_key)
|
||||
|
||||
|
||||
with local_session() as session:
|
||||
user = session.query(Author).where(Author.id == user_id).first()
|
||||
if not user:
|
||||
logger.warning(f"[auth] confirmEmail: Пользователь с ID {user_id} не найден.")
|
||||
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
||||
|
||||
|
||||
# Создаем сессионный токен с новым форматом вызова и явным временем истечения
|
||||
device_info = {"email": user.email} if hasattr(user, "email") else None
|
||||
session_token = await TokenStorage.create_session(
|
||||
user_id=str(user_id),
|
||||
username=user.username or user.email or user.slug or username,
|
||||
device_info=device_info
|
||||
device_info=device_info,
|
||||
)
|
||||
|
||||
|
||||
user.email_verified = True
|
||||
user.last_seen = int(time.time())
|
||||
session.add(user)
|
||||
@@ -155,7 +162,7 @@ async def confirm_email(_, info, token):
|
||||
"token": None,
|
||||
"author": None,
|
||||
"error": f"Ошибка подтверждения email: {str(e)}",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_user(user_dict):
|
||||
@@ -231,9 +238,7 @@ async def register_by_email(_, _info, email: str, password: str = "", name: str
|
||||
try:
|
||||
# Если auth_send_link асинхронный...
|
||||
await send_link(_, _info, email)
|
||||
logger.info(
|
||||
f"[auth] registerUser: Пользователь {email} зарегистрирован, ссылка для подтверждения отправлена."
|
||||
)
|
||||
logger.info(f"[auth] registerUser: Пользователь {email} зарегистрирован, ссылка для подтверждения отправлена.")
|
||||
# При регистрации возвращаем данные самому пользователю, поэтому не фильтруем
|
||||
return {
|
||||
"success": True,
|
||||
@@ -306,7 +311,7 @@ async def login(_, info, email: str, password: str):
|
||||
logger.info(
|
||||
f"[auth] login: Найден автор {email}, id={author.id}, имя={author.name}, пароль есть: {bool(author.password)}"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем наличие роли reader
|
||||
has_reader_role = False
|
||||
if hasattr(author, "roles") and author.roles:
|
||||
@@ -314,12 +319,12 @@ async def login(_, info, email: str, password: str):
|
||||
if role.id == "reader":
|
||||
has_reader_role = True
|
||||
break
|
||||
|
||||
|
||||
# Если у пользователя нет роли reader и он не админ, запрещаем вход
|
||||
if not has_reader_role:
|
||||
# Проверяем, есть ли роль admin или super
|
||||
is_admin = author.email in ADMIN_EMAILS.split(",")
|
||||
|
||||
|
||||
if not is_admin:
|
||||
logger.warning(f"[auth] login: У пользователя {email} нет роли 'reader', в доступе отказано")
|
||||
return {
|
||||
@@ -365,9 +370,7 @@ async def login(_, info, email: str, password: str):
|
||||
or not hasattr(valid_author, "username")
|
||||
and not hasattr(valid_author, "email")
|
||||
):
|
||||
logger.error(
|
||||
f"[auth] login: Объект автора не содержит необходимых атрибутов: {valid_author}"
|
||||
)
|
||||
logger.error(f"[auth] login: Объект автора не содержит необходимых атрибутов: {valid_author}")
|
||||
return {
|
||||
"success": False,
|
||||
"token": None,
|
||||
@@ -380,7 +383,7 @@ async def login(_, info, email: str, password: str):
|
||||
token = await TokenStorage.create_session(
|
||||
user_id=str(valid_author.id),
|
||||
username=valid_author.username or valid_author.email or valid_author.slug or "",
|
||||
device_info={"email": valid_author.email} if hasattr(valid_author, "email") else None
|
||||
device_info={"email": valid_author.email} if hasattr(valid_author, "email") else None,
|
||||
)
|
||||
logger.info(f"[auth] login: токен успешно создан, длина: {len(token) if token else 0}")
|
||||
|
||||
@@ -390,7 +393,7 @@ async def login(_, info, email: str, password: str):
|
||||
|
||||
# Устанавливаем httponly cookie различными способами для надежности
|
||||
cookie_set = False
|
||||
|
||||
|
||||
# Метод 1: GraphQL контекст через extensions
|
||||
try:
|
||||
if hasattr(info.context, "extensions") and hasattr(info.context.extensions, "set_cookie"):
|
||||
@@ -406,7 +409,7 @@ async def login(_, info, email: str, password: str):
|
||||
cookie_set = True
|
||||
except Exception as e:
|
||||
logger.error(f"[auth] login: Ошибка при установке cookie через extensions: {str(e)}")
|
||||
|
||||
|
||||
# Метод 2: GraphQL контекст через response
|
||||
if not cookie_set:
|
||||
try:
|
||||
@@ -423,11 +426,12 @@ async def login(_, info, email: str, password: str):
|
||||
cookie_set = True
|
||||
except Exception as e:
|
||||
logger.error(f"[auth] login: Ошибка при установке cookie через response: {str(e)}")
|
||||
|
||||
|
||||
# Если ни один способ не сработал, создаем response в контексте
|
||||
if not cookie_set and hasattr(info.context, "request") and not hasattr(info.context, "response"):
|
||||
try:
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
response = JSONResponse({})
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
@@ -442,12 +446,12 @@ async def login(_, info, email: str, password: str):
|
||||
cookie_set = True
|
||||
except Exception as e:
|
||||
logger.error(f"[auth] login: Ошибка при создании response и установке cookie: {str(e)}")
|
||||
|
||||
|
||||
if not cookie_set:
|
||||
logger.warning(f"[auth] login: Не удалось установить cookie никаким способом")
|
||||
|
||||
|
||||
# Возвращаем успешный результат с данными для клиента
|
||||
# Для ответа клиенту используем dict() с параметром access=True,
|
||||
# Для ответа клиенту используем dict() с параметром access=True,
|
||||
# чтобы получить полный доступ к данным для самого пользователя
|
||||
logger.info(f"[auth] login: Успешный вход для {email}")
|
||||
author_dict = valid_author.dict(access=True)
|
||||
@@ -485,7 +489,7 @@ async def is_email_used(_, _info, email):
|
||||
async def logout_resolver(_, info: GraphQLResolveInfo):
|
||||
"""
|
||||
Выход из системы через GraphQL с удалением сессии и cookie.
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Результат операции выхода
|
||||
"""
|
||||
@@ -500,7 +504,7 @@ async def logout_resolver(_, info: GraphQLResolveInfo):
|
||||
|
||||
success = False
|
||||
message = ""
|
||||
|
||||
|
||||
# Если токен найден, отзываем его
|
||||
if token:
|
||||
try:
|
||||
@@ -544,12 +548,12 @@ async def logout_resolver(_, info: GraphQLResolveInfo):
|
||||
async def refresh_token_resolver(_, info: GraphQLResolveInfo):
|
||||
"""
|
||||
Обновление токена аутентификации через GraphQL.
|
||||
|
||||
|
||||
Returns:
|
||||
AuthResult с данными пользователя и обновленным токеном или сообщением об ошибке
|
||||
"""
|
||||
request = info.context["request"]
|
||||
|
||||
|
||||
# Получаем текущий токен из cookie или заголовка
|
||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if not token:
|
||||
@@ -617,12 +621,7 @@ async def refresh_token_resolver(_, info: GraphQLResolveInfo):
|
||||
logger.debug(traceback.format_exc())
|
||||
|
||||
logger.info(f"[auth] refresh_token_resolver: Токен успешно обновлен для пользователя {user_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"token": new_token,
|
||||
"author": author,
|
||||
"error": None
|
||||
}
|
||||
return {"success": True, "token": new_token, "author": author, "error": None}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[auth] refresh_token_resolver: Ошибка при обновлении токена: {e}")
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from auth.orm import Author
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cached_query,
|
||||
@@ -13,7 +14,6 @@ from cache.cache import (
|
||||
get_cached_follower_topics,
|
||||
invalidate_cache_by_prefix,
|
||||
)
|
||||
from auth.orm import Author
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
@@ -74,27 +74,26 @@ async def get_authors_with_stats(limit=50, offset=0, by: Optional[str] = None, c
|
||||
|
||||
# Функция для получения авторов из БД
|
||||
async def fetch_authors_with_stats():
|
||||
logger.debug(
|
||||
f"Выполняем запрос на получение авторов со статистикой: limit={limit}, offset={offset}, by={by}"
|
||||
)
|
||||
logger.debug(f"Выполняем запрос на получение авторов со статистикой: limit={limit}, offset={offset}, by={by}")
|
||||
|
||||
with local_session() as session:
|
||||
# Базовый запрос для получения авторов
|
||||
base_query = select(Author).where(Author.deleted_at.is_(None))
|
||||
|
||||
# Применяем сортировку
|
||||
|
||||
|
||||
# vars for statistics sorting
|
||||
stats_sort_field = None
|
||||
stats_sort_direction = "desc"
|
||||
|
||||
|
||||
if by:
|
||||
if isinstance(by, dict):
|
||||
logger.debug(f"Processing dict-based sorting: {by}")
|
||||
# Обработка словаря параметров сортировки
|
||||
from sqlalchemy import asc, desc, func
|
||||
from orm.shout import ShoutAuthor
|
||||
|
||||
from auth.orm import AuthorFollower
|
||||
from orm.shout import ShoutAuthor
|
||||
|
||||
# Checking for order field in the dictionary
|
||||
if "order" in by:
|
||||
@@ -135,50 +134,40 @@ async def get_authors_with_stats(limit=50, offset=0, by: Optional[str] = None, c
|
||||
# If sorting by statistics, modify the query
|
||||
if stats_sort_field == "shouts":
|
||||
# Sorting by the number of shouts
|
||||
from sqlalchemy import func, and_
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
|
||||
|
||||
subquery = (
|
||||
select(
|
||||
ShoutAuthor.author,
|
||||
func.count(func.distinct(Shout.id)).label("shouts_count")
|
||||
)
|
||||
select(ShoutAuthor.author, func.count(func.distinct(Shout.id)).label("shouts_count"))
|
||||
.select_from(ShoutAuthor)
|
||||
.join(Shout, ShoutAuthor.shout == Shout.id)
|
||||
.where(
|
||||
and_(
|
||||
Shout.deleted_at.is_(None),
|
||||
Shout.published_at.is_not(None)
|
||||
)
|
||||
)
|
||||
.where(and_(Shout.deleted_at.is_(None), Shout.published_at.is_not(None)))
|
||||
.group_by(ShoutAuthor.author)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
base_query = (
|
||||
base_query
|
||||
.outerjoin(subquery, Author.id == subquery.c.author)
|
||||
.order_by(desc(func.coalesce(subquery.c.shouts_count, 0)))
|
||||
|
||||
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
|
||||
desc(func.coalesce(subquery.c.shouts_count, 0))
|
||||
)
|
||||
elif stats_sort_field == "followers":
|
||||
# Sorting by the number of followers
|
||||
from sqlalchemy import func
|
||||
|
||||
from auth.orm import AuthorFollower
|
||||
|
||||
|
||||
subquery = (
|
||||
select(
|
||||
AuthorFollower.author,
|
||||
func.count(func.distinct(AuthorFollower.follower)).label("followers_count")
|
||||
func.count(func.distinct(AuthorFollower.follower)).label("followers_count"),
|
||||
)
|
||||
.select_from(AuthorFollower)
|
||||
.group_by(AuthorFollower.author)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
base_query = (
|
||||
base_query
|
||||
.outerjoin(subquery, Author.id == subquery.c.author)
|
||||
.order_by(desc(func.coalesce(subquery.c.followers_count, 0)))
|
||||
|
||||
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
|
||||
desc(func.coalesce(subquery.c.followers_count, 0))
|
||||
)
|
||||
|
||||
# Применяем лимит и смещение
|
||||
@@ -219,7 +208,7 @@ async def get_authors_with_stats(limit=50, offset=0, by: Optional[str] = None, c
|
||||
"shouts": shouts_stats.get(author.id, 0),
|
||||
"followers": followers_stats.get(author.id, 0),
|
||||
}
|
||||
|
||||
|
||||
result.append(author_dict)
|
||||
|
||||
# Кешируем каждого автора отдельно для использования в других функциях
|
||||
@@ -299,7 +288,7 @@ async def update_author(_, info, profile):
|
||||
# Кэшируем полную версию для админов
|
||||
author_dict = author_with_stat.dict(access=is_admin)
|
||||
asyncio.create_task(cache_author(author_dict))
|
||||
|
||||
|
||||
# Возвращаем обычную полную версию, т.к. это владелец
|
||||
return {"error": None, "author": author}
|
||||
except Exception as exc:
|
||||
@@ -328,16 +317,16 @@ async def get_authors_all(_, info):
|
||||
async def get_author(_, info, slug="", author_id=0):
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
|
||||
author_dict = None
|
||||
try:
|
||||
author_id = get_author_id_from(slug=slug, user="", author_id=author_id)
|
||||
if not author_id:
|
||||
raise ValueError("cant find")
|
||||
|
||||
|
||||
# Получаем данные автора из кэша (полные данные)
|
||||
cached_author = await get_cached_author(int(author_id), get_with_stat)
|
||||
|
||||
|
||||
# Применяем фильтрацию на стороне клиента, так как в кэше хранится полная версия
|
||||
if cached_author:
|
||||
# Создаем объект автора для использования метода dict
|
||||
@@ -361,7 +350,7 @@ async def get_author(_, info, slug="", author_id=0):
|
||||
# Кэшируем полные данные для админов
|
||||
original_dict = author_with_stat.dict(access=True)
|
||||
asyncio.create_task(cache_author(original_dict))
|
||||
|
||||
|
||||
# Возвращаем отфильтрованную версию
|
||||
author_dict = author_with_stat.dict(access=is_admin)
|
||||
# Добавляем статистику
|
||||
@@ -393,11 +382,12 @@ async def load_authors_by(_, info, by, limit, offset):
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
|
||||
# Используем оптимизированную функцию для получения авторов
|
||||
return await get_authors_with_stats(limit, offset, by, viewer_id, is_admin)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
logger.error(f"{exc}:\n{traceback.format_exc()}")
|
||||
return []
|
||||
|
||||
@@ -413,7 +403,7 @@ async def load_authors_search(_, info, text: str, limit: int = 10, offset: int =
|
||||
Returns:
|
||||
list: List of authors matching the search criteria
|
||||
"""
|
||||
|
||||
|
||||
# Get author IDs from search engine (already sorted by relevance)
|
||||
search_results = await search_service.search_authors(text, limit, offset)
|
||||
|
||||
@@ -429,13 +419,13 @@ async def load_authors_search(_, info, text: str, limit: int = 10, offset: int =
|
||||
# Simple query to get authors by IDs - no need for stats here
|
||||
authors_query = select(Author).filter(Author.id.in_(author_ids))
|
||||
db_authors = session.execute(authors_query).scalars().all()
|
||||
|
||||
|
||||
if not db_authors:
|
||||
return []
|
||||
|
||||
# Create a dictionary for quick lookup
|
||||
authors_dict = {str(author.id): author for author in db_authors}
|
||||
|
||||
|
||||
# Keep the order from search results (maintains the relevance sorting)
|
||||
ordered_authors = [authors_dict[author_id] for author_id in author_ids if author_id in authors_dict]
|
||||
|
||||
@@ -468,7 +458,7 @@ async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
|
||||
logger.debug(f"getting follows for @{slug}")
|
||||
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
||||
if not author_id:
|
||||
@@ -477,7 +467,7 @@ async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||
# Получаем данные из кэша
|
||||
followed_authors_raw = await get_cached_follower_authors(author_id)
|
||||
followed_topics = await get_cached_follower_topics(author_id)
|
||||
|
||||
|
||||
# Фильтруем чувствительные данные авторов
|
||||
followed_authors = []
|
||||
for author_data in followed_authors_raw:
|
||||
@@ -517,15 +507,14 @@ async def get_author_follows_authors(_, info, slug="", user=None, author_id=None
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
|
||||
logger.debug(f"getting followed authors for @{slug}")
|
||||
if not author_id:
|
||||
return []
|
||||
|
||||
|
||||
# Получаем данные из кэша
|
||||
followed_authors_raw = await get_cached_follower_authors(author_id)
|
||||
|
||||
|
||||
# Фильтруем чувствительные данные авторов
|
||||
followed_authors = []
|
||||
for author_data in followed_authors_raw:
|
||||
@@ -540,7 +529,7 @@ async def get_author_follows_authors(_, info, slug="", user=None, author_id=None
|
||||
# is_admin - булево значение, является ли текущий пользователь админом
|
||||
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
|
||||
followed_authors.append(temp_author.dict(access=has_access))
|
||||
|
||||
|
||||
return followed_authors
|
||||
|
||||
|
||||
@@ -562,15 +551,15 @@ async def get_author_followers(_, info, slug: str = "", user: str = "", author_i
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
|
||||
logger.debug(f"getting followers for author @{slug} or ID:{author_id}")
|
||||
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
||||
if not author_id:
|
||||
return []
|
||||
|
||||
|
||||
# Получаем данные из кэша
|
||||
followers_raw = await get_cached_author_followers(author_id)
|
||||
|
||||
|
||||
# Фильтруем чувствительные данные авторов
|
||||
followers = []
|
||||
for follower_data in followers_raw:
|
||||
@@ -585,5 +574,5 @@ async def get_author_followers(_, info, slug: str = "", user: str = "", author_i
|
||||
# is_admin - булево значение, является ли текущий пользователь админом
|
||||
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
|
||||
followers.append(temp_author.dict(access=has_access))
|
||||
|
||||
|
||||
return followers
|
||||
|
@@ -72,9 +72,7 @@ def toggle_bookmark_shout(_, info, slug: str) -> CommonResult:
|
||||
|
||||
if existing_bookmark:
|
||||
db.execute(
|
||||
delete(AuthorBookmark).where(
|
||||
AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id
|
||||
)
|
||||
delete(AuthorBookmark).where(AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id)
|
||||
)
|
||||
result = False
|
||||
else:
|
||||
|
@@ -74,9 +74,9 @@ async def update_community(_, info, community_data):
|
||||
if slug:
|
||||
with local_session() as session:
|
||||
try:
|
||||
session.query(Community).where(
|
||||
Community.created_by == author_id, Community.slug == slug
|
||||
).update(community_data)
|
||||
session.query(Community).where(Community.created_by == author_id, Community.slug == slug).update(
|
||||
community_data
|
||||
)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
@@ -90,9 +90,7 @@ async def delete_community(_, info, slug: str):
|
||||
author_id = author_dict.get("id")
|
||||
with local_session() as session:
|
||||
try:
|
||||
session.query(Community).where(
|
||||
Community.slug == slug, Community.created_by == author_id
|
||||
).delete()
|
||||
session.query(Community).where(Community.slug == slug, Community.created_by == author_id).delete()
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from auth.orm import Author
|
||||
from cache.cache import (
|
||||
invalidate_shout_related_cache,
|
||||
invalidate_shouts_cache,
|
||||
)
|
||||
from auth.orm import Author
|
||||
from orm.draft import Draft, DraftAuthor, DraftTopic
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from services.auth import login_required
|
||||
@@ -449,15 +450,15 @@ async def publish_draft(_, info, draft_id: int):
|
||||
|
||||
# Добавляем темы
|
||||
for topic in draft.topics or []:
|
||||
st = ShoutTopic(
|
||||
topic=topic.id, shout=shout.id, main=topic.main if hasattr(topic, "main") else False
|
||||
)
|
||||
st = ShoutTopic(topic=topic.id, shout=shout.id, main=topic.main if hasattr(topic, "main") else False)
|
||||
session.add(st)
|
||||
|
||||
session.commit()
|
||||
|
||||
# Инвалидируем кеш
|
||||
cache_keys = [f"shouts:{shout.id}", ]
|
||||
cache_keys = [
|
||||
f"shouts:{shout.id}",
|
||||
]
|
||||
await invalidate_shouts_cache(cache_keys)
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
|
||||
@@ -482,67 +483,59 @@ async def publish_draft(_, info, draft_id: int):
|
||||
async def unpublish_draft(_, info, draft_id: int):
|
||||
"""
|
||||
Снимает с публикации черновик, обновляя связанный Shout.
|
||||
|
||||
|
||||
Args:
|
||||
draft_id (int): ID черновика, публикацию которого нужно снять
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Результат операции с информацией о черновике или сообщением об ошибке
|
||||
"""
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
|
||||
if author_id:
|
||||
return {"error": "Author ID is required"}
|
||||
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Загружаем черновик со связанной публикацией
|
||||
draft = (
|
||||
session.query(Draft)
|
||||
.options(
|
||||
joinedload(Draft.publication),
|
||||
joinedload(Draft.authors),
|
||||
joinedload(Draft.topics)
|
||||
)
|
||||
.options(joinedload(Draft.publication), joinedload(Draft.authors), joinedload(Draft.topics))
|
||||
.filter(Draft.id == draft_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
if not draft:
|
||||
return {"error": "Draft not found"}
|
||||
|
||||
|
||||
# Проверяем, есть ли публикация
|
||||
if not draft.publication:
|
||||
return {"error": "This draft is not published yet"}
|
||||
|
||||
|
||||
shout = draft.publication
|
||||
|
||||
|
||||
# Снимаем с публикации
|
||||
shout.published_at = None
|
||||
shout.updated_at = int(time.time())
|
||||
shout.updated_by = author_id
|
||||
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
# Инвалидируем кэш
|
||||
cache_keys = [f"shouts:{shout.id}"]
|
||||
await invalidate_shouts_cache(cache_keys)
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
|
||||
|
||||
# Формируем результат
|
||||
draft_dict = draft.dict()
|
||||
# Добавляем информацию о публикации
|
||||
draft_dict["publication"] = {
|
||||
"id": shout.id,
|
||||
"slug": shout.slug,
|
||||
"published_at": None
|
||||
}
|
||||
|
||||
draft_dict["publication"] = {"id": shout.id, "slug": shout.slug, "published_at": None}
|
||||
|
||||
logger.info(f"Successfully unpublished shout #{shout.id} for draft #{draft_id}")
|
||||
|
||||
|
||||
return {"draft": draft_dict}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to unpublish draft {draft_id}: {e}", exc_info=True)
|
||||
return {"error": f"Failed to unpublish draft: {str(e)}"}
|
||||
|
@@ -5,13 +5,13 @@ from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
from sqlalchemy.sql.functions import coalesce
|
||||
|
||||
from auth.orm import Author
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cache_topic,
|
||||
invalidate_shout_related_cache,
|
||||
invalidate_shouts_cache,
|
||||
)
|
||||
from auth.orm import Author
|
||||
from orm.draft import Draft
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic
|
||||
@@ -179,9 +179,7 @@ async def create_shout(_, info, inp):
|
||||
lead = inp.get("lead", "")
|
||||
body_text = extract_text(body)
|
||||
lead_text = extract_text(lead)
|
||||
seo = inp.get(
|
||||
"seo", lead_text.strip() or body_text.strip()[:300].split(". ")[:-1].join(". ")
|
||||
)
|
||||
seo = inp.get("seo", lead_text.strip() or body_text.strip()[:300].split(". ")[:-1].join(". "))
|
||||
new_shout = Shout(
|
||||
slug=slug,
|
||||
body=body,
|
||||
@@ -278,9 +276,7 @@ def patch_main_topic(session, main_topic_slug, shout):
|
||||
with session.begin():
|
||||
# Получаем текущий главный топик
|
||||
old_main = (
|
||||
session.query(ShoutTopic)
|
||||
.filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.main.is_(True)))
|
||||
.first()
|
||||
session.query(ShoutTopic).filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.main.is_(True))).first()
|
||||
)
|
||||
if old_main:
|
||||
logger.info(f"Found current main topic: {old_main.topic.slug}")
|
||||
@@ -314,9 +310,7 @@ def patch_main_topic(session, main_topic_slug, shout):
|
||||
session.flush()
|
||||
logger.info(f"Main topic updated for shout#{shout.id}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"No changes needed for main topic (old={old_main is not None}, new={new_main is not None})"
|
||||
)
|
||||
logger.warning(f"No changes needed for main topic (old={old_main is not None}, new={new_main is not None})")
|
||||
|
||||
|
||||
def patch_topics(session, shout, topics_input):
|
||||
@@ -410,9 +404,7 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
||||
logger.info(f"Processing update for shout#{shout_id} by author #{author_id}")
|
||||
shout_by_id = (
|
||||
session.query(Shout)
|
||||
.options(
|
||||
joinedload(Shout.topics).joinedload(ShoutTopic.topic), joinedload(Shout.authors)
|
||||
)
|
||||
.options(joinedload(Shout.topics).joinedload(ShoutTopic.topic), joinedload(Shout.authors))
|
||||
.filter(Shout.id == shout_id)
|
||||
.first()
|
||||
)
|
||||
@@ -441,10 +433,7 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
||||
shout_input["slug"] = slug
|
||||
logger.info(f"shout#{shout_id} slug patched")
|
||||
|
||||
if (
|
||||
filter(lambda x: x.id == author_id, [x for x in shout_by_id.authors])
|
||||
or "editor" in roles
|
||||
):
|
||||
if filter(lambda x: x.id == author_id, [x for x in shout_by_id.authors]) or "editor" in roles:
|
||||
logger.info(f"Author #{author_id} has permission to edit shout#{shout_id}")
|
||||
|
||||
# topics patch
|
||||
@@ -558,9 +547,7 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
||||
# Получаем полные данные шаута со связями
|
||||
shout_with_relations = (
|
||||
session.query(Shout)
|
||||
.options(
|
||||
joinedload(Shout.topics).joinedload(ShoutTopic.topic), joinedload(Shout.authors)
|
||||
)
|
||||
.options(joinedload(Shout.topics).joinedload(ShoutTopic.topic), joinedload(Shout.authors))
|
||||
.filter(Shout.id == shout_id)
|
||||
.first()
|
||||
)
|
||||
|
@@ -71,9 +71,7 @@ def shouts_by_follower(info, follower_id: int, options):
|
||||
q = query_with_stat(info)
|
||||
reader_followed_authors = select(AuthorFollower.author).where(AuthorFollower.follower == follower_id)
|
||||
reader_followed_topics = select(TopicFollower.topic).where(TopicFollower.follower == follower_id)
|
||||
reader_followed_shouts = select(ShoutReactionsFollower.shout).where(
|
||||
ShoutReactionsFollower.follower == follower_id
|
||||
)
|
||||
reader_followed_shouts = select(ShoutReactionsFollower.shout).where(ShoutReactionsFollower.follower == follower_id)
|
||||
followed_subquery = (
|
||||
select(Shout.id)
|
||||
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
||||
@@ -142,9 +140,7 @@ async def load_shouts_authored_by(_, info, slug: str, options) -> List[Shout]:
|
||||
q = (
|
||||
query_with_stat(info)
|
||||
if has_field(info, "stat")
|
||||
else select(Shout).filter(
|
||||
and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
)
|
||||
else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.filter(Shout.authors.any(id=author_id))
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
@@ -173,9 +169,7 @@ async def load_shouts_with_topic(_, info, slug: str, options) -> List[Shout]:
|
||||
q = (
|
||||
query_with_stat(info)
|
||||
if has_field(info, "stat")
|
||||
else select(Shout).filter(
|
||||
and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
)
|
||||
else select(Shout).filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.filter(Shout.topics.any(id=topic_id))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
|
@@ -4,13 +4,13 @@ from graphql import GraphQLError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.sql import and_
|
||||
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cache_topic,
|
||||
get_cached_follower_authors,
|
||||
get_cached_follower_topics,
|
||||
)
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from orm.community import Community, CommunityFollower
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout, ShoutReactionsFollower
|
||||
@@ -65,14 +65,14 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||
return {"error": f"{what.lower()} not found"}
|
||||
if not entity_id and entity:
|
||||
entity_id = entity.id
|
||||
|
||||
|
||||
# Если это автор, учитываем фильтрацию данных
|
||||
if what == "AUTHOR":
|
||||
# Полная версия для кэширования
|
||||
entity_dict = entity.dict(is_admin=True)
|
||||
else:
|
||||
entity_dict = entity.dict()
|
||||
|
||||
entity_dict = entity.dict()
|
||||
|
||||
logger.debug(f"entity_id: {entity_id}, entity_dict: {entity_dict}")
|
||||
|
||||
if entity_id:
|
||||
@@ -87,9 +87,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||
.first()
|
||||
)
|
||||
if existing_sub:
|
||||
logger.info(
|
||||
f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}"
|
||||
)
|
||||
logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}")
|
||||
else:
|
||||
logger.debug("Добавление новой записи в базу данных")
|
||||
sub = follower_class(follower=follower_id, **{entity_type: entity_id})
|
||||
@@ -105,12 +103,12 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||
if get_cached_follows_method:
|
||||
logger.debug("Получение подписок из кэша")
|
||||
existing_follows = await get_cached_follows_method(follower_id)
|
||||
|
||||
|
||||
# Если это авторы, получаем безопасную версию
|
||||
if what == "AUTHOR":
|
||||
# Получаем ID текущего пользователя и фильтруем данные
|
||||
follows_filtered = []
|
||||
|
||||
|
||||
for author_data in existing_follows:
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
@@ -119,7 +117,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
follows_filtered.append(temp_author.dict(viewer_id, False))
|
||||
|
||||
|
||||
if not existing_sub:
|
||||
# Создаем объект автора для entity_dict
|
||||
temp_author = Author()
|
||||
@@ -132,7 +130,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||
follows = follows_filtered
|
||||
else:
|
||||
follows = [*existing_follows, entity_dict] if not existing_sub else existing_follows
|
||||
|
||||
|
||||
logger.debug("Обновлен список подписок")
|
||||
|
||||
if what == "AUTHOR" and not existing_sub:
|
||||
@@ -214,20 +212,20 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
||||
await cache_method(entity.dict(is_admin=True))
|
||||
else:
|
||||
await cache_method(entity.dict())
|
||||
|
||||
|
||||
if get_cached_follows_method:
|
||||
logger.debug("Получение подписок из кэша")
|
||||
existing_follows = await get_cached_follows_method(follower_id)
|
||||
|
||||
|
||||
# Если это авторы, получаем безопасную версию
|
||||
if what == "AUTHOR":
|
||||
# Получаем ID текущего пользователя и фильтруем данные
|
||||
follows_filtered = []
|
||||
|
||||
|
||||
for author_data in existing_follows:
|
||||
if author_data["id"] == entity_id:
|
||||
continue
|
||||
|
||||
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in author_data.items():
|
||||
@@ -235,11 +233,11 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
follows_filtered.append(temp_author.dict(viewer_id, False))
|
||||
|
||||
|
||||
follows = follows_filtered
|
||||
else:
|
||||
follows = [item for item in existing_follows if item["id"] != entity_id]
|
||||
|
||||
|
||||
logger.debug("Обновлен список подписок")
|
||||
|
||||
if what == "AUTHOR":
|
||||
|
@@ -66,9 +66,7 @@ def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[
|
||||
return total, unread, notifications
|
||||
|
||||
|
||||
def group_notification(
|
||||
thread, authors=None, shout=None, reactions=None, entity="follower", action="follow"
|
||||
):
|
||||
def group_notification(thread, authors=None, shout=None, reactions=None, entity="follower", action="follow"):
|
||||
reactions = reactions or []
|
||||
authors = authors or []
|
||||
return {
|
||||
|
@@ -14,11 +14,7 @@ def handle_proposing(kind: ReactionKind, reply_to: int, shout_id: int):
|
||||
session.query(Reaction).filter(Reaction.id == reply_to, Reaction.shout == shout_id).first()
|
||||
)
|
||||
|
||||
if (
|
||||
replied_reaction
|
||||
and replied_reaction.kind is ReactionKind.PROPOSE.value
|
||||
and replied_reaction.quote
|
||||
):
|
||||
if replied_reaction and replied_reaction.kind is ReactionKind.PROPOSE.value and replied_reaction.quote:
|
||||
# patch all the proposals' quotes
|
||||
proposals = (
|
||||
session.query(Reaction)
|
||||
|
@@ -186,9 +186,7 @@ def count_author_shouts_rating(session, author_id) -> int:
|
||||
|
||||
def get_author_rating_old(session, author: Author):
|
||||
likes_count = (
|
||||
session.query(AuthorRating)
|
||||
.filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True)))
|
||||
.count()
|
||||
session.query(AuthorRating).filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True))).count()
|
||||
)
|
||||
dislikes_count = (
|
||||
session.query(AuthorRating)
|
||||
|
@@ -334,9 +334,7 @@ async def create_reaction(_, info, reaction):
|
||||
with local_session() as session:
|
||||
authors = session.query(ShoutAuthor.author).filter(ShoutAuthor.shout == shout_id).scalar()
|
||||
is_author = (
|
||||
bool(list(filter(lambda x: x == int(author_id), authors)))
|
||||
if isinstance(authors, list)
|
||||
else False
|
||||
bool(list(filter(lambda x: x == int(author_id), authors))) if isinstance(authors, list) else False
|
||||
)
|
||||
reaction_input["created_by"] = author_id
|
||||
kind = reaction_input.get("kind")
|
||||
|
@@ -138,9 +138,7 @@ def query_with_stat(info):
|
||||
select(
|
||||
ShoutTopic.shout,
|
||||
json_array_builder(
|
||||
json_builder(
|
||||
"id", Topic.id, "title", Topic.title, "slug", Topic.slug, "is_main", ShoutTopic.main
|
||||
)
|
||||
json_builder("id", Topic.id, "title", Topic.title, "slug", Topic.slug, "is_main", ShoutTopic.main)
|
||||
).label("topics"),
|
||||
)
|
||||
.outerjoin(Topic, ShoutTopic.topic == Topic.id)
|
||||
@@ -227,7 +225,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||
"slug": a.slug,
|
||||
"pic": a.pic,
|
||||
}
|
||||
|
||||
|
||||
# Обработка поля updated_by
|
||||
if has_field(info, "updated_by"):
|
||||
if shout_dict.get("updated_by"):
|
||||
@@ -246,7 +244,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||
else:
|
||||
# Если updated_by не указан, устанавливаем поле в null
|
||||
shout_dict["updated_by"] = None
|
||||
|
||||
|
||||
# Обработка поля deleted_by
|
||||
if has_field(info, "deleted_by"):
|
||||
if shout_dict.get("deleted_by"):
|
||||
@@ -287,9 +285,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||
if hasattr(row, "main_topic"):
|
||||
# logger.debug(f"Raw main_topic for shout#{shout_id}: {row.main_topic}")
|
||||
main_topic = (
|
||||
orjson.loads(row.main_topic)
|
||||
if isinstance(row.main_topic, str)
|
||||
else row.main_topic
|
||||
orjson.loads(row.main_topic) if isinstance(row.main_topic, str) else row.main_topic
|
||||
)
|
||||
# logger.debug(f"Parsed main_topic for shout#{shout_id}: {main_topic}")
|
||||
|
||||
@@ -325,9 +321,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||
media_data = orjson.loads(media_data)
|
||||
except orjson.JSONDecodeError:
|
||||
media_data = []
|
||||
shout_dict["media"] = (
|
||||
[media_data] if isinstance(media_data, dict) else media_data
|
||||
)
|
||||
shout_dict["media"] = [media_data] if isinstance(media_data, dict) else media_data
|
||||
|
||||
shouts.append(shout_dict)
|
||||
|
||||
@@ -415,9 +409,7 @@ def apply_sorting(q, options):
|
||||
"""
|
||||
order_str = options.get("order_by")
|
||||
if order_str in ["rating", "comments_count", "last_commented_at"]:
|
||||
query_order_by = (
|
||||
desc(text(order_str)) if options.get("order_by_desc", True) else asc(text(order_str))
|
||||
)
|
||||
query_order_by = desc(text(order_str)) if options.get("order_by_desc", True) else asc(text(order_str))
|
||||
q = q.distinct(text(order_str), Shout.id).order_by( # DISTINCT ON включает поле сортировки
|
||||
nulls_last(query_order_by), Shout.id
|
||||
)
|
||||
@@ -513,15 +505,11 @@ async def load_shouts_unrated(_, info, options):
|
||||
q = select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
q = q.join(Author, Author.id == Shout.created_by)
|
||||
q = q.add_columns(
|
||||
json_builder("id", Author.id, "name", Author.name, "slug", Author.slug, "pic", Author.pic).label(
|
||||
"main_author"
|
||||
)
|
||||
json_builder("id", Author.id, "name", Author.name, "slug", Author.slug, "pic", Author.pic).label("main_author")
|
||||
)
|
||||
q = q.join(ShoutTopic, and_(ShoutTopic.shout == Shout.id, ShoutTopic.main.is_(True)))
|
||||
q = q.join(Topic, Topic.id == ShoutTopic.topic)
|
||||
q = q.add_columns(
|
||||
json_builder("id", Topic.id, "title", Topic.title, "slug", Topic.slug).label("main_topic")
|
||||
)
|
||||
q = q.add_columns(json_builder("id", Topic.id, "title", Topic.title, "slug", Topic.slug).label("main_topic"))
|
||||
q = q.where(Shout.id.not_in(rated_shouts))
|
||||
q = q.order_by(func.random())
|
||||
|
||||
|
@@ -3,8 +3,8 @@ import asyncio
|
||||
from sqlalchemy import and_, distinct, func, join, select
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from cache.cache import cache_author
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from cache.cache import cache_author
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic, TopicFollower
|
||||
@@ -177,9 +177,7 @@ def get_topic_comments_stat(topic_id: int) -> int:
|
||||
.subquery()
|
||||
)
|
||||
# Запрос для суммирования количества комментариев по теме
|
||||
q = select(func.coalesce(func.sum(sub_comments.c.comments_count), 0)).filter(
|
||||
ShoutTopic.topic == topic_id
|
||||
)
|
||||
q = select(func.coalesce(func.sum(sub_comments.c.comments_count), 0)).filter(ShoutTopic.topic == topic_id)
|
||||
q = q.outerjoin(sub_comments, ShoutTopic.shout == sub_comments.c.shout_id)
|
||||
with local_session() as session:
|
||||
result = session.execute(q).first()
|
||||
@@ -239,9 +237,7 @@ def get_author_followers_stat(author_id: int) -> int:
|
||||
:return: Количество уникальных подписчиков автора.
|
||||
"""
|
||||
aliased_followers = aliased(AuthorFollower)
|
||||
q = select(func.count(distinct(aliased_followers.follower))).filter(
|
||||
aliased_followers.author == author_id
|
||||
)
|
||||
q = select(func.count(distinct(aliased_followers.follower))).filter(aliased_followers.author == author_id)
|
||||
with local_session() as session:
|
||||
result = session.execute(q).first()
|
||||
return result[0] if result else 0
|
||||
@@ -293,9 +289,7 @@ def get_with_stat(q):
|
||||
stat["shouts"] = cols[1] # Статистика по публикациям
|
||||
stat["followers"] = cols[2] # Статистика по подписчикам
|
||||
if is_author:
|
||||
stat["authors"] = get_author_authors_stat(
|
||||
entity.id
|
||||
) # Статистика по подпискам на авторов
|
||||
stat["authors"] = get_author_authors_stat(entity.id) # Статистика по подпискам на авторов
|
||||
stat["comments"] = get_author_comments_stat(entity.id) # Статистика по комментариям
|
||||
else:
|
||||
stat["authors"] = get_topic_authors_stat(entity.id) # Статистика по авторам темы
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy import desc, select, text
|
||||
|
||||
from auth.orm import Author
|
||||
from cache.cache import (
|
||||
cache_topic,
|
||||
cached_query,
|
||||
@@ -8,9 +9,8 @@ from cache.cache import (
|
||||
get_cached_topic_followers,
|
||||
invalidate_cache_by_prefix,
|
||||
)
|
||||
from auth.orm import Author
|
||||
from orm.topic import Topic
|
||||
from orm.reaction import ReactionKind
|
||||
from orm.topic import Topic
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
|
Reference in New Issue
Block a user