tests-passed

This commit is contained in:
2025-07-31 18:55:59 +03:00
parent b7abb8d8a1
commit e7230ba63c
126 changed files with 8326 additions and 3207 deletions

View File

@@ -1,50 +1,22 @@
from typing import Any
from graphql import GraphQLResolveInfo
from sqlalchemy import distinct, func
from auth.orm import Author
from orm.community import Community, CommunityFollower
from auth.permissions import ContextualPermissionCheck
from orm.community import Community, CommunityAuthor, CommunityFollower
from orm.shout import Shout, ShoutAuthor
from services.db import local_session
from services.rbac import require_any_permission, require_permission
from services.schema import mutation, query, type_community
from utils.logger import root_logger as logger
@query.field("get_communities_all")
async def get_communities_all(_: None, _info: GraphQLResolveInfo) -> list[Community]:
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
return session.query(Community).all()
@query.field("get_community")
@@ -60,13 +32,17 @@ async def get_communities_by_author(
with local_session() as session:
q = session.query(Community).join(CommunityFollower)
if slug:
author_id = session.query(Author).where(Author.slug == slug).first().id
q = q.where(CommunityFollower.author == author_id)
author = session.query(Author).where(Author.slug == slug).first()
if author:
author_id = author.id
q = q.where(CommunityFollower.follower == author_id)
if user:
author_id = session.query(Author).where(Author.id == user).first().id
q = q.where(CommunityFollower.author == author_id)
author = session.query(Author).where(Author.id == user).first()
if author:
author_id = author.id
q = q.where(CommunityFollower.follower == author_id)
if author_id:
q = q.where(CommunityFollower.author == author_id)
q = q.where(CommunityFollower.follower == author_id)
return q.all()
return []
@@ -76,11 +52,14 @@ async def get_communities_by_author(
async def join_community(_: None, info: GraphQLResolveInfo, slug: str) -> dict[str, Any]:
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not author_id:
return {"ok": False, "error": "Unauthorized"}
with local_session() as session:
community = session.query(Community).where(Community.slug == slug).first()
if not community:
return {"ok": False, "error": "Community not found"}
session.add(CommunityFollower(community=community.id, follower=author_id))
session.add(CommunityFollower(community=community.id, follower=int(author_id)))
session.commit()
return {"ok": True}
@@ -91,7 +70,7 @@ async def leave_community(_: None, info: GraphQLResolveInfo, slug: str) -> dict[
author_id = author_dict.get("id")
with local_session() as session:
session.query(CommunityFollower).where(
CommunityFollower.author == author_id, CommunityFollower.community == slug
CommunityFollower.follower == author_id, CommunityFollower.community == slug
).delete()
session.commit()
return {"ok": True}
@@ -161,14 +140,20 @@ async def update_community(_: None, info: GraphQLResolveInfo, community_input: d
try:
with local_session() as session:
# Находим сообщество для обновления
community = session.query(Community).filter(Community.slug == slug).first()
community = session.query(Community).where(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 []
# Получаем роли пользователя в сообществе
community_author = (
auth_session.query(CommunityAuthor)
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community.id)
.first()
)
user_roles = community_author.role_list if community_author else []
# Разрешаем редактирование если пользователь - создатель или имеет роль admin/editor
if community.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
@@ -188,81 +173,51 @@ async def update_community(_: None, info: GraphQLResolveInfo, community_input: d
@mutation.field("delete_community")
@require_any_permission(["community:delete_own", "community:delete_any"])
async def delete_community(_: None, info: GraphQLResolveInfo, slug: str) -> 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": "Не удалось определить автора"}
async def delete_community(root, info, slug: str) -> dict[str, Any]:
try:
# Используем local_session как контекстный менеджер
with local_session() as session:
# Находим сообщество для удаления
community = session.query(Community).filter(Community.slug == slug).first()
# Находим сообщество по slug
community = session.query(Community).where(Community.slug == slug).first()
if not community:
return {"error": "Сообщество не найдено"}
return {"error": "Сообщество не найдено", "success": False}
# Проверяем права на удаление (создатель или админ/редактор)
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 []
# Проверяем права на удаление
user_id = info.context.get("user_id", 0)
permission_check = ContextualPermissionCheck()
# Разрешаем удаление если пользователь - создатель или имеет роль admin/editor
if community.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
return {"error": "Недостаточно прав для удаления этого сообщества"}
# Проверяем права на удаление сообщества
if not await permission_check.can_delete_community(user_id, community, session):
return {"error": "Недостаточно прав", "success": False}
# Удаляем сообщество
session.delete(community)
session.commit()
return {"error": None}
return {"success": True, "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)
# Логируем ошибку
logger.error(f"Ошибка удаления сообщества: {e}")
return {"error": str(e), "success": False}
@type_community.field("stat")
def resolve_community_stat(obj: Community, *_: Any) -> dict[str, int]:
def resolve_community_stat(community: Community | dict[str, Any], *_: Any) -> dict[str, int]:
"""
Резолвер поля stat для Community.
Возвращает статистику сообщества: количество публикаций, подписчиков и авторов.
"""
from sqlalchemy import distinct, func
from orm.shout import Shout, ShoutAuthor
community_id = community.get("id") if isinstance(community, dict) else community.id
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))
.where(Shout.community == community_id, Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
.scalar()
or 0
)
@@ -270,7 +225,7 @@ def resolve_community_stat(obj: Community, *_: Any) -> dict[str, int]:
# Количество подписчиков сообщества
followers_count = (
session.query(func.count(CommunityFollower.follower))
.filter(CommunityFollower.community == obj.id)
.where(CommunityFollower.community == community_id)
.scalar()
or 0
)
@@ -279,7 +234,7 @@ def resolve_community_stat(obj: Community, *_: Any) -> dict[str, int]:
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))
.where(Shout.community == community_id, Shout.published_at.is_not(None), Shout.deleted_at.is_(None))
.scalar()
or 0
)
@@ -287,8 +242,6 @@ def resolve_community_stat(obj: Community, *_: Any) -> dict[str, int]:
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}")
logger.error(f"Ошибка при получении статистики сообщества {community_id}: {e}")
# Возвращаем нулевую статистику при ошибке
return {"shouts": 0, "followers": 0, "authors": 0}