0.5.8-panel-upgrade-community-crud-fix
All checks were successful
Deploy on push / deploy (push) Successful in 6s
All checks were successful
Deploy on push / deploy (push) Successful in 6s
This commit is contained in:
@@ -62,11 +62,11 @@ async def admin_get_users(
|
||||
current_page = (offset // per_page) + 1 if per_page > 0 else 1
|
||||
|
||||
# Применяем пагинацию
|
||||
users = query.order_by(Author.id).offset(offset).limit(limit).all()
|
||||
authors = query.order_by(Author.id).offset(offset).limit(limit).all()
|
||||
|
||||
# Преобразуем в формат для API
|
||||
return {
|
||||
"users": [
|
||||
"authors": [
|
||||
{
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
@@ -76,7 +76,7 @@ async def admin_get_users(
|
||||
"created_at": user.created_at,
|
||||
"last_seen": user.last_seen,
|
||||
}
|
||||
for user in users
|
||||
for user in authors
|
||||
],
|
||||
"total": total_count,
|
||||
"page": current_page,
|
||||
@@ -247,11 +247,11 @@ async def update_env_variables(_: None, info: GraphQLResolveInfo, variables: lis
|
||||
@admin_auth_required
|
||||
async def admin_update_user(_: None, info: GraphQLResolveInfo, user: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Обновляет роли пользователя
|
||||
Обновляет данные пользователя (роли, email, имя, slug)
|
||||
|
||||
Args:
|
||||
info: Контекст GraphQL запроса
|
||||
user: Данные для обновления пользователя (содержит id и roles)
|
||||
user: Данные для обновления пользователя
|
||||
|
||||
Returns:
|
||||
Boolean: результат операции или объект с ошибкой
|
||||
@@ -259,6 +259,9 @@ async def admin_update_user(_: None, info: GraphQLResolveInfo, user: dict[str, A
|
||||
try:
|
||||
user_id = user.get("id")
|
||||
roles = user.get("roles", [])
|
||||
email = user.get("email")
|
||||
name = user.get("name")
|
||||
slug = user.get("slug")
|
||||
|
||||
if not roles:
|
||||
logger.warning(f"Пользователю {user_id} не назначено ни одной роли. Доступ в систему будет заблокирован.")
|
||||
@@ -272,6 +275,28 @@ async def admin_update_user(_: None, info: GraphQLResolveInfo, user: dict[str, A
|
||||
logger.error(error_msg)
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
# Обновляем основные поля профиля
|
||||
profile_updated = False
|
||||
if email is not None and email != author.email:
|
||||
# Проверяем уникальность email
|
||||
existing_author = session.query(Author).filter(Author.email == email, Author.id != user_id).first()
|
||||
if existing_author:
|
||||
return {"success": False, "error": f"Email {email} уже используется другим пользователем"}
|
||||
author.email = email
|
||||
profile_updated = True
|
||||
|
||||
if name is not None and name != author.name:
|
||||
author.name = name
|
||||
profile_updated = True
|
||||
|
||||
if slug is not None and slug != author.slug:
|
||||
# Проверяем уникальность slug
|
||||
existing_author = session.query(Author).filter(Author.slug == slug, Author.id != user_id).first()
|
||||
if existing_author:
|
||||
return {"success": False, "error": f"Slug {slug} уже используется другим пользователем"}
|
||||
author.slug = slug
|
||||
profile_updated = True
|
||||
|
||||
# Получаем ID сообщества по умолчанию
|
||||
default_community_id = 1 # Используем значение по умолчанию из модели AuthorRole
|
||||
|
||||
@@ -307,19 +332,25 @@ async def admin_update_user(_: None, info: GraphQLResolveInfo, user: dict[str, A
|
||||
f"Пользователю {author.email or author.id} не назначена роль 'reader'. Доступ в систему будет ограничен."
|
||||
)
|
||||
|
||||
logger.info(f"Роли пользователя {author.email or author.id} обновлены: {', '.join(found_role_ids)}")
|
||||
update_details = []
|
||||
if profile_updated:
|
||||
update_details.append("профиль")
|
||||
if roles:
|
||||
update_details.append(f"роли: {', '.join(found_role_ids)}")
|
||||
|
||||
logger.info(f"Данные пользователя {author.email or author.id} обновлены: {', '.join(update_details)}")
|
||||
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
# Обработка вложенных исключений
|
||||
session.rollback()
|
||||
error_msg = f"Ошибка при изменении ролей: {e!s}"
|
||||
error_msg = f"Ошибка при изменении данных пользователя: {e!s}"
|
||||
logger.error(error_msg)
|
||||
return {"success": False, "error": error_msg}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_msg = f"Ошибка при обновлении ролей пользователя: {e!s}"
|
||||
error_msg = f"Ошибка при обновлении данных пользователя: {e!s}"
|
||||
logger.error(error_msg)
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "error": error_msg}
|
||||
|
@@ -2,15 +2,49 @@ from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
|
||||
from auth.decorators import editor_or_admin_required
|
||||
from auth.orm import Author
|
||||
from orm.community import Community, CommunityFollower
|
||||
from services.db import local_session
|
||||
from services.schema import mutation, query
|
||||
from services.schema import mutation, query, type_community
|
||||
|
||||
|
||||
@query.field("get_communities_all")
|
||||
async def get_communities_all(_: None, _info: GraphQLResolveInfo) -> list[Community]:
|
||||
return local_session().query(Community).all()
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
with local_session() as session:
|
||||
# Загружаем сообщества с проверкой существования авторов
|
||||
communities = (
|
||||
session.query(Community)
|
||||
.options(joinedload(Community.created_by_author))
|
||||
.join(
|
||||
Author,
|
||||
Community.created_by == Author.id, # INNER JOIN - исключает сообщества без авторов
|
||||
)
|
||||
.filter(
|
||||
Community.created_by.isnot(None), # Дополнительная проверка
|
||||
Author.id.isnot(None), # Проверяем что автор существует
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Дополнительная проверка валидности данных
|
||||
valid_communities = []
|
||||
for community in communities:
|
||||
if (
|
||||
community.created_by
|
||||
and hasattr(community, "created_by_author")
|
||||
and community.created_by_author
|
||||
and community.created_by_author.id
|
||||
):
|
||||
valid_communities.append(community)
|
||||
else:
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
logger.warning(f"Исключено сообщество {community.id} ({community.slug}) - проблемы с автором")
|
||||
|
||||
return valid_communities
|
||||
|
||||
|
||||
@query.field("get_community")
|
||||
@@ -63,41 +97,192 @@ async def leave_community(_: None, info: GraphQLResolveInfo, slug: str) -> dict[
|
||||
|
||||
|
||||
@mutation.field("create_community")
|
||||
async def create_community(_: None, info: GraphQLResolveInfo, community_data: dict[str, Any]) -> dict[str, Any]:
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
with local_session() as session:
|
||||
session.add(Community(author=author_id, **community_data))
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
@editor_or_admin_required
|
||||
async def create_community(_: None, info: GraphQLResolveInfo, community_input: dict[str, Any]) -> dict[str, Any]:
|
||||
# Получаем author_id из контекста через декоратор авторизации
|
||||
request = info.context.get("request")
|
||||
author_id = None
|
||||
|
||||
if hasattr(request, "auth") and request.auth and hasattr(request.auth, "author_id"):
|
||||
author_id = request.auth.author_id
|
||||
elif hasattr(request, "scope") and "auth" in request.scope:
|
||||
auth_info = request.scope.get("auth", {})
|
||||
if isinstance(auth_info, dict):
|
||||
author_id = auth_info.get("author_id")
|
||||
elif hasattr(auth_info, "author_id"):
|
||||
author_id = auth_info.author_id
|
||||
|
||||
if not author_id:
|
||||
return {"error": "Не удалось определить автора"}
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Исключаем created_by из входных данных - он всегда из токена
|
||||
filtered_input = {k: v for k, v in community_input.items() if k != "created_by"}
|
||||
|
||||
# Создаем новое сообщество с обязательным created_by из токена
|
||||
new_community = Community(created_by=author_id, **filtered_input)
|
||||
session.add(new_community)
|
||||
session.commit()
|
||||
return {"error": None}
|
||||
except Exception as e:
|
||||
return {"error": f"Ошибка создания сообщества: {e!s}"}
|
||||
|
||||
|
||||
@mutation.field("update_community")
|
||||
async def update_community(_: None, info: GraphQLResolveInfo, community_data: dict[str, Any]) -> dict[str, Any]:
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
slug = community_data.get("slug")
|
||||
if slug:
|
||||
@editor_or_admin_required
|
||||
async def update_community(_: None, info: GraphQLResolveInfo, community_input: dict[str, Any]) -> dict[str, Any]:
|
||||
# Получаем author_id из контекста через декоратор авторизации
|
||||
request = info.context.get("request")
|
||||
author_id = None
|
||||
|
||||
if hasattr(request, "auth") and request.auth and hasattr(request.auth, "author_id"):
|
||||
author_id = request.auth.author_id
|
||||
elif hasattr(request, "scope") and "auth" in request.scope:
|
||||
auth_info = request.scope.get("auth", {})
|
||||
if isinstance(auth_info, dict):
|
||||
author_id = auth_info.get("author_id")
|
||||
elif hasattr(auth_info, "author_id"):
|
||||
author_id = auth_info.author_id
|
||||
|
||||
if not author_id:
|
||||
return {"error": "Не удалось определить автора"}
|
||||
|
||||
slug = community_input.get("slug")
|
||||
if not slug:
|
||||
return {"error": "Не указан slug сообщества"}
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
try:
|
||||
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)}
|
||||
return {"ok": True}
|
||||
return {"ok": False, "error": "Please, set community slug in input"}
|
||||
# Находим сообщество для обновления
|
||||
community = session.query(Community).filter(Community.slug == slug).first()
|
||||
if not community:
|
||||
return {"error": "Сообщество не найдено"}
|
||||
|
||||
# Проверяем права на редактирование (создатель или админ/редактор)
|
||||
with local_session() as auth_session:
|
||||
author = auth_session.query(Author).filter(Author.id == author_id).first()
|
||||
user_roles = [role.id for role in author.roles] if author and author.roles else []
|
||||
|
||||
# Разрешаем редактирование если пользователь - создатель или имеет роль admin/editor
|
||||
if community.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
|
||||
return {"error": "Недостаточно прав для редактирования этого сообщества"}
|
||||
|
||||
# Обновляем поля сообщества
|
||||
for key, value in community_input.items():
|
||||
# Исключаем изменение created_by - создатель не может быть изменен
|
||||
if hasattr(community, key) and key not in ["slug", "created_by"]:
|
||||
setattr(community, key, value)
|
||||
|
||||
session.commit()
|
||||
return {"error": None}
|
||||
except Exception as e:
|
||||
return {"error": f"Ошибка обновления сообщества: {e!s}"}
|
||||
|
||||
|
||||
@mutation.field("delete_community")
|
||||
@editor_or_admin_required
|
||||
async def delete_community(_: None, info: GraphQLResolveInfo, slug: str) -> dict[str, Any]:
|
||||
author_dict = info.context.get("author", {})
|
||||
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()
|
||||
# Получаем author_id из контекста через декоратор авторизации
|
||||
request = info.context.get("request")
|
||||
author_id = None
|
||||
|
||||
if hasattr(request, "auth") and request.auth and hasattr(request.auth, "author_id"):
|
||||
author_id = request.auth.author_id
|
||||
elif hasattr(request, "scope") and "auth" in request.scope:
|
||||
auth_info = request.scope.get("auth", {})
|
||||
if isinstance(auth_info, dict):
|
||||
author_id = auth_info.get("author_id")
|
||||
elif hasattr(auth_info, "author_id"):
|
||||
author_id = auth_info.author_id
|
||||
|
||||
if not author_id:
|
||||
return {"error": "Не удалось определить автора"}
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Находим сообщество для удаления
|
||||
community = session.query(Community).filter(Community.slug == slug).first()
|
||||
if not community:
|
||||
return {"error": "Сообщество не найдено"}
|
||||
|
||||
# Проверяем права на удаление (создатель или админ/редактор)
|
||||
with local_session() as auth_session:
|
||||
author = auth_session.query(Author).filter(Author.id == author_id).first()
|
||||
user_roles = [role.id for role in author.roles] if author and author.roles else []
|
||||
|
||||
# Разрешаем удаление если пользователь - создатель или имеет роль admin/editor
|
||||
if community.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
|
||||
return {"error": "Недостаточно прав для удаления этого сообщества"}
|
||||
|
||||
# Удаляем сообщество
|
||||
session.delete(community)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
return {"error": None}
|
||||
except Exception as e:
|
||||
return {"error": f"Ошибка удаления сообщества: {e!s}"}
|
||||
|
||||
|
||||
@type_community.field("created_by")
|
||||
def resolve_community_created_by(obj: Community, *_: Any) -> Author:
|
||||
"""
|
||||
Резолвер поля created_by для Community.
|
||||
Возвращает автора, создавшего сообщество.
|
||||
"""
|
||||
# Если связь уже загружена через joinedload и валидна
|
||||
if hasattr(obj, "created_by_author") and obj.created_by_author and obj.created_by_author.id:
|
||||
return obj.created_by_author
|
||||
|
||||
# Критическая ошибка - это не должно происходить после фильтрации в get_communities_all
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
logger.error(f"КРИТИЧЕСКАЯ ОШИБКА: Резолвер created_by вызван для сообщества {obj.id} без валидного автора")
|
||||
error_message = f"Сообщество {obj.id} не имеет валидного создателя"
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
@type_community.field("stat")
|
||||
def resolve_community_stat(obj: Community, *_: Any) -> dict[str, int]:
|
||||
"""
|
||||
Резолвер поля stat для Community.
|
||||
Возвращает статистику сообщества: количество публикаций, подписчиков и авторов.
|
||||
"""
|
||||
from sqlalchemy import distinct, func
|
||||
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Количество опубликованных публикаций в сообществе
|
||||
shouts_count = (
|
||||
session.query(func.count(Shout.id))
|
||||
.filter(Shout.community == obj.id, Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Количество подписчиков сообщества
|
||||
followers_count = (
|
||||
session.query(func.count(CommunityFollower.follower))
|
||||
.filter(CommunityFollower.community == obj.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Количество уникальных авторов, опубликовавших в сообществе
|
||||
authors_count = (
|
||||
session.query(func.count(distinct(ShoutAuthor.author)))
|
||||
.join(Shout, ShoutAuthor.shout == Shout.id)
|
||||
.filter(Shout.community == obj.id, Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
return {"shouts": int(shouts_count), "followers": int(followers_count), "authors": int(authors_count)}
|
||||
|
||||
except Exception as e:
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
logger.error(f"Ошибка при получении статистики сообщества {obj.id}: {e}")
|
||||
# Возвращаем нулевую статистику при ошибке
|
||||
return {"shouts": 0, "followers": 0, "authors": 0}
|
||||
|
@@ -11,6 +11,7 @@ from cache.cache import (
|
||||
get_cached_topic_by_slug,
|
||||
get_cached_topic_followers,
|
||||
invalidate_cache_by_prefix,
|
||||
invalidate_topic_followers_cache,
|
||||
)
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
@@ -446,3 +447,55 @@ async def get_topic_authors(_: None, _info: GraphQLResolveInfo, slug: str) -> li
|
||||
topic = await get_cached_topic_by_slug(slug, get_with_stat)
|
||||
topic_id = getattr(topic, "id", None) if isinstance(topic, Topic) else topic.get("id") if topic else None
|
||||
return await get_cached_topic_authors(topic_id) if topic_id else []
|
||||
|
||||
|
||||
# Мутация для удаления темы по ID (для админ-панели)
|
||||
@mutation.field("delete_topic_by_id")
|
||||
@login_required
|
||||
async def delete_topic_by_id(_: None, info: GraphQLResolveInfo, topic_id: int) -> dict[str, Any]:
|
||||
"""
|
||||
Удаляет тему по ID. Используется в админ-панели.
|
||||
|
||||
Args:
|
||||
topic_id: ID темы для удаления
|
||||
|
||||
Returns:
|
||||
dict: Результат операции
|
||||
"""
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
return {"success": False, "message": "Топик не найден"}
|
||||
|
||||
author = session.query(Author).filter(Author.id == viewer_id).first()
|
||||
if not author:
|
||||
return {"success": False, "message": "Не авторизован"}
|
||||
|
||||
# TODO: проверить права администратора
|
||||
# Для админ-панели допускаем удаление любых топиков администратором
|
||||
|
||||
try:
|
||||
# Инвалидируем кеши подписчиков ПЕРЕД удалением данных из БД
|
||||
await invalidate_topic_followers_cache(topic_id)
|
||||
|
||||
# Удаляем связанные данные (подписчики, связи с публикациями)
|
||||
session.query(TopicFollower).filter(TopicFollower.topic == topic_id).delete()
|
||||
session.query(ShoutTopic).filter(ShoutTopic.topic == topic_id).delete()
|
||||
|
||||
# Удаляем сам топик
|
||||
session.delete(topic)
|
||||
session.commit()
|
||||
|
||||
# Инвалидируем основные кеши топика
|
||||
await invalidate_topics_cache(topic_id)
|
||||
if topic.slug:
|
||||
await redis.execute("DEL", f"topic:slug:{topic.slug}")
|
||||
|
||||
logger.info(f"Топик {topic_id} успешно удален")
|
||||
return {"success": True, "message": "Топик успешно удален"}
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Ошибка при удалении топика {topic_id}: {e}")
|
||||
return {"success": False, "message": f"Ошибка при удалении: {e!s}"}
|
||||
|
Reference in New Issue
Block a user