0.7.1-fix
All checks were successful
Deploy on push / deploy (push) Successful in 9s

This commit is contained in:
2025-07-02 22:49:20 +03:00
parent 82111ed0f6
commit 27c5a57709
7 changed files with 232 additions and 125 deletions

View File

@@ -14,7 +14,6 @@ from orm.invite import Invite, InviteStatus
from orm.shout import Shout
from services.db import local_session
from services.env import EnvManager, EnvVariable
from services.rbac import admin_only
from services.schema import mutation, query
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from utils.logger import root_logger as logger
@@ -42,6 +41,99 @@ default_role_descriptions = {
}
# === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ DRY ===
def normalize_pagination(limit: int = 20, offset: int = 0) -> tuple[int, int]:
"""
Нормализует параметры пагинации.
Args:
limit: Максимальное количество записей
offset: Смещение
Returns:
Кортеж (limit, offset) с нормализованными значениями
"""
return max(1, min(100, limit or 20)), max(0, offset or 0)
def calculate_pagination_info(total_count: int, limit: int, offset: int) -> dict[str, int]:
"""
Вычисляет информацию о пагинации.
Args:
total_count: Общее количество записей
limit: Количество записей на странице
offset: Смещение
Returns:
Словарь с информацией о пагинации
"""
per_page = limit
if total_count is None or per_page in (None, 0):
total_pages = 1
else:
total_pages = ceil(total_count / per_page)
current_page = (offset // per_page) + 1 if per_page > 0 else 1
return {
"total": total_count,
"page": current_page,
"perPage": per_page,
"totalPages": total_pages,
}
def handle_admin_error(operation: str, error: Exception) -> GraphQLError:
"""
Обрабатывает ошибки в админ-резолверах.
Args:
operation: Название операции
error: Исключение
Returns:
GraphQLError для возврата клиенту
"""
import traceback
logger.error(f"Ошибка при {operation}: {error!s}")
logger.error(traceback.format_exc())
msg = f"Не удалось {operation}: {error!s}"
return GraphQLError(msg)
def get_author_info(author_id: int, session) -> dict[str, Any]:
"""
Получает информацию об авторе для отображения в админ-панели.
Args:
author_id: ID автора
session: Сессия БД
Returns:
Словарь с информацией об авторе
"""
if not author_id:
return None
author = session.query(Author).filter(Author.id == author_id).first()
if author:
return {
"id": author.id,
"email": author.email,
"name": author.name,
"slug": author.slug or f"user-{author.id}",
}
return {
"id": author_id,
"email": "unknown",
"name": "unknown",
"slug": f"user-{author_id}",
}
def _get_user_roles(user: Author, community_id: int = 1) -> list[str]:
"""
Получает полный список ролей пользователя в указанном сообществе, включая
@@ -86,7 +178,7 @@ async def admin_get_users(
Получает список пользователей для админ-панели с поддержкой пагинации и поиска
Args:
info: Контекст GraphQL запроса
_info: Контекст GraphQL запроса
limit: Максимальное количество записей для получения
offset: Смещение в списке результатов
search: Строка поиска (по email, имени или ID)
@@ -95,9 +187,8 @@ async def admin_get_users(
Пагинированный список пользователей
"""
try:
# Нормализуем параметры
limit = max(1, min(100, limit or 20)) # Ограничиваем количество записей от 1 до 100
offset = max(0, offset or 0) # Смещение не может быть отрицательным
# Нормализуем параметры пагинации
limit, offset = normalize_pagination(limit, offset)
with local_session() as session:
# Базовый запрос
@@ -117,17 +208,12 @@ async def admin_get_users(
# Получаем общее количество записей
total_count = query.count()
# Вычисляем информацию о пагинации
per_page = limit
if total_count is None or per_page in (None, 0):
total_pages = 1
else:
total_pages = ceil(total_count / per_page)
current_page = (offset // per_page) + 1 if per_page > 0 else 1
# Применяем пагинацию
authors = query.order_by(Author.id).offset(offset).limit(limit).all()
# Вычисляем информацию о пагинации
pagination_info = calculate_pagination_info(total_count, limit, offset)
# Преобразуем в формат для API
return {
"authors": [
@@ -142,19 +228,11 @@ async def admin_get_users(
}
for user in authors
],
"total": total_count,
"page": current_page,
"perPage": per_page,
"totalPages": total_pages,
**pagination_info,
}
except Exception as e:
import traceback
logger.error(f"Ошибка при получении списка пользователей: {e!s}")
logger.error(traceback.format_exc())
msg = f"Не удалось получить список пользователей: {e!s}"
raise GraphQLError(msg) from e
raise handle_admin_error("получении списка пользователей", e) from e
@query.field("adminGetRoles")
@@ -224,7 +302,7 @@ async def admin_get_roles(_: None, info: GraphQLResolveInfo, community: int = No
@query.field("getEnvVariables")
@admin_only
@admin_auth_required
async def get_env_variables(_: None, info: GraphQLResolveInfo) -> list[dict[str, Any]]:
"""
Получает список переменных окружения, сгруппированных по секциям
@@ -908,9 +986,8 @@ async def admin_get_invites(
Пагинированный список приглашений
"""
try:
# Нормализуем параметры
limit = max(1, min(100, limit or 10))
offset = max(0, offset or 0)
# Нормализуем параметры пагинации
limit, offset = normalize_pagination(limit, offset)
with local_session() as session:
# Базовый запрос с загрузкой связанных объектов
@@ -957,26 +1034,19 @@ async def admin_get_invites(
# Получаем общее количество записей
total_count = query.count()
# Вычисляем информацию о пагинации
per_page = limit
if total_count is None or per_page in (None, 0):
total_pages = 1
else:
total_pages = ceil(total_count / per_page)
current_page = (offset // per_page) + 1 if per_page > 0 else 1
# Применяем пагинацию и сортировку (по ID приглашающего, затем автора, затем публикации)
invites = (
query.order_by(Invite.inviter_id, Invite.author_id, Invite.shout_id).offset(offset).limit(limit).all()
)
# Вычисляем информацию о пагинации
pagination_info = calculate_pagination_info(total_count, limit, offset)
# Преобразуем в формат для API
result_invites = []
for invite in invites:
# Получаем автора публикации
created_by_author = None
if invite.shout and invite.shout.created_by:
created_by_author = session.query(Author).filter(Author.id == invite.shout.created_by).first()
# Получаем информацию о создателе публикации
created_by_info = get_author_info(invite.shout.created_by if invite.shout else None, session)
invite_dict = {
"inviter_id": invite.inviter_id,
@@ -987,86 +1057,32 @@ async def admin_get_invites(
"id": invite.inviter.id,
"name": invite.inviter.name or "Без имени",
"email": invite.inviter.email,
"slug": invite.inviter.slug or f"user-{invite.inviter.id}", # Добавляем значение по умолчанию
"slug": invite.inviter.slug or f"user-{invite.inviter.id}",
},
"author": {
"id": invite.author.id,
"name": invite.author.name or "Без имени",
"email": invite.author.email,
"slug": invite.author.slug or f"user-{invite.author.id}", # Добавляем значение по умолчанию
"slug": invite.author.slug or f"user-{invite.author.id}",
},
"shout": {
"id": invite.shout.id,
"title": invite.shout.title,
"slug": invite.shout.slug,
"created_by": created_by_info,
},
"created_at": None, # У приглашений нет created_at поля в текущей модели
}
# Добавляем информацию о создателе публикации, если она доступна
if created_by_author:
# Создаем новый словарь для shout
shout_dict = {}
# Копируем основные поля
if isinstance(invite_dict["shout"], dict):
shout_info = invite_dict["shout"]
shout_dict["id"] = shout_info.get("id")
shout_dict["title"] = shout_info.get("title")
shout_dict["slug"] = shout_info.get("slug")
else:
# Если это не словарь, берем данные напрямую из объекта invite.shout
shout_dict["id"] = invite.shout.id
shout_dict["title"] = invite.shout.title
shout_dict["slug"] = invite.shout.slug
# Добавляем информацию о создателе
shout_dict["created_by"] = {
"id": created_by_author.id,
"name": created_by_author.name or "Без имени",
"email": created_by_author.email,
"slug": created_by_author.slug or f"user-{created_by_author.id}",
}
invite_dict["shout"] = shout_dict
else:
# Создаем новый словарь для shout
shout_dict = {}
# Копируем основные поля
if isinstance(invite_dict["shout"], dict):
shout_info = invite_dict["shout"]
shout_dict["id"] = shout_info.get("id")
shout_dict["title"] = shout_info.get("title")
shout_dict["slug"] = shout_info.get("slug")
else:
# Если это не словарь, берем данные напрямую из объекта invite.shout
shout_dict["id"] = invite.shout.id
shout_dict["title"] = invite.shout.title
shout_dict["slug"] = invite.shout.slug
# Указываем, что created_by отсутствует
shout_dict["created_by"] = None
invite_dict["shout"] = shout_dict
result_invites.append(invite_dict)
return {
"invites": result_invites,
"total": total_count,
"page": current_page,
"perPage": per_page,
"totalPages": total_pages,
**pagination_info,
}
except Exception as e:
import traceback
logger.error(f"Ошибка при получении списка приглашений: {e!s}")
logger.error(traceback.format_exc())
msg = f"Не удалось получить список приглашений: {e!s}"
raise GraphQLError(msg) from e
raise handle_admin_error("получении списка приглашений", e) from e
@mutation.field("adminUpdateInvite")