tests-passed
This commit is contained in:
@@ -2,21 +2,30 @@
|
||||
Админ-резолверы - тонкие GraphQL обёртки над AdminService
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from graphql.error import GraphQLError
|
||||
from graphql import GraphQLError, GraphQLResolveInfo
|
||||
from sqlalchemy import and_, case, func, or_
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from auth.decorators import admin_auth_required
|
||||
from services.admin import admin_service
|
||||
from auth.orm import Author
|
||||
from orm.community import Community, CommunityAuthor
|
||||
from orm.draft import DraftTopic
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout, ShoutTopic
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from resolvers.editor import delete_shout, update_shout
|
||||
from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_cache
|
||||
from services.admin import AdminService
|
||||
from services.common_result import handle_error
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from services.schema import mutation, query
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def handle_error(operation: str, error: Exception) -> GraphQLError:
|
||||
"""Обрабатывает ошибки в резолверах"""
|
||||
logger.error(f"Ошибка при {operation}: {error}")
|
||||
return GraphQLError(f"Не удалось {operation}: {error}")
|
||||
admin_service = AdminService()
|
||||
|
||||
|
||||
# === ПОЛЬЗОВАТЕЛИ ===
|
||||
@@ -53,15 +62,15 @@ async def admin_update_user(_: None, _info: GraphQLResolveInfo, user: dict[str,
|
||||
async def admin_get_shouts(
|
||||
_: None,
|
||||
_info: GraphQLResolveInfo,
|
||||
limit: int = 20,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
search: str = "",
|
||||
status: str = "all",
|
||||
community: int = None,
|
||||
community: Optional[int] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Получает список публикаций"""
|
||||
try:
|
||||
return admin_service.get_shouts(limit, offset, search, status, community)
|
||||
return await admin_service.get_shouts(limit, offset, search, status, community)
|
||||
except Exception as e:
|
||||
raise handle_error("получении списка публикаций", e) from e
|
||||
|
||||
@@ -71,8 +80,6 @@ async def admin_get_shouts(
|
||||
async def admin_update_shout(_: None, info: GraphQLResolveInfo, shout: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Обновляет публикацию через editor.py"""
|
||||
try:
|
||||
from resolvers.editor import update_shout
|
||||
|
||||
shout_id = shout.get("id")
|
||||
if not shout_id:
|
||||
return {"success": False, "error": "ID публикации не указан"}
|
||||
@@ -95,8 +102,6 @@ async def admin_update_shout(_: None, info: GraphQLResolveInfo, shout: dict[str,
|
||||
async def admin_delete_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]:
|
||||
"""Удаляет публикацию через editor.py"""
|
||||
try:
|
||||
from resolvers.editor import delete_shout
|
||||
|
||||
result = await delete_shout(None, info, shout_id)
|
||||
if result.error:
|
||||
return {"success": False, "error": result.error}
|
||||
@@ -163,37 +168,9 @@ async def admin_delete_invite(
|
||||
|
||||
@query.field("adminGetTopics")
|
||||
@admin_auth_required
|
||||
async def admin_get_topics(_: None, _info: GraphQLResolveInfo, community_id: int) -> list[dict[str, Any]]:
|
||||
"""Получает все топики сообщества для админ-панели"""
|
||||
try:
|
||||
from orm.topic import Topic
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Получаем все топики сообщества без лимитов
|
||||
topics = session.query(Topic).filter(Topic.community == community_id).order_by(Topic.id).all()
|
||||
|
||||
# Сериализуем топики в простой формат для админки
|
||||
result: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": topic.id,
|
||||
"title": topic.title or "",
|
||||
"slug": topic.slug or f"topic-{topic.id}",
|
||||
"body": topic.body or "",
|
||||
"community": topic.community,
|
||||
"parent_ids": topic.parent_ids or [],
|
||||
"pic": topic.pic,
|
||||
"oid": getattr(topic, "oid", None),
|
||||
"is_main": getattr(topic, "is_main", False),
|
||||
}
|
||||
for topic in topics
|
||||
]
|
||||
|
||||
logger.info(f"Загружено топиков для сообщества: {len(result)}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise handle_error("получении списка топиков", e) from e
|
||||
async def admin_get_topics(_: None, _info: GraphQLResolveInfo, community_id: int) -> list[Topic]:
|
||||
with local_session() as session:
|
||||
return session.query(Topic).where(Topic.community == community_id).all()
|
||||
|
||||
|
||||
@mutation.field("adminUpdateTopic")
|
||||
@@ -201,17 +178,12 @@ async def admin_get_topics(_: None, _info: GraphQLResolveInfo, community_id: int
|
||||
async def admin_update_topic(_: None, _info: GraphQLResolveInfo, topic: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Обновляет топик через админ-панель"""
|
||||
try:
|
||||
from orm.topic import Topic
|
||||
from resolvers.topic import invalidate_topics_cache
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
|
||||
topic_id = topic.get("id")
|
||||
if not topic_id:
|
||||
return {"success": False, "error": "ID топика не указан"}
|
||||
|
||||
with local_session() as session:
|
||||
existing_topic = session.query(Topic).filter(Topic.id == topic_id).first()
|
||||
existing_topic = session.query(Topic).where(Topic.id == topic_id).first()
|
||||
if not existing_topic:
|
||||
return {"success": False, "error": "Топик не найден"}
|
||||
|
||||
@@ -248,10 +220,6 @@ async def admin_update_topic(_: None, _info: GraphQLResolveInfo, topic: dict[str
|
||||
async def admin_create_topic(_: None, _info: GraphQLResolveInfo, topic: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Создает новый топик через админ-панель"""
|
||||
try:
|
||||
from orm.topic import Topic
|
||||
from resolvers.topic import invalidate_topics_cache
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Создаем новый топик
|
||||
new_topic = Topic(**topic)
|
||||
@@ -285,13 +253,6 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
dict: Результат операции с информацией о слиянии
|
||||
"""
|
||||
try:
|
||||
from orm.draft import DraftTopic
|
||||
from orm.shout import ShoutTopic
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_cache
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
|
||||
target_topic_id = merge_input["target_topic_id"]
|
||||
source_topic_ids = merge_input["source_topic_ids"]
|
||||
preserve_target = merge_input.get("preserve_target_properties", True)
|
||||
@@ -302,12 +263,12 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
|
||||
with local_session() as session:
|
||||
# Получаем целевую тему
|
||||
target_topic = session.query(Topic).filter(Topic.id == target_topic_id).first()
|
||||
target_topic = session.query(Topic).where(Topic.id == target_topic_id).first()
|
||||
if not target_topic:
|
||||
return {"success": False, "error": f"Целевая тема с ID {target_topic_id} не найдена"}
|
||||
|
||||
# Получаем исходные темы
|
||||
source_topics = session.query(Topic).filter(Topic.id.in_(source_topic_ids)).all()
|
||||
source_topics = session.query(Topic).where(Topic.id.in_(source_topic_ids)).all()
|
||||
if len(source_topics) != len(source_topic_ids):
|
||||
found_ids = [t.id for t in source_topics]
|
||||
missing_ids = [topic_id for topic_id in source_topic_ids if topic_id not in found_ids]
|
||||
@@ -325,13 +286,13 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
# Переносим подписчиков из исходных тем в целевую
|
||||
for source_topic in source_topics:
|
||||
# Получаем подписчиков исходной темы
|
||||
source_followers = session.query(TopicFollower).filter(TopicFollower.topic == source_topic.id).all()
|
||||
source_followers = session.query(TopicFollower).where(TopicFollower.topic == source_topic.id).all()
|
||||
|
||||
for follower in source_followers:
|
||||
# Проверяем, не подписан ли уже пользователь на целевую тему
|
||||
existing = (
|
||||
session.query(TopicFollower)
|
||||
.filter(TopicFollower.topic == target_topic_id, TopicFollower.follower == follower.follower)
|
||||
.where(TopicFollower.topic == target_topic_id, TopicFollower.follower == follower.follower)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -352,17 +313,18 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
# Переносим публикации из исходных тем в целевую
|
||||
for source_topic in source_topics:
|
||||
# Получаем связи публикаций с исходной темой
|
||||
shout_topics = session.query(ShoutTopic).filter(ShoutTopic.topic == source_topic.id).all()
|
||||
shout_topics = session.query(ShoutTopic).where(ShoutTopic.topic == source_topic.id).all()
|
||||
|
||||
for shout_topic in shout_topics:
|
||||
# Проверяем, не связана ли уже публикация с целевой темой
|
||||
existing = (
|
||||
existing_shout_topic: ShoutTopic | None = (
|
||||
session.query(ShoutTopic)
|
||||
.filter(ShoutTopic.topic == target_topic_id, ShoutTopic.shout == shout_topic.shout)
|
||||
.where(ShoutTopic.topic == target_topic_id)
|
||||
.where(ShoutTopic.shout == shout_topic.shout)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
if not existing_shout_topic:
|
||||
# Создаем новую связь с целевой темой
|
||||
new_shout_topic = ShoutTopic(
|
||||
topic=target_topic_id, shout=shout_topic.shout, main=shout_topic.main
|
||||
@@ -376,20 +338,21 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
# Переносим черновики из исходных тем в целевую
|
||||
for source_topic in source_topics:
|
||||
# Получаем связи черновиков с исходной темой
|
||||
draft_topics = session.query(DraftTopic).filter(DraftTopic.topic == source_topic.id).all()
|
||||
draft_topics = session.query(DraftTopic).where(DraftTopic.topic == source_topic.id).all()
|
||||
|
||||
for draft_topic in draft_topics:
|
||||
# Проверяем, не связан ли уже черновик с целевой темой
|
||||
existing = (
|
||||
existing_draft_topic: DraftTopic | None = (
|
||||
session.query(DraftTopic)
|
||||
.filter(DraftTopic.topic == target_topic_id, DraftTopic.shout == draft_topic.shout)
|
||||
.where(DraftTopic.topic == target_topic_id)
|
||||
.where(DraftTopic.draft == draft_topic.draft)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
if not existing_draft_topic:
|
||||
# Создаем новую связь с целевой темой
|
||||
new_draft_topic = DraftTopic(
|
||||
topic=target_topic_id, shout=draft_topic.shout, main=draft_topic.main
|
||||
topic=target_topic_id, draft=draft_topic.draft, main=draft_topic.main
|
||||
)
|
||||
session.add(new_draft_topic)
|
||||
merge_stats["drafts_moved"] += 1
|
||||
@@ -400,7 +363,7 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
# Обновляем parent_ids дочерних топиков
|
||||
for source_topic in source_topics:
|
||||
# Находим всех детей исходной темы
|
||||
child_topics = session.query(Topic).filter(Topic.parent_ids.contains(int(source_topic.id))).all() # type: ignore[arg-type]
|
||||
child_topics = session.query(Topic).where(Topic.parent_ids.contains(int(source_topic.id))).all() # type: ignore[arg-type]
|
||||
|
||||
for child_topic in child_topics:
|
||||
current_parent_ids = list(child_topic.parent_ids or [])
|
||||
@@ -409,7 +372,7 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
target_topic_id if parent_id == source_topic.id else parent_id
|
||||
for parent_id in current_parent_ids
|
||||
]
|
||||
child_topic.parent_ids = updated_parent_ids
|
||||
child_topic.parent_ids = list(updated_parent_ids)
|
||||
|
||||
# Объединяем parent_ids если не сохраняем только целевые свойства
|
||||
if not preserve_target:
|
||||
@@ -423,7 +386,7 @@ async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: di
|
||||
all_parent_ids.discard(target_topic_id)
|
||||
for source_id in source_topic_ids:
|
||||
all_parent_ids.discard(source_id)
|
||||
target_topic.parent_ids = list(all_parent_ids) if all_parent_ids else []
|
||||
target_topic.parent_ids = list(all_parent_ids) if all_parent_ids else None
|
||||
|
||||
# Инвалидируем кеши ПЕРЕД удалением тем
|
||||
for source_topic in source_topics:
|
||||
@@ -493,7 +456,7 @@ async def update_env_variables(_: None, _info: GraphQLResolveInfo, variables: li
|
||||
|
||||
@query.field("adminGetRoles")
|
||||
@admin_auth_required
|
||||
async def admin_get_roles(_: None, _info: GraphQLResolveInfo, community: int = None) -> list[dict[str, Any]]:
|
||||
async def admin_get_roles(_: None, _info: GraphQLResolveInfo, community: int | None = None) -> list[dict[str, Any]]:
|
||||
"""Получает список ролей"""
|
||||
try:
|
||||
return admin_service.get_roles(community)
|
||||
@@ -513,14 +476,12 @@ async def admin_get_user_community_roles(
|
||||
) -> dict[str, Any]:
|
||||
"""Получает роли пользователя в сообществе"""
|
||||
# [непроверенное] Временная заглушка - нужно вынести в сервис
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
community_author = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -540,25 +501,20 @@ async def admin_get_community_members(
|
||||
) -> dict[str, Any]:
|
||||
"""Получает участников сообщества"""
|
||||
# [непроверенное] Временная заглушка - нужно вынести в сервис
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
members_query = (
|
||||
session.query(Author, CommunityAuthor)
|
||||
.join(CommunityAuthor, Author.id == CommunityAuthor.author_id)
|
||||
.filter(CommunityAuthor.community_id == community_id)
|
||||
.where(CommunityAuthor.community_id == community_id)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
members = []
|
||||
members: list[dict[str, Any]] = []
|
||||
for author, community_author in members_query:
|
||||
roles = []
|
||||
roles: list[str] = []
|
||||
if community_author.roles:
|
||||
roles = [role.strip() for role in community_author.roles.split(",") if role.strip()]
|
||||
|
||||
@@ -574,7 +530,7 @@ async def admin_get_community_members(
|
||||
|
||||
total = (
|
||||
session.query(func.count(CommunityAuthor.author_id))
|
||||
.filter(CommunityAuthor.community_id == community_id)
|
||||
.where(CommunityAuthor.community_id == community_id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@@ -589,12 +545,10 @@ async def admin_get_community_members(
|
||||
async def admin_get_community_role_settings(_: None, _info: GraphQLResolveInfo, community_id: int) -> dict[str, Any]:
|
||||
"""Получает настройки ролей сообщества"""
|
||||
# [непроверенное] Временная заглушка - нужно вынести в сервис
|
||||
from orm.community import Community
|
||||
from services.db import local_session
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
community = session.query(Community).filter(Community.id == community_id).first()
|
||||
community = session.query(Community).where(Community.id == community_id).first()
|
||||
if not community:
|
||||
return {
|
||||
"community_id": community_id,
|
||||
@@ -630,20 +584,12 @@ async def admin_get_reactions(
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
search: str = "",
|
||||
kind: str = None,
|
||||
shout_id: int = None,
|
||||
kind: str | None = None,
|
||||
shout_id: int | None = None,
|
||||
status: str = "all",
|
||||
) -> dict[str, Any]:
|
||||
"""Получает список реакций для админ-панели"""
|
||||
try:
|
||||
from sqlalchemy import and_, case, func, or_
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Базовый запрос с джойнами
|
||||
query = (
|
||||
@@ -653,7 +599,7 @@ async def admin_get_reactions(
|
||||
)
|
||||
|
||||
# Фильтрация
|
||||
filters = []
|
||||
filters: list[Any] = []
|
||||
|
||||
# Фильтр по статусу (как в публикациях)
|
||||
if status == "active":
|
||||
@@ -677,7 +623,7 @@ async def admin_get_reactions(
|
||||
filters.append(Reaction.shout == shout_id)
|
||||
|
||||
if filters:
|
||||
query = query.filter(and_(*filters))
|
||||
query = query.where(and_(*filters))
|
||||
|
||||
# Общее количество
|
||||
total = query.count()
|
||||
@@ -686,7 +632,7 @@ async def admin_get_reactions(
|
||||
reactions_data = query.order_by(Reaction.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
# Формируем результат
|
||||
reactions = []
|
||||
reactions: list[dict[str, Any]] = []
|
||||
for reaction, author, shout in reactions_data:
|
||||
# Получаем статистику для каждой реакции
|
||||
aliased_reaction = aliased(Reaction)
|
||||
@@ -699,7 +645,7 @@ async def admin_get_reactions(
|
||||
)
|
||||
).label("rating"),
|
||||
)
|
||||
.filter(
|
||||
.where(
|
||||
aliased_reaction.reply_to == reaction.id,
|
||||
# Убираем фильтр deleted_at чтобы включить все реакции в статистику
|
||||
)
|
||||
@@ -760,18 +706,13 @@ async def admin_get_reactions(
|
||||
async def admin_update_reaction(_: None, _info: GraphQLResolveInfo, reaction: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Обновляет реакцию"""
|
||||
try:
|
||||
import time
|
||||
|
||||
from orm.reaction import Reaction
|
||||
from services.db import local_session
|
||||
|
||||
reaction_id = reaction.get("id")
|
||||
if not reaction_id:
|
||||
return {"success": False, "error": "ID реакции не указан"}
|
||||
|
||||
with local_session() as session:
|
||||
# Находим реакцию
|
||||
db_reaction = session.query(Reaction).filter(Reaction.id == reaction_id).first()
|
||||
db_reaction = session.query(Reaction).where(Reaction.id == reaction_id).first()
|
||||
if not db_reaction:
|
||||
return {"success": False, "error": "Реакция не найдена"}
|
||||
|
||||
@@ -779,10 +720,10 @@ async def admin_update_reaction(_: None, _info: GraphQLResolveInfo, reaction: di
|
||||
if "body" in reaction:
|
||||
db_reaction.body = reaction["body"]
|
||||
if "deleted_at" in reaction:
|
||||
db_reaction.deleted_at = reaction["deleted_at"]
|
||||
db_reaction.deleted_at = int(time.time()) # type: ignore[assignment]
|
||||
|
||||
# Обновляем время изменения
|
||||
db_reaction.updated_at = int(time.time())
|
||||
db_reaction.updated_at = int(time.time()) # type: ignore[assignment]
|
||||
|
||||
session.commit()
|
||||
|
||||
@@ -799,19 +740,14 @@ async def admin_update_reaction(_: None, _info: GraphQLResolveInfo, reaction: di
|
||||
async def admin_delete_reaction(_: None, _info: GraphQLResolveInfo, reaction_id: int) -> dict[str, Any]:
|
||||
"""Удаляет реакцию (мягкое удаление)"""
|
||||
try:
|
||||
import time
|
||||
|
||||
from orm.reaction import Reaction
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Находим реакцию
|
||||
db_reaction = session.query(Reaction).filter(Reaction.id == reaction_id).first()
|
||||
db_reaction = session.query(Reaction).where(Reaction.id == reaction_id).first()
|
||||
if not db_reaction:
|
||||
return {"success": False, "error": "Реакция не найдена"}
|
||||
|
||||
# Устанавливаем время удаления
|
||||
db_reaction.deleted_at = int(time.time())
|
||||
db_reaction.deleted_at = int(time.time()) # type: ignore[assignment]
|
||||
|
||||
session.commit()
|
||||
|
||||
@@ -828,12 +764,9 @@ async def admin_delete_reaction(_: None, _info: GraphQLResolveInfo, reaction_id:
|
||||
async def admin_restore_reaction(_: None, _info: GraphQLResolveInfo, reaction_id: int) -> dict[str, Any]:
|
||||
"""Восстанавливает удаленную реакцию"""
|
||||
try:
|
||||
from orm.reaction import Reaction
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Находим реакцию
|
||||
db_reaction = session.query(Reaction).filter(Reaction.id == reaction_id).first()
|
||||
db_reaction = session.query(Reaction).where(Reaction.id == reaction_id).first()
|
||||
if not db_reaction:
|
||||
return {"success": False, "error": "Реакция не найдена"}
|
||||
|
||||
|
@@ -2,28 +2,21 @@
|
||||
Auth резолверы - тонкие GraphQL обёртки над AuthService
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Union
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from graphql.error import GraphQLError
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from services.auth import auth_service
|
||||
from services.schema import mutation, query, type_author
|
||||
from settings import SESSION_COOKIE_NAME
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def handle_error(operation: str, error: Exception) -> GraphQLError:
|
||||
"""Обрабатывает ошибки в резолверах"""
|
||||
logger.error(f"Ошибка при {operation}: {error}")
|
||||
return GraphQLError(f"Не удалось {operation}: {error}")
|
||||
|
||||
|
||||
# === РЕЗОЛВЕР ДЛЯ ТИПА AUTHOR ===
|
||||
|
||||
|
||||
@type_author.field("roles")
|
||||
def resolve_roles(obj: Union[Dict, Any], info: GraphQLResolveInfo) -> List[str]:
|
||||
def resolve_roles(obj: Union[dict, Any], info: GraphQLResolveInfo) -> list[str]:
|
||||
"""Резолвер для поля roles автора"""
|
||||
try:
|
||||
if hasattr(obj, "get_roles"):
|
||||
@@ -60,13 +53,13 @@ async def register_user(
|
||||
@mutation.field("sendLink")
|
||||
async def send_link(
|
||||
_: None, _info: GraphQLResolveInfo, email: str, lang: str = "ru", template: str = "confirm"
|
||||
) -> dict[str, Any]:
|
||||
) -> bool:
|
||||
"""Отправляет ссылку подтверждения"""
|
||||
try:
|
||||
result = await auth_service.send_verification_link(email, lang, template)
|
||||
return result
|
||||
return bool(await auth_service.send_verification_link(email, lang, template))
|
||||
except Exception as e:
|
||||
raise handle_error("отправке ссылки подтверждения", e) from e
|
||||
logger.error(f"Ошибка отправки ссылки подтверждения: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@mutation.field("confirmEmail")
|
||||
@@ -93,8 +86,6 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
|
||||
# Устанавливаем cookie если есть токен
|
||||
if result.get("success") and result.get("token") and request:
|
||||
try:
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
if not hasattr(info.context, "response"):
|
||||
response = JSONResponse({})
|
||||
response.set_cookie(
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Optional, TypedDict
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import and_, asc, func, select, text
|
||||
from sqlalchemy.sql import desc as sql_desc
|
||||
|
||||
from auth.orm import Author
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cached_query,
|
||||
@@ -15,6 +17,8 @@ from cache.cache import (
|
||||
get_cached_follower_topics,
|
||||
invalidate_cache_by_prefix,
|
||||
)
|
||||
from orm.community import Community, CommunityAuthor, CommunityFollower
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.auth import login_required
|
||||
from services.common_result import CommonResult
|
||||
@@ -80,7 +84,7 @@ async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
|
||||
authors = session.execute(authors_query).scalars().unique().all()
|
||||
|
||||
# Преобразуем авторов в словари с учетом прав доступа
|
||||
return [author.dict(False) for author in authors]
|
||||
return [author.dict() for author in authors]
|
||||
|
||||
# Используем универсальную функцию для кеширования запросов
|
||||
return await cached_query(cache_key, fetch_all_authors)
|
||||
@@ -89,7 +93,7 @@ async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
|
||||
# Вспомогательная функция для получения авторов со статистикой с пагинацией
|
||||
async def get_authors_with_stats(
|
||||
limit: int = 10, offset: int = 0, by: Optional[AuthorsBy] = None, current_user_id: Optional[int] = None
|
||||
):
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Получает авторов со статистикой с пагинацией.
|
||||
|
||||
@@ -112,13 +116,6 @@ async def get_authors_with_stats(
|
||||
"""
|
||||
logger.debug(f"Выполняем запрос на получение авторов со статистикой: limit={limit}, offset={offset}, by={by}")
|
||||
|
||||
# Импорты SQLAlchemy для избежания конфликтов имен
|
||||
from sqlalchemy import and_, asc, func
|
||||
from sqlalchemy import desc as sql_desc
|
||||
|
||||
from auth.orm import AuthorFollower
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
|
||||
with local_session() as session:
|
||||
# Базовый запрос для получения авторов
|
||||
base_query = select(Author).where(Author.deleted_at.is_(None))
|
||||
@@ -303,7 +300,7 @@ async def invalidate_authors_cache(author_id=None) -> None:
|
||||
|
||||
# Получаем author_id автора, если есть
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
if author and Author.id:
|
||||
specific_keys.append(f"author:id:{Author.id}")
|
||||
|
||||
@@ -355,8 +352,6 @@ async def update_author(_: None, info: GraphQLResolveInfo, profile: dict[str, An
|
||||
# Если мы дошли до сюда, значит автор не найден
|
||||
return CommonResult(error="Author not found", author=None)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return CommonResult(error=str(exc), author=None)
|
||||
|
||||
@@ -403,13 +398,13 @@ async def get_author(
|
||||
|
||||
if not author_dict or not author_dict.get("stat"):
|
||||
# update stat from db
|
||||
author_query = select(Author).filter(Author.id == author_id)
|
||||
author_query = select(Author).where(Author.id == author_id)
|
||||
result = get_with_stat(author_query)
|
||||
if result:
|
||||
author_with_stat = result[0]
|
||||
if isinstance(author_with_stat, Author):
|
||||
# Кэшируем полные данные для админов
|
||||
original_dict = author_with_stat.dict(True)
|
||||
original_dict = author_with_stat.dict()
|
||||
_t = asyncio.create_task(cache_author(original_dict))
|
||||
|
||||
# Возвращаем отфильтрованную версию
|
||||
@@ -420,8 +415,6 @@ async def get_author(
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
logger.error(f"{exc}:\n{traceback.format_exc()}")
|
||||
return author_dict
|
||||
|
||||
@@ -446,8 +439,6 @@ async def load_authors_by(
|
||||
# Используем оптимизированную функцию для получения авторов
|
||||
return await get_authors_with_stats(limit, offset, by, viewer_id)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
logger.error(f"{exc}:\n{traceback.format_exc()}")
|
||||
return []
|
||||
|
||||
@@ -469,11 +460,11 @@ def get_author_id_from(
|
||||
with local_session() as session:
|
||||
author = None
|
||||
if slug:
|
||||
author = session.query(Author).filter(Author.slug == slug).first()
|
||||
author = session.query(Author).where(Author.slug == slug).first()
|
||||
if author:
|
||||
return int(author.id)
|
||||
if user:
|
||||
author = session.query(Author).filter(Author.id == user).first()
|
||||
author = session.query(Author).where(Author.id == user).first()
|
||||
if author:
|
||||
return int(author.id)
|
||||
except Exception as exc:
|
||||
@@ -598,8 +589,6 @@ def create_author(**kwargs) -> Author:
|
||||
author.name = kwargs.get("name") or kwargs.get("slug") # type: ignore[assignment] # если не указано # type: ignore[assignment]
|
||||
|
||||
with local_session() as session:
|
||||
from orm.community import Community, CommunityAuthor, CommunityFollower
|
||||
|
||||
session.add(author)
|
||||
session.flush() # Получаем ID автора
|
||||
|
||||
@@ -607,7 +596,7 @@ def create_author(**kwargs) -> Author:
|
||||
target_community_id = kwargs.get("community_id", 1) # По умолчанию основное сообщество
|
||||
|
||||
# Получаем сообщество для назначения дефолтных ролей
|
||||
community = session.query(Community).filter(Community.id == target_community_id).first()
|
||||
community = session.query(Community).where(Community.id == target_community_id).first()
|
||||
if community:
|
||||
default_roles = community.get_default_roles()
|
||||
|
||||
|
@@ -14,7 +14,7 @@ from services.schema import mutation, query
|
||||
|
||||
@query.field("load_shouts_bookmarked")
|
||||
@login_required
|
||||
def load_shouts_bookmarked(_: None, info, options):
|
||||
def load_shouts_bookmarked(_: None, info, options) -> list[Shout]:
|
||||
"""
|
||||
Load bookmarked shouts for the authenticated user.
|
||||
|
||||
@@ -33,14 +33,15 @@ def load_shouts_bookmarked(_: None, info, options):
|
||||
|
||||
q = query_with_stat(info)
|
||||
q = q.join(AuthorBookmark)
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Shout.id == AuthorBookmark.shout,
|
||||
AuthorBookmark.author == author_id,
|
||||
)
|
||||
)
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset)
|
||||
shouts = get_shouts_with_links(info, q, limit, offset)
|
||||
return shouts
|
||||
|
||||
|
||||
@mutation.field("toggle_bookmark_shout")
|
||||
@@ -61,15 +62,13 @@ def toggle_bookmark_shout(_: None, info, slug: str) -> CommonResult:
|
||||
raise GraphQLError(msg)
|
||||
|
||||
with local_session() as db:
|
||||
shout = db.query(Shout).filter(Shout.slug == slug).first()
|
||||
shout = db.query(Shout).where(Shout.slug == slug).first()
|
||||
if not shout:
|
||||
msg = "Shout not found"
|
||||
raise GraphQLError(msg)
|
||||
|
||||
existing_bookmark = (
|
||||
db.query(AuthorBookmark)
|
||||
.filter(AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id)
|
||||
.first()
|
||||
db.query(AuthorBookmark).where(AuthorBookmark.author == author_id, AuthorBookmark.shout == shout.id).first()
|
||||
)
|
||||
|
||||
if existing_bookmark:
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.invite import Invite, InviteStatus
|
||||
from orm.shout import Shout
|
||||
@@ -8,7 +10,7 @@ from services.schema import mutation
|
||||
|
||||
@mutation.field("accept_invite")
|
||||
@login_required
|
||||
async def accept_invite(_: None, info, invite_id: int):
|
||||
async def accept_invite(_: None, info, invite_id: int) -> dict[str, Any]:
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
if author_id:
|
||||
@@ -16,13 +18,13 @@ async def accept_invite(_: None, info, invite_id: int):
|
||||
# Check if the user exists
|
||||
with local_session() as session:
|
||||
# Check if the invite exists
|
||||
invite = session.query(Invite).filter(Invite.id == invite_id).first()
|
||||
invite = session.query(Invite).where(Invite.id == invite_id).first()
|
||||
if invite and invite.author_id is author_id and invite.status is InviteStatus.PENDING.value:
|
||||
# Add the user to the shout authors
|
||||
shout = session.query(Shout).filter(Shout.id == invite.shout_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == invite.shout_id).first()
|
||||
if shout:
|
||||
if author_id not in shout.authors:
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
if author:
|
||||
shout.authors.append(author)
|
||||
session.add(shout)
|
||||
@@ -32,12 +34,12 @@ async def accept_invite(_: None, info, invite_id: int):
|
||||
return {"error": "Shout not found"}
|
||||
return {"error": "Invalid invite or already accepted/rejected"}
|
||||
else:
|
||||
return {"error": "Unauthorized"}
|
||||
return {"error": "UnauthorizedError"}
|
||||
|
||||
|
||||
@mutation.field("reject_invite")
|
||||
@login_required
|
||||
async def reject_invite(_: None, info, invite_id: int):
|
||||
async def reject_invite(_: None, info, invite_id: int) -> dict[str, Any]:
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
@@ -46,7 +48,7 @@ async def reject_invite(_: None, info, invite_id: int):
|
||||
with local_session() as session:
|
||||
author_id = int(author_id)
|
||||
# Check if the invite exists
|
||||
invite = session.query(Invite).filter(Invite.id == invite_id).first()
|
||||
invite = session.query(Invite).where(Invite.id == invite_id).first()
|
||||
if invite and invite.author_id is author_id and invite.status is InviteStatus.PENDING.value:
|
||||
# Delete the invite
|
||||
session.delete(invite)
|
||||
@@ -58,7 +60,7 @@ async def reject_invite(_: None, info, invite_id: int):
|
||||
|
||||
@mutation.field("create_invite")
|
||||
@login_required
|
||||
async def create_invite(_: None, info, slug: str = "", author_id: int = 0):
|
||||
async def create_invite(_: None, info, slug: str = "", author_id: int = 0) -> dict[str, Any]:
|
||||
author_dict = info.context["author"]
|
||||
viewer_id = author_dict.get("id")
|
||||
roles = info.context.get("roles", [])
|
||||
@@ -68,13 +70,13 @@ async def create_invite(_: None, info, slug: str = "", author_id: int = 0):
|
||||
if author_id:
|
||||
# Check if the inviter is the owner of the shout
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
inviter = session.query(Author).filter(Author.id == viewer_id).first()
|
||||
shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
inviter = session.query(Author).where(Author.id == viewer_id).first()
|
||||
if inviter and shout and shout.authors and inviter.id is shout.created_by:
|
||||
# Check if an invite already exists
|
||||
existing_invite = (
|
||||
session.query(Invite)
|
||||
.filter(
|
||||
.where(
|
||||
Invite.inviter_id == inviter.id,
|
||||
Invite.author_id == author_id,
|
||||
Invite.shout_id == shout.id,
|
||||
@@ -103,16 +105,16 @@ async def create_invite(_: None, info, slug: str = "", author_id: int = 0):
|
||||
|
||||
@mutation.field("remove_author")
|
||||
@login_required
|
||||
async def remove_author(_: None, info, slug: str = "", author_id: int = 0):
|
||||
async def remove_author(_: None, info, slug: str = "", author_id: int = 0) -> dict[str, Any]:
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
roles = info.context.get("roles", [])
|
||||
if not viewer_id and not is_admin and "admin" not in roles and "editor" not in roles:
|
||||
return {"error": "Access denied"}
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
if author:
|
||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
# NOTE: owner should be first in a list
|
||||
if shout and author.id is shout.created_by:
|
||||
shout.authors = [author for author in shout.authors if author.id != author_id]
|
||||
@@ -123,16 +125,16 @@ async def remove_author(_: None, info, slug: str = "", author_id: int = 0):
|
||||
|
||||
@mutation.field("remove_invite")
|
||||
@login_required
|
||||
async def remove_invite(_: None, info, invite_id: int):
|
||||
async def remove_invite(_: None, info, invite_id: int) -> dict[str, Any]:
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
if isinstance(author_id, int):
|
||||
# Check if the user exists
|
||||
with local_session() as session:
|
||||
# Check if the invite exists
|
||||
invite = session.query(Invite).filter(Invite.id == invite_id).first()
|
||||
invite = session.query(Invite).where(Invite.id == invite_id).first()
|
||||
if isinstance(invite, Invite):
|
||||
shout = session.query(Shout).filter(Shout.id == invite.shout_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == invite.shout_id).first()
|
||||
if shout and shout.deleted_at is None and invite:
|
||||
if invite.inviter_id is author_id or author_id == shout.created_by:
|
||||
if invite.status is InviteStatus.PENDING.value:
|
||||
@@ -140,9 +142,9 @@ async def remove_invite(_: None, info, invite_id: int):
|
||||
session.delete(invite)
|
||||
session.commit()
|
||||
return {}
|
||||
return None
|
||||
return None
|
||||
return None
|
||||
return {"error": "Invite already accepted/rejected or deleted"}
|
||||
return {"error": "Access denied"}
|
||||
return {"error": "Shout not found"}
|
||||
return {"error": "Invalid invite or already accepted/rejected"}
|
||||
else:
|
||||
return {"error": "Author not found"}
|
||||
|
@@ -1,19 +1,20 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from auth.decorators import editor_or_admin_required
|
||||
from auth.orm import Author
|
||||
from orm.collection import Collection, ShoutCollection
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
from services.schema import mutation, query, type_collection
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@query.field("get_collections_all")
|
||||
async def get_collections_all(_: None, _info: GraphQLResolveInfo) -> list[Collection]:
|
||||
"""Получает все коллекции"""
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
with local_session() as session:
|
||||
# Загружаем коллекции с проверкой существования авторов
|
||||
collections = (
|
||||
@@ -23,7 +24,7 @@ async def get_collections_all(_: None, _info: GraphQLResolveInfo) -> list[Collec
|
||||
Author,
|
||||
Collection.created_by == Author.id, # INNER JOIN - исключает коллекции без авторов
|
||||
)
|
||||
.filter(
|
||||
.where(
|
||||
Collection.created_by.isnot(None), # Дополнительная проверка
|
||||
Author.id.isnot(None), # Проверяем что автор существует
|
||||
)
|
||||
@@ -41,8 +42,6 @@ async def get_collections_all(_: None, _info: GraphQLResolveInfo) -> list[Collec
|
||||
):
|
||||
valid_collections.append(collection)
|
||||
else:
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
logger.warning(f"Исключена коллекция {collection.id} ({collection.slug}) - проблемы с автором")
|
||||
|
||||
return valid_collections
|
||||
@@ -138,14 +137,23 @@ async def update_collection(_: None, info: GraphQLResolveInfo, collection_input:
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Находим коллекцию для обновления
|
||||
collection = session.query(Collection).filter(Collection.slug == slug).first()
|
||||
collection = session.query(Collection).where(Collection.slug == slug).first()
|
||||
if not collection:
|
||||
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 == 1, # Используем сообщество по умолчанию
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
user_roles = community_author.role_list if community_author else []
|
||||
|
||||
# Разрешаем редактирование если пользователь - создатель или имеет роль admin/editor
|
||||
if collection.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
|
||||
@@ -186,21 +194,30 @@ async def delete_collection(_: None, info: GraphQLResolveInfo, slug: str) -> dic
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Находим коллекцию для удаления
|
||||
collection = session.query(Collection).filter(Collection.slug == slug).first()
|
||||
collection = session.query(Collection).where(Collection.slug == slug).first()
|
||||
if not collection:
|
||||
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 == 1, # Используем сообщество по умолчанию
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
user_roles = community_author.role_list if community_author else []
|
||||
|
||||
# Разрешаем удаление если пользователь - создатель или имеет роль admin/editor
|
||||
if collection.created_by != author_id and "admin" not in user_roles and "editor" not in user_roles:
|
||||
return {"error": "Недостаточно прав для удаления этой коллекции"}
|
||||
|
||||
# Удаляем связи с публикациями
|
||||
session.query(ShoutCollection).filter(ShoutCollection.collection == collection.id).delete()
|
||||
session.query(ShoutCollection).where(ShoutCollection.collection == collection.id).delete()
|
||||
|
||||
# Удаляем коллекцию
|
||||
session.delete(collection)
|
||||
@@ -217,10 +234,8 @@ def resolve_collection_created_by(obj: Collection, *_: Any) -> Optional[Author]:
|
||||
if hasattr(obj, "created_by_author") and obj.created_by_author:
|
||||
return obj.created_by_author
|
||||
|
||||
author = session.query(Author).filter(Author.id == obj.created_by).first()
|
||||
author = session.query(Author).where(Author.id == obj.created_by).first()
|
||||
if not author:
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
logger.warning(f"Автор с ID {obj.created_by} не найден для коллекции {obj.id}")
|
||||
|
||||
return author
|
||||
@@ -230,5 +245,4 @@ def resolve_collection_created_by(obj: Collection, *_: Any) -> Optional[Author]:
|
||||
def resolve_collection_amount(obj: Collection, *_: Any) -> int:
|
||||
"""Резолвер для количества публикаций в коллекции"""
|
||||
with local_session() as session:
|
||||
count = session.query(ShoutCollection).filter(ShoutCollection.collection == obj.id).count()
|
||||
return count
|
||||
return session.query(ShoutCollection).where(ShoutCollection.collection == obj.id).count()
|
||||
|
@@ -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}
|
||||
|
@@ -96,7 +96,7 @@ async def load_drafts(_: None, info: GraphQLResolveInfo) -> dict[str, Any]:
|
||||
joinedload(Draft.topics),
|
||||
joinedload(Draft.authors),
|
||||
)
|
||||
.filter(Draft.authors.any(Author.id == author_id))
|
||||
.where(Draft.authors.any(Author.id == author_id))
|
||||
)
|
||||
drafts = drafts_query.all()
|
||||
|
||||
@@ -168,12 +168,41 @@ async def create_draft(_: None, info: GraphQLResolveInfo, draft_input: dict[str,
|
||||
# Добавляем текущее время создания и ID автора
|
||||
draft_input["created_at"] = int(time.time())
|
||||
draft_input["created_by"] = author_id
|
||||
draft = Draft(**draft_input)
|
||||
|
||||
# Исключаем поле shout из создания draft (оно добавляется только при публикации)
|
||||
draft_input.pop("shout", None)
|
||||
|
||||
# Создаем draft вручную, исключая проблемные поля
|
||||
draft = Draft()
|
||||
draft.created_at = draft_input["created_at"]
|
||||
draft.created_by = draft_input["created_by"]
|
||||
draft.community = draft_input.get("community", 1)
|
||||
draft.layout = draft_input.get("layout", "article")
|
||||
draft.title = draft_input.get("title", "")
|
||||
draft.body = draft_input.get("body", "")
|
||||
draft.lang = draft_input.get("lang", "ru")
|
||||
|
||||
# Опциональные поля
|
||||
if "slug" in draft_input:
|
||||
draft.slug = draft_input["slug"]
|
||||
if "subtitle" in draft_input:
|
||||
draft.subtitle = draft_input["subtitle"]
|
||||
if "lead" in draft_input:
|
||||
draft.lead = draft_input["lead"]
|
||||
if "cover" in draft_input:
|
||||
draft.cover = draft_input["cover"]
|
||||
if "cover_caption" in draft_input:
|
||||
draft.cover_caption = draft_input["cover_caption"]
|
||||
if "seo" in draft_input:
|
||||
draft.seo = draft_input["seo"]
|
||||
if "media" in draft_input:
|
||||
draft.media = draft_input["media"]
|
||||
|
||||
session.add(draft)
|
||||
session.flush()
|
||||
|
||||
# Добавляем создателя как автора
|
||||
da = DraftAuthor(shout=draft.id, author=author_id)
|
||||
da = DraftAuthor(draft=draft.id, author=author_id)
|
||||
session.add(da)
|
||||
|
||||
session.commit()
|
||||
@@ -222,7 +251,7 @@ async def update_draft(_: None, info: GraphQLResolveInfo, draft_id: int, draft_i
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
||||
draft = session.query(Draft).where(Draft.id == draft_id).first()
|
||||
if not draft:
|
||||
return {"error": "Draft not found"}
|
||||
|
||||
@@ -254,10 +283,10 @@ async def update_draft(_: None, info: GraphQLResolveInfo, draft_id: int, draft_i
|
||||
author_ids = filtered_input.pop("author_ids")
|
||||
if author_ids:
|
||||
# Очищаем текущие связи
|
||||
session.query(DraftAuthor).filter(DraftAuthor.shout == draft_id).delete()
|
||||
session.query(DraftAuthor).where(DraftAuthor.draft == draft_id).delete()
|
||||
# Добавляем новые связи
|
||||
for aid in author_ids:
|
||||
da = DraftAuthor(shout=draft_id, author=aid)
|
||||
da = DraftAuthor(draft=draft_id, author=aid)
|
||||
session.add(da)
|
||||
|
||||
# Обновляем связи с темами если переданы
|
||||
@@ -266,11 +295,11 @@ async def update_draft(_: None, info: GraphQLResolveInfo, draft_id: int, draft_i
|
||||
main_topic_id = filtered_input.pop("main_topic_id", None)
|
||||
if topic_ids:
|
||||
# Очищаем текущие связи
|
||||
session.query(DraftTopic).filter(DraftTopic.shout == draft_id).delete()
|
||||
session.query(DraftTopic).where(DraftTopic.draft == draft_id).delete()
|
||||
# Добавляем новые связи
|
||||
for tid in topic_ids:
|
||||
dt = DraftTopic(
|
||||
shout=draft_id,
|
||||
draft=draft_id,
|
||||
topic=tid,
|
||||
main=(tid == main_topic_id) if main_topic_id else False,
|
||||
)
|
||||
@@ -320,10 +349,10 @@ async def delete_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dict
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
with local_session() as session:
|
||||
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
||||
draft = session.query(Draft).where(Draft.id == draft_id).first()
|
||||
if not draft:
|
||||
return {"error": "Draft not found"}
|
||||
if author_id != draft.created_by and draft.authors.filter(Author.id == author_id).count() == 0:
|
||||
if author_id != draft.created_by and draft.authors.where(Author.id == author_id).count() == 0:
|
||||
return {"error": "You are not allowed to delete this draft"}
|
||||
session.delete(draft)
|
||||
session.commit()
|
||||
@@ -386,8 +415,8 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic
|
||||
# Загружаем черновик со всеми связями
|
||||
draft = (
|
||||
session.query(Draft)
|
||||
.options(joinedload(Draft.topics), joinedload(Draft.authors), joinedload(Draft.publication))
|
||||
.filter(Draft.id == draft_id)
|
||||
.options(joinedload(Draft.topics), joinedload(Draft.authors))
|
||||
.where(Draft.id == draft_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -401,7 +430,8 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic
|
||||
return {"error": f"Cannot publish draft: {error}"}
|
||||
|
||||
# Проверяем, есть ли уже публикация для этого черновика
|
||||
if draft.publication:
|
||||
shout = None
|
||||
if hasattr(draft, "publication") and draft.publication:
|
||||
shout = draft.publication
|
||||
# Обновляем существующую публикацию
|
||||
if hasattr(draft, "body"):
|
||||
@@ -428,14 +458,14 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic
|
||||
# Создаем новую публикацию
|
||||
shout = create_shout_from_draft(session, draft, author_id)
|
||||
now = int(time.time())
|
||||
shout.created_at = now
|
||||
shout.published_at = now
|
||||
shout.created_at = int(now)
|
||||
shout.published_at = int(now)
|
||||
session.add(shout)
|
||||
session.flush() # Получаем ID нового шаута
|
||||
|
||||
# Очищаем существующие связи
|
||||
session.query(ShoutAuthor).filter(ShoutAuthor.shout == shout.id).delete()
|
||||
session.query(ShoutTopic).filter(ShoutTopic.shout == shout.id).delete()
|
||||
session.query(ShoutAuthor).where(ShoutAuthor.shout == shout.id).delete()
|
||||
session.query(ShoutTopic).where(ShoutTopic.shout == shout.id).delete()
|
||||
|
||||
# Добавляем авторов
|
||||
for author in draft.authors or []:
|
||||
@@ -457,7 +487,7 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
|
||||
# Уведомляем о публикации
|
||||
await notify_shout(shout.id)
|
||||
await notify_shout(shout.dict(), "published")
|
||||
|
||||
# Обновляем поисковый индекс
|
||||
await search_service.perform_index(shout)
|
||||
@@ -495,8 +525,8 @@ async def unpublish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> d
|
||||
# Загружаем черновик со связанной публикацией
|
||||
draft = (
|
||||
session.query(Draft)
|
||||
.options(joinedload(Draft.publication), joinedload(Draft.authors), joinedload(Draft.topics))
|
||||
.filter(Draft.id == draft_id)
|
||||
.options(joinedload(Draft.authors), joinedload(Draft.topics))
|
||||
.where(Draft.id == draft_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -504,11 +534,12 @@ async def unpublish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> d
|
||||
return {"error": "Draft not found"}
|
||||
|
||||
# Проверяем, есть ли публикация
|
||||
if not draft.publication:
|
||||
shout = None
|
||||
if hasattr(draft, "publication") and draft.publication:
|
||||
shout = draft.publication
|
||||
else:
|
||||
return {"error": "This draft is not published yet"}
|
||||
|
||||
shout = draft.publication
|
||||
|
||||
# Снимаем с публикации
|
||||
shout.published_at = None
|
||||
shout.updated_at = int(time.time())
|
||||
|
@@ -8,12 +8,6 @@ from sqlalchemy.orm import joinedload
|
||||
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 orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic
|
||||
from resolvers.follower import follow
|
||||
@@ -28,7 +22,7 @@ from utils.extract_text import extract_text
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
async def cache_by_id(entity, entity_id: int, cache_method):
|
||||
async def cache_by_id(entity, entity_id: int, cache_method) -> None:
|
||||
"""Cache an entity by its ID using the provided cache method.
|
||||
|
||||
Args:
|
||||
@@ -46,20 +40,20 @@ async def cache_by_id(entity, entity_id: int, cache_method):
|
||||
... assert 'name' in author
|
||||
... return author
|
||||
"""
|
||||
caching_query = select(entity).filter(entity.id == entity_id)
|
||||
caching_query = select(entity).where(entity.id == entity_id)
|
||||
result = get_with_stat(caching_query)
|
||||
if not result or not result[0]:
|
||||
logger.warning(f"{entity.__name__} with id {entity_id} not found")
|
||||
return None
|
||||
x = result[0]
|
||||
d = x.dict() # convert object to dictionary
|
||||
cache_method(d)
|
||||
await cache_method(d)
|
||||
return d
|
||||
|
||||
|
||||
@query.field("get_my_shout")
|
||||
@login_required
|
||||
async def get_my_shout(_: None, info, shout_id: int):
|
||||
async def get_my_shout(_: None, info, shout_id: int) -> dict[str, Any]:
|
||||
"""Get a shout by ID if the requesting user has permission to view it.
|
||||
|
||||
DEPRECATED: use `load_drafts` instead
|
||||
@@ -97,9 +91,9 @@ async def get_my_shout(_: None, info, shout_id: int):
|
||||
with local_session() as session:
|
||||
shout = (
|
||||
session.query(Shout)
|
||||
.filter(Shout.id == shout_id)
|
||||
.where(Shout.id == shout_id)
|
||||
.options(joinedload(Shout.authors), joinedload(Shout.topics))
|
||||
.filter(Shout.deleted_at.is_(None))
|
||||
.where(Shout.deleted_at.is_(None))
|
||||
.first()
|
||||
)
|
||||
if not shout:
|
||||
@@ -147,8 +141,8 @@ async def get_shouts_drafts(_: None, info: GraphQLResolveInfo) -> list[dict]:
|
||||
q = (
|
||||
select(Shout)
|
||||
.options(joinedload(Shout.authors), joinedload(Shout.topics))
|
||||
.filter(and_(Shout.deleted_at.is_(None), Shout.created_by == int(author_id)))
|
||||
.filter(Shout.published_at.is_(None))
|
||||
.where(and_(Shout.deleted_at.is_(None), Shout.created_by == int(author_id)))
|
||||
.where(Shout.published_at.is_(None))
|
||||
.order_by(desc(coalesce(Shout.updated_at, Shout.created_at)))
|
||||
.group_by(Shout.id)
|
||||
)
|
||||
@@ -197,12 +191,12 @@ async def create_shout(_: None, info: GraphQLResolveInfo, inp: dict) -> dict:
|
||||
|
||||
# Проверяем уникальность slug
|
||||
logger.debug(f"Checking for existing slug: {slug}")
|
||||
same_slug_shout = session.query(Shout).filter(Shout.slug == new_shout.slug).first()
|
||||
same_slug_shout = session.query(Shout).where(Shout.slug == new_shout.slug).first()
|
||||
c = 1
|
||||
while same_slug_shout is not None:
|
||||
logger.debug(f"Found duplicate slug, trying iteration {c}")
|
||||
new_shout.slug = f"{slug}-{c}" # type: ignore[assignment]
|
||||
same_slug_shout = session.query(Shout).filter(Shout.slug == new_shout.slug).first()
|
||||
same_slug_shout = session.query(Shout).where(Shout.slug == new_shout.slug).first()
|
||||
c += 1
|
||||
|
||||
try:
|
||||
@@ -250,7 +244,7 @@ async def create_shout(_: None, info: GraphQLResolveInfo, inp: dict) -> dict:
|
||||
return {"error": f"Error in final commit: {e!s}"}
|
||||
|
||||
# Получаем созданную публикацию
|
||||
shout = session.query(Shout).filter(Shout.id == new_shout.id).first()
|
||||
shout = session.query(Shout).where(Shout.id == new_shout.id).first()
|
||||
|
||||
if shout:
|
||||
# Подписываем автора
|
||||
@@ -280,7 +274,7 @@ def patch_main_topic(session: Any, main_topic_slug: str, shout: Any) -> None:
|
||||
with session.begin():
|
||||
# Получаем текущий главный топик
|
||||
old_main = (
|
||||
session.query(ShoutTopic).filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.main.is_(True))).first()
|
||||
session.query(ShoutTopic).where(and_(ShoutTopic.shout == shout.id, ShoutTopic.main.is_(True))).first()
|
||||
)
|
||||
if old_main:
|
||||
logger.info(f"Found current main topic: {old_main.topic.slug}")
|
||||
@@ -288,7 +282,7 @@ def patch_main_topic(session: Any, main_topic_slug: str, shout: Any) -> None:
|
||||
logger.info("No current main topic found")
|
||||
|
||||
# Находим новый главный топик
|
||||
main_topic = session.query(Topic).filter(Topic.slug == main_topic_slug).first()
|
||||
main_topic = session.query(Topic).where(Topic.slug == main_topic_slug).first()
|
||||
if not main_topic:
|
||||
logger.error(f"Main topic with slug '{main_topic_slug}' not found")
|
||||
return
|
||||
@@ -298,7 +292,7 @@ def patch_main_topic(session: Any, main_topic_slug: str, shout: Any) -> None:
|
||||
# Находим связь с новым главным топиком
|
||||
new_main = (
|
||||
session.query(ShoutTopic)
|
||||
.filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.topic == main_topic.id))
|
||||
.where(and_(ShoutTopic.shout == shout.id, ShoutTopic.topic == main_topic.id))
|
||||
.first()
|
||||
)
|
||||
logger.debug(f"Found new main topic relation: {new_main is not None}")
|
||||
@@ -357,7 +351,7 @@ def patch_topics(session: Any, shout: Any, topics_input: list[Any]) -> None:
|
||||
session.flush()
|
||||
|
||||
# Получаем текущие связи
|
||||
current_links = session.query(ShoutTopic).filter(ShoutTopic.shout == shout.id).all()
|
||||
current_links = session.query(ShoutTopic).where(ShoutTopic.shout == shout.id).all()
|
||||
logger.info(f"Current topic links: {[{t.topic: t.main} for t in current_links]}")
|
||||
|
||||
# Удаляем старые связи
|
||||
@@ -391,13 +385,21 @@ def patch_topics(session: Any, shout: Any, topics_input: list[Any]) -> None:
|
||||
async def update_shout(
|
||||
_: None, info: GraphQLResolveInfo, shout_id: int, shout_input: dict | None = None, *, publish: bool = False
|
||||
) -> CommonResult:
|
||||
# Поздние импорты для избежания циклических зависимостей
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cache_topic,
|
||||
invalidate_shout_related_cache,
|
||||
invalidate_shouts_cache,
|
||||
)
|
||||
|
||||
"""Update an existing shout with optional publishing"""
|
||||
logger.info(f"update_shout called with shout_id={shout_id}, publish={publish}")
|
||||
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
if not author_id:
|
||||
logger.error("Unauthorized update attempt")
|
||||
logger.error("UnauthorizedError update attempt")
|
||||
return CommonResult(error="unauthorized", shout=None)
|
||||
|
||||
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
|
||||
@@ -415,7 +417,7 @@ async def update_shout(
|
||||
shout_by_id = (
|
||||
session.query(Shout)
|
||||
.options(joinedload(Shout.topics).joinedload(ShoutTopic.topic), joinedload(Shout.authors))
|
||||
.filter(Shout.id == shout_id)
|
||||
.where(Shout.id == shout_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -434,12 +436,12 @@ async def update_shout(
|
||||
logger.info(f"Current topics for shout#{shout_id}: {current_topics}")
|
||||
|
||||
if slug != shout_by_id.slug:
|
||||
same_slug_shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
same_slug_shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
c = 1
|
||||
while same_slug_shout is not None:
|
||||
c += 1
|
||||
same_slug_shout.slug = f"{slug}-{c}" # type: ignore[assignment]
|
||||
same_slug_shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
same_slug_shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
shout_input["slug"] = slug
|
||||
logger.info(f"shout#{shout_id} slug patched")
|
||||
|
||||
@@ -481,7 +483,7 @@ async def update_shout(
|
||||
logger.info(f"Checking author link for shout#{shout_id} and author#{author_id}")
|
||||
author_link = (
|
||||
session.query(ShoutAuthor)
|
||||
.filter(and_(ShoutAuthor.shout == shout_id, ShoutAuthor.author == author_id))
|
||||
.where(and_(ShoutAuthor.shout == shout_id, ShoutAuthor.author == author_id))
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -570,6 +572,11 @@ async def update_shout(
|
||||
# @mutation.field("delete_shout")
|
||||
# @login_required
|
||||
async def delete_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> CommonResult:
|
||||
# Поздние импорты для избежания циклических зависимостей
|
||||
from cache.cache import (
|
||||
invalidate_shout_related_cache,
|
||||
)
|
||||
|
||||
"""Delete a shout (mark as deleted)"""
|
||||
author_dict = info.context.get("author", {})
|
||||
if not author_dict:
|
||||
@@ -579,27 +586,26 @@ async def delete_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> Comm
|
||||
roles = info.context.get("roles", [])
|
||||
|
||||
with local_session() as session:
|
||||
if author_id:
|
||||
if shout_id:
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
if shout:
|
||||
# Check if user has permission to delete
|
||||
if any(x.id == author_id for x in shout.authors) or "editor" in roles:
|
||||
# Use setattr to avoid MyPy complaints about Column assignment
|
||||
shout.deleted_at = int(time.time()) # type: ignore[assignment]
|
||||
session.add(shout)
|
||||
session.commit()
|
||||
if author_id and shout_id:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if shout:
|
||||
# Check if user has permission to delete
|
||||
if any(x.id == author_id for x in shout.authors) or "editor" in roles:
|
||||
# Use setattr to avoid MyPy complaints about Column assignment
|
||||
shout.deleted_at = int(time.time()) # type: ignore[assignment]
|
||||
session.add(shout)
|
||||
session.commit()
|
||||
|
||||
# Get shout data for notification
|
||||
shout_dict = shout.dict()
|
||||
# Get shout data for notification
|
||||
shout_dict = shout.dict()
|
||||
|
||||
# Invalidate cache
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
# Invalidate cache
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
|
||||
# Notify about deletion
|
||||
await notify_shout(shout_dict, "delete")
|
||||
return CommonResult(error=None, shout=shout)
|
||||
return CommonResult(error="access denied", shout=None)
|
||||
# Notify about deletion
|
||||
await notify_shout(shout_dict, "delete")
|
||||
return CommonResult(error=None, shout=shout)
|
||||
return CommonResult(error="access denied", shout=None)
|
||||
return CommonResult(error="shout not found", shout=None)
|
||||
|
||||
|
||||
@@ -661,6 +667,12 @@ async def unpublish_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> C
|
||||
"""
|
||||
Unpublish a shout by setting published_at to NULL
|
||||
"""
|
||||
# Поздние импорты для избежания циклических зависимостей
|
||||
from cache.cache import (
|
||||
invalidate_shout_related_cache,
|
||||
invalidate_shouts_cache,
|
||||
)
|
||||
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
roles = info.context.get("roles", [])
|
||||
@@ -671,7 +683,7 @@ async def unpublish_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> C
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Получаем шаут с авторами
|
||||
shout = session.query(Shout).options(joinedload(Shout.authors)).filter(Shout.id == shout_id).first()
|
||||
shout = session.query(Shout).options(joinedload(Shout.authors)).where(Shout.id == shout_id).first()
|
||||
|
||||
if not shout:
|
||||
return CommonResult(error="Shout not found", shout=None)
|
||||
@@ -703,7 +715,6 @@ async def unpublish_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> C
|
||||
|
||||
# Получаем обновленные данные шаута
|
||||
session.refresh(shout)
|
||||
shout_dict = shout.dict()
|
||||
|
||||
logger.info(f"Shout {shout_id} unpublished successfully")
|
||||
return CommonResult(error=None, shout=shout)
|
||||
|
@@ -1,5 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import and_, select
|
||||
from sqlalchemy import Select, and_, select
|
||||
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic
|
||||
@@ -30,7 +32,7 @@ async def load_shouts_coauthored(_: None, info: GraphQLResolveInfo, options: dic
|
||||
if not author_id:
|
||||
return []
|
||||
q = query_with_stat(info)
|
||||
q = q.filter(Shout.authors.any(id=author_id))
|
||||
q = q.where(Shout.authors.any(id=author_id))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
|
||||
@@ -54,7 +56,7 @@ async def load_shouts_discussed(_: None, info: GraphQLResolveInfo, options: dict
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
|
||||
|
||||
def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict) -> list[Shout]:
|
||||
def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict[str, Any]) -> list[Shout]:
|
||||
"""
|
||||
Загружает публикации, на которые подписан автор.
|
||||
|
||||
@@ -68,9 +70,11 @@ def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict
|
||||
:return: Список публикаций.
|
||||
"""
|
||||
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_authors: Select = select(AuthorFollower.author).where(AuthorFollower.follower == follower_id)
|
||||
reader_followed_topics: Select = select(TopicFollower.topic).where(TopicFollower.follower == follower_id)
|
||||
reader_followed_shouts: Select = select(ShoutReactionsFollower.shout).where(
|
||||
ShoutReactionsFollower.follower == follower_id
|
||||
)
|
||||
followed_subquery = (
|
||||
select(Shout.id)
|
||||
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
||||
@@ -82,7 +86,7 @@ def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict
|
||||
)
|
||||
.scalar_subquery()
|
||||
)
|
||||
q = q.filter(Shout.id.in_(followed_subquery))
|
||||
q = q.where(Shout.id.in_(followed_subquery))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
|
||||
@@ -98,7 +102,7 @@ async def load_shouts_followed_by(_: None, info: GraphQLResolveInfo, slug: str,
|
||||
:return: Список публикаций.
|
||||
"""
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.slug == slug).first()
|
||||
author = session.query(Author).where(Author.slug == slug).first()
|
||||
if author:
|
||||
follower_id = author.dict()["id"]
|
||||
return shouts_by_follower(info, follower_id, options)
|
||||
@@ -120,7 +124,7 @@ async def load_shouts_feed(_: None, info: GraphQLResolveInfo, options: dict) ->
|
||||
|
||||
|
||||
@query.field("load_shouts_authored_by")
|
||||
async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str, options: dict) -> list[Shout]:
|
||||
async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str, options: dict[str, Any]) -> list[Shout]:
|
||||
"""
|
||||
Загружает публикации, написанные автором по slug.
|
||||
|
||||
@@ -130,16 +134,16 @@ async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str,
|
||||
:return: Список публикаций.
|
||||
"""
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.slug == slug).first()
|
||||
author = session.query(Author).where(Author.slug == slug).first()
|
||||
if author:
|
||||
try:
|
||||
author_id: int = author.dict()["id"]
|
||||
q = (
|
||||
q: Select = (
|
||||
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).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.filter(Shout.authors.any(id=author_id))
|
||||
q = q.where(Shout.authors.any(id=author_id))
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
except Exception as error:
|
||||
@@ -148,7 +152,7 @@ async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str,
|
||||
|
||||
|
||||
@query.field("load_shouts_with_topic")
|
||||
async def load_shouts_with_topic(_: None, info: GraphQLResolveInfo, slug: str, options: dict) -> list[Shout]:
|
||||
async def load_shouts_with_topic(_: None, info: GraphQLResolveInfo, slug: str, options: dict[str, Any]) -> list[Shout]:
|
||||
"""
|
||||
Загружает публикации, связанные с темой по slug.
|
||||
|
||||
@@ -158,16 +162,16 @@ async def load_shouts_with_topic(_: None, info: GraphQLResolveInfo, slug: str, o
|
||||
:return: Список публикаций.
|
||||
"""
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||||
topic = session.query(Topic).where(Topic.slug == slug).first()
|
||||
if topic:
|
||||
try:
|
||||
topic_id: int = topic.dict()["id"]
|
||||
q = (
|
||||
q: Select = (
|
||||
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).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.filter(Shout.topics.any(id=topic_id))
|
||||
q = q.where(Shout.topics.any(id=topic_id))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
except Exception as error:
|
||||
|
@@ -1,20 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
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 orm.community import Community, CommunityFollower
|
||||
from orm.shout import Shout, ShoutReactionsFollower
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
from services.notify import notify_follower
|
||||
@@ -25,18 +19,31 @@ from utils.logger import root_logger as logger
|
||||
|
||||
@mutation.field("follow")
|
||||
@login_required
|
||||
async def follow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int = 0) -> dict:
|
||||
async def follow(
|
||||
_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None
|
||||
) -> dict[str, Any]:
|
||||
logger.debug("Начало выполнения функции 'follow'")
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
if not viewer_id:
|
||||
return {"error": "Access denied"}
|
||||
follower_dict = info.context.get("author") or {}
|
||||
logger.debug(f"follower: {follower_dict}")
|
||||
|
||||
if not viewer_id or not follower_dict:
|
||||
return {"error": "Access denied"}
|
||||
logger.warning("Неавторизованный доступ при попытке подписаться")
|
||||
return {"error": "UnauthorizedError"}
|
||||
|
||||
follower_id = follower_dict.get("id")
|
||||
logger.debug(f"follower_id: {follower_id}")
|
||||
|
||||
# Поздние импорты для избежания циклических зависимостей
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cache_topic,
|
||||
get_cached_follower_authors,
|
||||
get_cached_follower_topics,
|
||||
)
|
||||
|
||||
entity_classes = {
|
||||
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
|
||||
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
|
||||
@@ -50,33 +57,42 @@ async def follow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "", e
|
||||
|
||||
entity_class, follower_class, get_cached_follows_method, cache_method = entity_classes[what]
|
||||
entity_type = what.lower()
|
||||
entity_dict = None
|
||||
follows = []
|
||||
error = None
|
||||
follows: list[dict[str, Any]] = []
|
||||
error: str | None = None
|
||||
|
||||
try:
|
||||
logger.debug("Попытка получить сущность из базы данных")
|
||||
with local_session() as session:
|
||||
entity_query = select(entity_class).filter(entity_class.slug == slug)
|
||||
entities = get_with_stat(entity_query)
|
||||
[entity] = entities
|
||||
# Используем query для получения сущности
|
||||
entity_query = session.query(entity_class)
|
||||
|
||||
# Проверяем наличие slug перед фильтрацией
|
||||
if hasattr(entity_class, "slug"):
|
||||
entity_query = entity_query.where(entity_class.slug == slug)
|
||||
|
||||
entity = entity_query.first()
|
||||
|
||||
if not entity:
|
||||
logger.warning(f"{what.lower()} не найден по slug: {slug}")
|
||||
return {"error": f"{what.lower()} not found"}
|
||||
if not entity_id and entity:
|
||||
entity_id = entity.id
|
||||
|
||||
# Получаем ID сущности
|
||||
if entity_id is None:
|
||||
entity_id = getattr(entity, "id", None)
|
||||
|
||||
if not entity_id:
|
||||
logger.warning(f"Не удалось получить ID для {what.lower()}")
|
||||
return {"error": f"Cannot get ID for {what.lower()}"}
|
||||
|
||||
# Если это автор, учитываем фильтрацию данных
|
||||
entity_dict = entity.dict(True) if what == "AUTHOR" else entity.dict()
|
||||
entity_dict = entity.dict() if hasattr(entity, "dict") else {}
|
||||
|
||||
logger.debug(f"entity_id: {entity_id}, entity_dict: {entity_dict}")
|
||||
|
||||
if entity_id:
|
||||
logger.debug("Проверка существующей подписки")
|
||||
with local_session() as session:
|
||||
if entity_id is not None and isinstance(entity_id, int):
|
||||
existing_sub = (
|
||||
session.query(follower_class)
|
||||
.filter(
|
||||
.where(
|
||||
follower_class.follower == follower_id, # type: ignore[attr-defined]
|
||||
getattr(follower_class, entity_type) == entity_id, # type: ignore[attr-defined]
|
||||
)
|
||||
@@ -123,7 +139,7 @@ async def follow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "", e
|
||||
if hasattr(temp_author, key):
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
follows_filtered.append(temp_author.dict(False))
|
||||
follows_filtered.append(temp_author.dict())
|
||||
|
||||
follows = follows_filtered
|
||||
else:
|
||||
@@ -131,17 +147,18 @@ async def follow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "", e
|
||||
|
||||
logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов")
|
||||
|
||||
return {f"{entity_type}s": follows, "error": error}
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception("Произошла ошибка в функции 'follow'")
|
||||
return {"error": str(exc)}
|
||||
|
||||
logger.debug(f"Функция 'follow' завершена: {entity_type}s={len(follows)}, error={error}")
|
||||
return {f"{entity_type}s": follows, "error": error}
|
||||
|
||||
|
||||
@mutation.field("unfollow")
|
||||
@login_required
|
||||
async def unfollow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int = 0) -> dict:
|
||||
async def unfollow(
|
||||
_: None, info: GraphQLResolveInfo, what: str, slug: str = "", entity_id: int | None = None
|
||||
) -> dict[str, Any]:
|
||||
logger.debug("Начало выполнения функции 'unfollow'")
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
if not viewer_id:
|
||||
@@ -151,11 +168,19 @@ async def unfollow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "",
|
||||
|
||||
if not viewer_id or not follower_dict:
|
||||
logger.warning("Неавторизованный доступ при попытке отписаться")
|
||||
return {"error": "Unauthorized"}
|
||||
return {"error": "UnauthorizedError"}
|
||||
|
||||
follower_id = follower_dict.get("id")
|
||||
logger.debug(f"follower_id: {follower_id}")
|
||||
|
||||
# Поздние импорты для избежания циклических зависимостей
|
||||
from cache.cache import (
|
||||
cache_author,
|
||||
cache_topic,
|
||||
get_cached_follower_authors,
|
||||
get_cached_follower_topics,
|
||||
)
|
||||
|
||||
entity_classes = {
|
||||
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
|
||||
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
|
||||
@@ -169,24 +194,32 @@ async def unfollow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "",
|
||||
|
||||
entity_class, follower_class, get_cached_follows_method, cache_method = entity_classes[what]
|
||||
entity_type = what.lower()
|
||||
follows = []
|
||||
error = None
|
||||
follows: list[dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
logger.debug("Попытка получить сущность из базы данных")
|
||||
with local_session() as session:
|
||||
entity = session.query(entity_class).filter(entity_class.slug == slug).first()
|
||||
# Используем query для получения сущности
|
||||
entity_query = session.query(entity_class)
|
||||
if hasattr(entity_class, "slug"):
|
||||
entity_query = entity_query.where(entity_class.slug == slug)
|
||||
|
||||
entity = entity_query.first()
|
||||
logger.debug(f"Полученная сущность: {entity}")
|
||||
if not entity:
|
||||
logger.warning(f"{what.lower()} не найден по slug: {slug}")
|
||||
return {"error": f"{what.lower()} not found"}
|
||||
if entity and not entity_id:
|
||||
entity_id = int(entity.id) # Convert Column to int
|
||||
logger.debug(f"entity_id: {entity_id}")
|
||||
|
||||
if not entity_id:
|
||||
entity_id = getattr(entity, "id", None)
|
||||
if not entity_id:
|
||||
logger.warning(f"Не удалось получить ID для {what.lower()}")
|
||||
return {"error": f"Cannot get ID for {what.lower()}"}
|
||||
|
||||
logger.debug(f"entity_id: {entity_id}")
|
||||
sub = (
|
||||
session.query(follower_class)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
follower_class.follower == follower_id, # type: ignore[attr-defined]
|
||||
getattr(follower_class, entity_type) == entity_id, # type: ignore[attr-defined]
|
||||
@@ -194,105 +227,75 @@ async def unfollow(_: None, info: GraphQLResolveInfo, what: str, slug: str = "",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
logger.warning(f"Подписка не найдена для {what.lower()} с ID {entity_id}")
|
||||
return {"error": "Not following"}
|
||||
|
||||
logger.debug(f"Найдена подписка для удаления: {sub}")
|
||||
if sub:
|
||||
session.delete(sub)
|
||||
session.commit()
|
||||
logger.info(f"Пользователь {follower_id} отписался от {what.lower()} с ID {entity_id}")
|
||||
session.delete(sub)
|
||||
session.commit()
|
||||
logger.info(f"Пользователь {follower_id} отписался от {what.lower()} с ID {entity_id}")
|
||||
|
||||
# Инвалидируем кэш подписок пользователя после успешной отписки
|
||||
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
|
||||
await redis.execute("DEL", cache_key_pattern)
|
||||
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
|
||||
# Инвалидируем кэш подписок пользователя
|
||||
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
|
||||
await redis.execute("DEL", cache_key_pattern)
|
||||
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
|
||||
|
||||
if cache_method:
|
||||
logger.debug("Обновление кэша после отписки")
|
||||
# Если это автор, кэшируем полную версию
|
||||
if what == "AUTHOR":
|
||||
await cache_method(entity.dict(True))
|
||||
else:
|
||||
await cache_method(entity.dict())
|
||||
|
||||
if what == "AUTHOR":
|
||||
logger.debug("Отправка уведомления автору об отписке")
|
||||
if isinstance(follower_dict, dict) and isinstance(entity_id, int):
|
||||
await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow")
|
||||
if get_cached_follows_method and isinstance(follower_id, int):
|
||||
logger.debug("Получение актуального списка подписок из кэша")
|
||||
follows = await get_cached_follows_method(follower_id)
|
||||
logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов")
|
||||
else:
|
||||
# Подписка не найдена, но это не критическая ошибка
|
||||
logger.warning(f"Подписка не найдена: follower_id={follower_id}, {entity_type}_id={entity_id}")
|
||||
error = "following was not found"
|
||||
follows = []
|
||||
|
||||
# Всегда получаем актуальный список подписок для возврата клиенту
|
||||
if get_cached_follows_method and isinstance(follower_id, int):
|
||||
logger.debug("Получение актуального списка подписок из кэша")
|
||||
existing_follows = await get_cached_follows_method(follower_id)
|
||||
if what == "AUTHOR" and isinstance(follower_dict, dict):
|
||||
await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow")
|
||||
|
||||
# Если это авторы, получаем безопасную версию
|
||||
if what == "AUTHOR":
|
||||
follows_filtered = []
|
||||
|
||||
for author_data in existing_follows:
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in author_data.items():
|
||||
if hasattr(temp_author, key):
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
follows_filtered.append(temp_author.dict(False))
|
||||
|
||||
follows = follows_filtered
|
||||
else:
|
||||
follows = existing_follows
|
||||
|
||||
logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов")
|
||||
return {f"{entity_type}s": follows, "error": None}
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception("Произошла ошибка в функции 'unfollow'")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return {"error": str(exc)}
|
||||
|
||||
logger.debug(f"Функция 'unfollow' завершена: {entity_type}s={len(follows)}, error={error}")
|
||||
return {f"{entity_type}s": follows, "error": error}
|
||||
|
||||
|
||||
@query.field("get_shout_followers")
|
||||
def get_shout_followers(_: None, _info: GraphQLResolveInfo, slug: str = "", shout_id: int | None = None) -> list[dict]:
|
||||
logger.debug("Начало выполнения функции 'get_shout_followers'")
|
||||
followers = []
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = None
|
||||
if slug:
|
||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
logger.debug(f"Найден shout по slug: {slug} -> {shout}")
|
||||
elif shout_id:
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
logger.debug(f"Найден shout по ID: {shout_id} -> {shout}")
|
||||
def get_shout_followers(
|
||||
_: None, _info: GraphQLResolveInfo, slug: str = "", shout_id: int | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Получает список подписчиков для шаута по slug или ID
|
||||
|
||||
if shout:
|
||||
shout_id = int(shout.id) # Convert Column to int
|
||||
logger.debug(f"shout_id для получения подписчиков: {shout_id}")
|
||||
Args:
|
||||
_: GraphQL root
|
||||
_info: GraphQL context info
|
||||
slug: Slug шаута (опционально)
|
||||
shout_id: ID шаута (опционально)
|
||||
|
||||
# Получение подписчиков из таблицы ShoutReactionsFollower
|
||||
shout_followers = (
|
||||
session.query(Author)
|
||||
.join(ShoutReactionsFollower, Author.id == ShoutReactionsFollower.follower)
|
||||
.filter(ShoutReactionsFollower.shout == shout_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Convert Author objects to dicts
|
||||
followers = [author.dict() for author in shout_followers]
|
||||
logger.debug(f"Найдено {len(followers)} подписчиков для shout {shout_id}")
|
||||
|
||||
except Exception as _exc:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.exception("Произошла ошибка в функции 'get_shout_followers'")
|
||||
Returns:
|
||||
Список подписчиков шаута
|
||||
"""
|
||||
if not slug and not shout_id:
|
||||
return []
|
||||
|
||||
# logger.debug(f"Функция 'get_shout_followers' завершена с {len(followers)} подписчиками")
|
||||
return followers
|
||||
with local_session() as session:
|
||||
# Если slug не указан, ищем шаут по ID
|
||||
if not slug and shout_id is not None:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
else:
|
||||
# Ищем шаут по slug
|
||||
shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
|
||||
if not shout:
|
||||
return []
|
||||
|
||||
# Получаем подписчиков шаута
|
||||
followers_query = (
|
||||
session.query(Author)
|
||||
.join(ShoutReactionsFollower, Author.id == ShoutReactionsFollower.follower)
|
||||
.where(ShoutReactionsFollower.shout == shout.id)
|
||||
)
|
||||
|
||||
followers = followers_query.all()
|
||||
|
||||
# Возвращаем безопасную версию данных
|
||||
return [follower.dict() for follower in followers]
|
||||
|
@@ -32,13 +32,13 @@ def query_notifications(author_id: int, after: int = 0) -> tuple[int, int, list[
|
||||
),
|
||||
)
|
||||
if after:
|
||||
q = q.filter(Notification.created_at > after)
|
||||
q = q.where(Notification.created_at > after)
|
||||
q = q.group_by(NotificationSeen.notification, Notification.created_at)
|
||||
|
||||
with local_session() as session:
|
||||
total = (
|
||||
session.query(Notification)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Notification.action == NotificationAction.CREATE.value,
|
||||
Notification.created_at > after,
|
||||
@@ -49,7 +49,7 @@ def query_notifications(author_id: int, after: int = 0) -> tuple[int, int, list[
|
||||
|
||||
unread = (
|
||||
session.query(Notification)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Notification.action == NotificationAction.CREATE.value,
|
||||
Notification.created_at > after,
|
||||
@@ -131,8 +131,8 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
|
||||
author_id = shout.get("created_by")
|
||||
thread_id = f"shout-{shout_id}"
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if author and shout:
|
||||
author_dict = author.dict()
|
||||
shout_dict = shout.dict()
|
||||
@@ -155,8 +155,8 @@ def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, o
|
||||
author_id = reaction.get("created_by", 0)
|
||||
if shout_id and author_id:
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if shout and author:
|
||||
author_dict = author.dict()
|
||||
shout_dict = shout.dict()
|
||||
@@ -260,7 +260,7 @@ async def notifications_seen_after(_: None, info: GraphQLResolveInfo, after: int
|
||||
author_id = info.context.get("author", {}).get("id")
|
||||
if author_id:
|
||||
with local_session() as session:
|
||||
nnn = session.query(Notification).filter(and_(Notification.created_at > after)).all()
|
||||
nnn = session.query(Notification).where(and_(Notification.created_at > after)).all()
|
||||
for notification in nnn:
|
||||
ns = NotificationSeen(notification=notification.id, author=author_id)
|
||||
session.add(ns)
|
||||
@@ -282,7 +282,7 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
|
||||
# TODO: handle new follower and new shout notifications
|
||||
new_reaction_notifications = (
|
||||
session.query(Notification)
|
||||
.filter(
|
||||
.where(
|
||||
Notification.action == "create",
|
||||
Notification.entity == "reaction",
|
||||
Notification.created_at > after,
|
||||
@@ -291,7 +291,7 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s
|
||||
)
|
||||
removed_reaction_notifications = (
|
||||
session.query(Notification)
|
||||
.filter(
|
||||
.where(
|
||||
Notification.action == "delete",
|
||||
Notification.entity == "reaction",
|
||||
Notification.created_at > after,
|
||||
|
@@ -11,14 +11,14 @@ def handle_proposing(kind: ReactionKind, reply_to: int, shout_id: int) -> None:
|
||||
with local_session() as session:
|
||||
if is_positive(kind):
|
||||
replied_reaction = (
|
||||
session.query(Reaction).filter(Reaction.id == reply_to, Reaction.shout == shout_id).first()
|
||||
session.query(Reaction).where(Reaction.id == reply_to, Reaction.shout == shout_id).first()
|
||||
)
|
||||
|
||||
if replied_reaction and replied_reaction.kind is ReactionKind.PROPOSE.value and replied_reaction.quote:
|
||||
# patch all the proposals' quotes
|
||||
proposals = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.kind == ReactionKind.PROPOSE.value,
|
||||
@@ -28,7 +28,7 @@ def handle_proposing(kind: ReactionKind, reply_to: int, shout_id: int) -> None:
|
||||
)
|
||||
|
||||
# patch shout's body
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if shout:
|
||||
body = replied_reaction.quote
|
||||
# Use setattr instead of Shout.update for Column assignment
|
||||
|
@@ -103,11 +103,11 @@ async def rate_author(_: None, info: GraphQLResolveInfo, rated_slug: str, value:
|
||||
rater_id = info.context.get("author", {}).get("id")
|
||||
with local_session() as session:
|
||||
rater_id = int(rater_id)
|
||||
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
|
||||
rated_author = session.query(Author).where(Author.slug == rated_slug).first()
|
||||
if rater_id and rated_author:
|
||||
rating = (
|
||||
session.query(AuthorRating)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
AuthorRating.rater == rater_id,
|
||||
AuthorRating.author == rated_author.id,
|
||||
@@ -140,7 +140,7 @@ def count_author_comments_rating(session: Session, author_id: int) -> int:
|
||||
replied_alias.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
.filter(replied_alias.kind == ReactionKind.LIKE.value)
|
||||
.where(replied_alias.kind == ReactionKind.LIKE.value)
|
||||
.count()
|
||||
) or 0
|
||||
replies_dislikes = (
|
||||
@@ -152,7 +152,7 @@ def count_author_comments_rating(session: Session, author_id: int) -> int:
|
||||
replied_alias.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
|
||||
.where(replied_alias.kind == ReactionKind.DISLIKE.value)
|
||||
.count()
|
||||
) or 0
|
||||
|
||||
@@ -170,7 +170,7 @@ def count_author_replies_rating(session: Session, author_id: int) -> int:
|
||||
replied_alias.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
.filter(replied_alias.kind == ReactionKind.LIKE.value)
|
||||
.where(replied_alias.kind == ReactionKind.LIKE.value)
|
||||
.count()
|
||||
) or 0
|
||||
replies_dislikes = (
|
||||
@@ -182,7 +182,7 @@ def count_author_replies_rating(session: Session, author_id: int) -> int:
|
||||
replied_alias.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
|
||||
.where(replied_alias.kind == ReactionKind.DISLIKE.value)
|
||||
.count()
|
||||
) or 0
|
||||
|
||||
@@ -193,7 +193,7 @@ def count_author_shouts_rating(session: Session, author_id: int) -> int:
|
||||
shouts_likes = (
|
||||
session.query(Reaction, Shout)
|
||||
.join(Shout, Shout.id == Reaction.shout)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Shout.authors.any(id=author_id),
|
||||
Reaction.kind == ReactionKind.LIKE.value,
|
||||
@@ -205,7 +205,7 @@ def count_author_shouts_rating(session: Session, author_id: int) -> int:
|
||||
shouts_dislikes = (
|
||||
session.query(Reaction, Shout)
|
||||
.join(Shout, Shout.id == Reaction.shout)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Shout.authors.any(id=author_id),
|
||||
Reaction.kind == ReactionKind.DISLIKE.value,
|
||||
@@ -219,10 +219,10 @@ def count_author_shouts_rating(session: Session, author_id: int) -> int:
|
||||
|
||||
def get_author_rating_old(session: Session, author: Author) -> dict[str, int]:
|
||||
likes_count = (
|
||||
session.query(AuthorRating).filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True))).count()
|
||||
session.query(AuthorRating).where(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True))).count()
|
||||
)
|
||||
dislikes_count = (
|
||||
session.query(AuthorRating).filter(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(False))).count()
|
||||
session.query(AuthorRating).where(and_(AuthorRating.author == author.id, AuthorRating.plus.is_(False))).count()
|
||||
)
|
||||
rating = likes_count - dislikes_count
|
||||
return {"rating": rating, "likes": likes_count, "dislikes": dislikes_count}
|
||||
@@ -232,14 +232,18 @@ def get_author_rating_shouts(session: Session, author: Author) -> int:
|
||||
q = (
|
||||
select(
|
||||
Reaction.shout,
|
||||
Reaction.plus,
|
||||
case(
|
||||
(Reaction.kind == ReactionKind.LIKE.value, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE.value, -1),
|
||||
else_=0,
|
||||
).label("rating_value"),
|
||||
)
|
||||
.select_from(Reaction)
|
||||
.join(ShoutAuthor, Reaction.shout == ShoutAuthor.shout)
|
||||
.where(
|
||||
and_(
|
||||
ShoutAuthor.author == author.id,
|
||||
Reaction.kind == "RATING",
|
||||
Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
|
||||
Reaction.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
@@ -248,7 +252,7 @@ def get_author_rating_shouts(session: Session, author: Author) -> int:
|
||||
results = session.execute(q)
|
||||
rating = 0
|
||||
for row in results:
|
||||
rating += 1 if row[1] else -1
|
||||
rating += row[1]
|
||||
|
||||
return rating
|
||||
|
||||
@@ -258,7 +262,11 @@ def get_author_rating_comments(session: Session, author: Author) -> int:
|
||||
q = (
|
||||
select(
|
||||
Reaction.id,
|
||||
Reaction.plus,
|
||||
case(
|
||||
(Reaction.kind == ReactionKind.LIKE.value, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE.value, -1),
|
||||
else_=0,
|
||||
).label("rating_value"),
|
||||
)
|
||||
.select_from(Reaction)
|
||||
.outerjoin(replied_comment, Reaction.reply_to == replied_comment.id)
|
||||
@@ -267,7 +275,7 @@ def get_author_rating_comments(session: Session, author: Author) -> int:
|
||||
.where(
|
||||
and_(
|
||||
ShoutAuthor.author == author.id,
|
||||
Reaction.kind == "RATING",
|
||||
Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
|
||||
Reaction.created_by != author.id,
|
||||
Reaction.deleted_at.is_(None),
|
||||
)
|
||||
@@ -277,7 +285,7 @@ def get_author_rating_comments(session: Session, author: Author) -> int:
|
||||
results = session.execute(q)
|
||||
rating = 0
|
||||
for row in results:
|
||||
rating += 1 if row[1] else -1
|
||||
rating += row[1]
|
||||
|
||||
return rating
|
||||
|
||||
|
@@ -1,14 +1,20 @@
|
||||
import contextlib
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import and_, asc, case, desc, func, select
|
||||
from sqlalchemy import Select, and_, asc, case, desc, func, select
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
from sqlalchemy.sql import ColumnElement
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.rating import PROPOSAL_REACTIONS, RATING_REACTIONS, is_negative, is_positive
|
||||
from orm.rating import (
|
||||
NEGATIVE_REACTIONS,
|
||||
POSITIVE_REACTIONS,
|
||||
PROPOSAL_REACTIONS,
|
||||
RATING_REACTIONS,
|
||||
is_positive,
|
||||
)
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
from resolvers.follower import follow
|
||||
@@ -21,7 +27,7 @@ from services.schema import mutation, query
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def query_reactions() -> select:
|
||||
def query_reactions() -> Select:
|
||||
"""
|
||||
Base query for fetching reactions with associated authors and shouts.
|
||||
|
||||
@@ -39,7 +45,7 @@ def query_reactions() -> select:
|
||||
)
|
||||
|
||||
|
||||
def add_reaction_stat_columns(q: select) -> select:
|
||||
def add_reaction_stat_columns(q: Select) -> Select:
|
||||
"""
|
||||
Add statistical columns to a reaction query.
|
||||
|
||||
@@ -57,7 +63,7 @@ def add_reaction_stat_columns(q: select) -> select:
|
||||
).add_columns(
|
||||
# Count unique comments
|
||||
func.coalesce(
|
||||
func.count(aliased_reaction.id).filter(aliased_reaction.kind == ReactionKind.COMMENT.value), 0
|
||||
func.count(aliased_reaction.id).where(aliased_reaction.kind == ReactionKind.COMMENT.value), 0
|
||||
).label("comments_stat"),
|
||||
# Calculate rating as the difference between likes and dislikes
|
||||
func.sum(
|
||||
@@ -70,7 +76,7 @@ def add_reaction_stat_columns(q: select) -> select:
|
||||
)
|
||||
|
||||
|
||||
def get_reactions_with_stat(q: select, limit: int = 10, offset: int = 0) -> list[dict]:
|
||||
def get_reactions_with_stat(q: Select, limit: int = 10, offset: int = 0) -> list[dict]:
|
||||
"""
|
||||
Execute the reaction query and retrieve reactions with statistics.
|
||||
|
||||
@@ -85,7 +91,7 @@ def get_reactions_with_stat(q: select, limit: int = 10, offset: int = 0) -> list
|
||||
# Убираем distinct() поскольку GROUP BY уже обеспечивает уникальность,
|
||||
# а distinct() вызывает ошибку PostgreSQL с JSON полями
|
||||
q = q.limit(limit).offset(offset)
|
||||
reactions = []
|
||||
reactions: list[dict] = []
|
||||
|
||||
with local_session() as session:
|
||||
result_rows = session.execute(q).unique()
|
||||
@@ -116,7 +122,7 @@ def is_featured_author(session: Session, author_id: int) -> bool:
|
||||
return session.query(
|
||||
session.query(Shout)
|
||||
.where(Shout.authors.any(id=author_id))
|
||||
.filter(Shout.featured_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
.where(Shout.featured_at.is_not(None), Shout.deleted_at.is_(None))
|
||||
.exists()
|
||||
).scalar()
|
||||
|
||||
@@ -130,7 +136,8 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool
|
||||
:param reaction: Reaction object.
|
||||
:return: True if shout should be featured, else False.
|
||||
"""
|
||||
if not reaction.get("reply_to") and is_positive(reaction.get("kind")):
|
||||
is_positive_kind = reaction.get("kind") == ReactionKind.LIKE.value
|
||||
if not reaction.get("reply_to") and is_positive_kind:
|
||||
# Проверяем, не содержит ли пост более 20% дизлайков
|
||||
# Если да, то не должен быть featured независимо от количества лайков
|
||||
if check_to_unfeature(session, reaction):
|
||||
@@ -140,9 +147,9 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool
|
||||
author_approvers = set()
|
||||
reacted_readers = (
|
||||
session.query(Reaction.created_by)
|
||||
.filter(
|
||||
.where(
|
||||
Reaction.shout == reaction.get("shout"),
|
||||
is_positive(Reaction.kind),
|
||||
Reaction.kind.in_(POSITIVE_REACTIONS),
|
||||
# Рейтинги (LIKE, DISLIKE) физически удаляются, поэтому фильтр deleted_at не нужен
|
||||
)
|
||||
.distinct()
|
||||
@@ -150,7 +157,7 @@ def check_to_feature(session: Session, approver_id: int, reaction: dict) -> bool
|
||||
)
|
||||
|
||||
# Добавляем текущего одобряющего
|
||||
approver = session.query(Author).filter(Author.id == approver_id).first()
|
||||
approver = session.query(Author).where(Author.id == approver_id).first()
|
||||
if approver and is_featured_author(session, approver_id):
|
||||
author_approvers.add(approver_id)
|
||||
|
||||
@@ -181,7 +188,7 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
||||
# Проверяем соотношение дизлайков, даже если текущая реакция не дизлайк
|
||||
total_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
.where(
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.reply_to.is_(None),
|
||||
Reaction.kind.in_(RATING_REACTIONS),
|
||||
@@ -192,9 +199,9 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
||||
|
||||
positive_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
.where(
|
||||
Reaction.shout == shout_id,
|
||||
is_positive(Reaction.kind),
|
||||
Reaction.kind.in_(POSITIVE_REACTIONS),
|
||||
Reaction.reply_to.is_(None),
|
||||
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
|
||||
)
|
||||
@@ -203,9 +210,9 @@ def check_to_unfeature(session: Session, reaction: dict) -> bool:
|
||||
|
||||
negative_reactions = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
.where(
|
||||
Reaction.shout == shout_id,
|
||||
is_negative(Reaction.kind),
|
||||
Reaction.kind.in_(NEGATIVE_REACTIONS),
|
||||
Reaction.reply_to.is_(None),
|
||||
# Рейтинги физически удаляются при удалении, поэтому фильтр deleted_at не нужен
|
||||
)
|
||||
@@ -235,13 +242,13 @@ async def set_featured(session: Session, shout_id: int) -> None:
|
||||
:param session: Database session.
|
||||
:param shout_id: Shout ID.
|
||||
"""
|
||||
s = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
s = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if s:
|
||||
current_time = int(time.time())
|
||||
# Use setattr to avoid MyPy complaints about Column assignment
|
||||
s.featured_at = current_time # type: ignore[assignment]
|
||||
session.commit()
|
||||
author = session.query(Author).filter(Author.id == s.created_by).first()
|
||||
author = session.query(Author).where(Author.id == s.created_by).first()
|
||||
if author:
|
||||
await add_user_role(str(author.id))
|
||||
session.add(s)
|
||||
@@ -255,7 +262,7 @@ def set_unfeatured(session: Session, shout_id: int) -> None:
|
||||
:param session: Database session.
|
||||
:param shout_id: Shout ID.
|
||||
"""
|
||||
session.query(Shout).filter(Shout.id == shout_id).update({"featured_at": None})
|
||||
session.query(Shout).where(Shout.id == shout_id).update({"featured_at": None})
|
||||
session.commit()
|
||||
|
||||
|
||||
@@ -288,7 +295,7 @@ async def _create_reaction(session: Session, shout_id: int, is_author: bool, aut
|
||||
# Handle rating
|
||||
if r.kind in RATING_REACTIONS:
|
||||
# Проверяем, является ли публикация featured
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
is_currently_featured = shout and shout.featured_at is not None
|
||||
|
||||
# Проверяем сначала условие для unfeature (для уже featured публикаций)
|
||||
@@ -317,26 +324,27 @@ def prepare_new_rating(reaction: dict, shout_id: int, session: Session, author_i
|
||||
:return: Dictionary with error or None.
|
||||
"""
|
||||
kind = reaction.get("kind")
|
||||
opposite_kind = ReactionKind.DISLIKE.value if is_positive(kind) else ReactionKind.LIKE.value
|
||||
if kind in RATING_REACTIONS:
|
||||
opposite_kind = ReactionKind.DISLIKE.value if is_positive(kind) else ReactionKind.LIKE.value
|
||||
|
||||
existing_ratings = (
|
||||
session.query(Reaction)
|
||||
.filter(
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.created_by == author_id,
|
||||
Reaction.kind.in_(RATING_REACTIONS),
|
||||
Reaction.deleted_at.is_(None),
|
||||
existing_ratings = (
|
||||
session.query(Reaction)
|
||||
.where(
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.created_by == author_id,
|
||||
Reaction.kind.in_(RATING_REACTIONS),
|
||||
Reaction.deleted_at.is_(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
for r in existing_ratings:
|
||||
if r.kind == kind:
|
||||
return {"error": "You can't rate the same thing twice"}
|
||||
if r.kind == opposite_kind:
|
||||
return {"error": "Remove opposite vote first"}
|
||||
if shout_id in [r.shout for r in existing_ratings]:
|
||||
return {"error": "You can't rate your own thing"}
|
||||
for r in existing_ratings:
|
||||
if r.kind == kind:
|
||||
return {"error": "You can't rate the same thing twice"}
|
||||
if r.kind == opposite_kind:
|
||||
return {"error": "Remove opposite vote first"}
|
||||
if shout_id in [r.shout for r in existing_ratings]:
|
||||
return {"error": "You can't rate your own thing"}
|
||||
|
||||
return None
|
||||
|
||||
@@ -366,7 +374,7 @@ async def create_reaction(_: None, info: GraphQLResolveInfo, reaction: dict) ->
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
authors = session.query(ShoutAuthor.author).filter(ShoutAuthor.shout == shout_id).scalar()
|
||||
authors = session.query(ShoutAuthor.author).where(ShoutAuthor.shout == shout_id).scalar()
|
||||
is_author = (
|
||||
bool(list(filter(lambda x: x == int(author_id), authors))) if isinstance(authors, list) else False
|
||||
)
|
||||
@@ -387,17 +395,14 @@ async def create_reaction(_: None, info: GraphQLResolveInfo, reaction: dict) ->
|
||||
|
||||
# follow if liked
|
||||
if kind == ReactionKind.LIKE.value:
|
||||
with contextlib.suppress(Exception):
|
||||
follow(None, info, "shout", shout_id=shout_id)
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||
follow(None, info, "shout", shout_id=shout_id)
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
if not shout:
|
||||
return {"error": "Shout not found"}
|
||||
rdict["shout"] = shout.dict()
|
||||
rdict["created_by"] = author_dict
|
||||
return {"reaction": rdict}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"{type(e).__name__}: {e}")
|
||||
return {"error": "Cannot create reaction."}
|
||||
@@ -424,7 +429,7 @@ async def update_reaction(_: None, info: GraphQLResolveInfo, reaction: dict) ->
|
||||
|
||||
with local_session() as session:
|
||||
try:
|
||||
reaction_query = query_reactions().filter(Reaction.id == rid)
|
||||
reaction_query = query_reactions().where(Reaction.id == rid)
|
||||
reaction_query = add_reaction_stat_columns(reaction_query)
|
||||
reaction_query = reaction_query.group_by(Reaction.id, Author.id, Shout.id)
|
||||
|
||||
@@ -472,12 +477,12 @@ async def delete_reaction(_: None, info: GraphQLResolveInfo, reaction_id: int) -
|
||||
roles = info.context.get("roles", [])
|
||||
|
||||
if not author_id:
|
||||
return {"error": "Unauthorized"}
|
||||
return {"error": "UnauthorizedError"}
|
||||
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == author_id).one()
|
||||
r = session.query(Reaction).filter(Reaction.id == reaction_id).one()
|
||||
author = session.query(Author).where(Author.id == author_id).one()
|
||||
r = session.query(Reaction).where(Reaction.id == reaction_id).one()
|
||||
|
||||
if r.created_by != author_id and "editor" not in roles:
|
||||
return {"error": "Access denied"}
|
||||
@@ -496,7 +501,7 @@ async def delete_reaction(_: None, info: GraphQLResolveInfo, reaction_id: int) -
|
||||
logger.debug(f"{author_id} user removing his #{reaction_id} reaction")
|
||||
reaction_dict = r.dict()
|
||||
# Проверяем, является ли публикация featured до удаления реакции
|
||||
shout = session.query(Shout).filter(Shout.id == r.shout).first()
|
||||
shout = session.query(Shout).where(Shout.id == r.shout).first()
|
||||
is_currently_featured = shout and shout.featured_at is not None
|
||||
|
||||
session.delete(r)
|
||||
@@ -506,16 +511,15 @@ async def delete_reaction(_: None, info: GraphQLResolveInfo, reaction_id: int) -
|
||||
if is_currently_featured and check_to_unfeature(session, reaction_dict):
|
||||
set_unfeatured(session, r.shout)
|
||||
|
||||
reaction_dict = r.dict()
|
||||
await notify_reaction(reaction_dict, "delete")
|
||||
await notify_reaction(r, "delete")
|
||||
|
||||
return {"error": None, "reaction": reaction_dict}
|
||||
return {"error": None, "reaction": r.dict()}
|
||||
except Exception as e:
|
||||
logger.error(f"{type(e).__name__}: {e}")
|
||||
return {"error": "Cannot delete reaction"}
|
||||
|
||||
|
||||
def apply_reaction_filters(by: dict, q: select) -> select:
|
||||
def apply_reaction_filters(by: dict, q: Select) -> Select:
|
||||
"""
|
||||
Apply filters to a reaction query.
|
||||
|
||||
@@ -525,42 +529,42 @@ def apply_reaction_filters(by: dict, q: select) -> select:
|
||||
"""
|
||||
shout_slug = by.get("shout")
|
||||
if shout_slug:
|
||||
q = q.filter(Shout.slug == shout_slug)
|
||||
q = q.where(Shout.slug == shout_slug)
|
||||
|
||||
shout_id = by.get("shout_id")
|
||||
if shout_id:
|
||||
q = q.filter(Shout.id == shout_id)
|
||||
q = q.where(Shout.id == shout_id)
|
||||
|
||||
shouts = by.get("shouts")
|
||||
if shouts:
|
||||
q = q.filter(Shout.slug.in_(shouts))
|
||||
q = q.where(Shout.slug.in_(shouts))
|
||||
|
||||
created_by = by.get("created_by", by.get("author_id"))
|
||||
if created_by:
|
||||
q = q.filter(Author.id == created_by)
|
||||
q = q.where(Author.id == created_by)
|
||||
|
||||
author_slug = by.get("author")
|
||||
if author_slug:
|
||||
q = q.filter(Author.slug == author_slug)
|
||||
q = q.where(Author.slug == author_slug)
|
||||
|
||||
topic = by.get("topic")
|
||||
if isinstance(topic, int):
|
||||
q = q.filter(Shout.topics.any(id=topic))
|
||||
q = q.where(Shout.topics.any(id=topic))
|
||||
|
||||
kinds = by.get("kinds")
|
||||
if isinstance(kinds, list):
|
||||
q = q.filter(Reaction.kind.in_(kinds))
|
||||
q = q.where(Reaction.kind.in_(kinds))
|
||||
|
||||
if by.get("reply_to"):
|
||||
q = q.filter(Reaction.reply_to == by.get("reply_to"))
|
||||
q = q.where(Reaction.reply_to == by.get("reply_to"))
|
||||
|
||||
by_search = by.get("search", "")
|
||||
if len(by_search) > 2:
|
||||
q = q.filter(Reaction.body.ilike(f"%{by_search}%"))
|
||||
q = q.where(Reaction.body.ilike(f"%{by_search}%"))
|
||||
|
||||
after = by.get("after")
|
||||
if isinstance(after, int):
|
||||
q = q.filter(Reaction.created_at > after)
|
||||
q = q.where(Reaction.created_at > after)
|
||||
|
||||
return q
|
||||
|
||||
@@ -617,7 +621,7 @@ async def load_shout_ratings(
|
||||
q = query_reactions()
|
||||
|
||||
# Filter, group, sort, limit, offset
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.shout == shout,
|
||||
@@ -649,7 +653,7 @@ async def load_shout_comments(
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Filter, group, sort, limit, offset
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.shout == shout,
|
||||
@@ -679,7 +683,7 @@ async def load_comment_ratings(
|
||||
q = query_reactions()
|
||||
|
||||
# Filter, group, sort, limit, offset
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.reply_to == comment,
|
||||
@@ -723,7 +727,7 @@ async def load_comments_branch(
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Фильтруем по статье и типу (комментарии)
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.shout == shout,
|
||||
@@ -732,7 +736,7 @@ async def load_comments_branch(
|
||||
)
|
||||
|
||||
# Фильтруем по родительскому ID
|
||||
q = q.filter(Reaction.reply_to.is_(None)) if parent_id is None else q.filter(Reaction.reply_to == parent_id)
|
||||
q = q.where(Reaction.reply_to.is_(None)) if parent_id is None else q.where(Reaction.reply_to == parent_id)
|
||||
|
||||
# Сортировка и группировка
|
||||
q = q.group_by(Reaction.id, Author.id, Shout.id)
|
||||
@@ -822,7 +826,7 @@ async def load_first_replies(comments: list[Any], limit: int, offset: int, sort:
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Фильтрация: только ответы на указанные комментарии
|
||||
q = q.filter(
|
||||
q = q.where(
|
||||
and_(
|
||||
Reaction.reply_to.in_(comment_ids),
|
||||
Reaction.deleted_at.is_(None),
|
||||
|
@@ -2,8 +2,8 @@ from typing import Any, Optional
|
||||
|
||||
import orjson
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import and_, nulls_last, text
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy import Select, and_, nulls_last, text
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
from sqlalchemy.sql.expression import asc, case, desc, func, select
|
||||
|
||||
from auth.orm import Author
|
||||
@@ -12,12 +12,12 @@ from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic
|
||||
from services.db import json_array_builder, json_builder, local_session
|
||||
from services.schema import query
|
||||
from services.search import search_text
|
||||
from services.search import SearchService, search_text
|
||||
from services.viewed import ViewedStorage
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def apply_options(q: select, options: dict[str, Any], reactions_created_by: int = 0) -> tuple[select, int, int]:
|
||||
def apply_options(q: Select, options: dict[str, Any], reactions_created_by: int = 0) -> tuple[Select, int, int]:
|
||||
"""
|
||||
Применяет опции фильтрации и сортировки
|
||||
[опционально] выбирая те публикации, на которые есть реакции/комментарии от указанного автора
|
||||
@@ -32,9 +32,9 @@ def apply_options(q: select, options: dict[str, Any], reactions_created_by: int
|
||||
q = apply_filters(q, filters)
|
||||
if reactions_created_by:
|
||||
q = q.join(Reaction, Reaction.shout == Shout.id)
|
||||
q = q.filter(Reaction.created_by == reactions_created_by)
|
||||
q = q.where(Reaction.created_by == reactions_created_by)
|
||||
if "commented" in filters:
|
||||
q = q.filter(Reaction.body.is_not(None))
|
||||
q = q.where(Reaction.body.is_not(None))
|
||||
q = apply_sorting(q, options)
|
||||
limit = options.get("limit", 10)
|
||||
offset = options.get("offset", 0)
|
||||
@@ -58,14 +58,14 @@ def has_field(info: GraphQLResolveInfo, fieldname: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def query_with_stat(info: GraphQLResolveInfo) -> select:
|
||||
def query_with_stat(info: GraphQLResolveInfo) -> Select:
|
||||
"""
|
||||
:param info: Информация о контексте GraphQL - для получения id авторизованного пользователя
|
||||
:return: Запрос с подзапросами статистики.
|
||||
|
||||
Добавляет подзапрос статистики
|
||||
"""
|
||||
q = select(Shout).filter(
|
||||
q = select(Shout).where(
|
||||
and_(
|
||||
Shout.published_at.is_not(None), # type: ignore[union-attr]
|
||||
Shout.deleted_at.is_(None), # type: ignore[union-attr]
|
||||
@@ -158,7 +158,7 @@ def query_with_stat(info: GraphQLResolveInfo) -> select:
|
||||
select(
|
||||
Reaction.shout,
|
||||
func.count(func.distinct(Reaction.id))
|
||||
.filter(Reaction.kind == ReactionKind.COMMENT.value)
|
||||
.where(Reaction.kind == ReactionKind.COMMENT.value)
|
||||
.label("comments_count"),
|
||||
func.sum(
|
||||
case(
|
||||
@@ -167,10 +167,10 @@ def query_with_stat(info: GraphQLResolveInfo) -> select:
|
||||
else_=0,
|
||||
)
|
||||
)
|
||||
.filter(Reaction.reply_to.is_(None))
|
||||
.where(Reaction.reply_to.is_(None))
|
||||
.label("rating"),
|
||||
func.max(Reaction.created_at)
|
||||
.filter(Reaction.kind == ReactionKind.COMMENT.value)
|
||||
.where(Reaction.kind == ReactionKind.COMMENT.value)
|
||||
.label("last_commented_at"),
|
||||
)
|
||||
.where(Reaction.deleted_at.is_(None))
|
||||
@@ -192,7 +192,7 @@ def query_with_stat(info: GraphQLResolveInfo) -> select:
|
||||
return q
|
||||
|
||||
|
||||
def get_shouts_with_links(info: GraphQLResolveInfo, q: select, limit: int = 20, offset: int = 0) -> list[Shout]:
|
||||
def get_shouts_with_links(info: GraphQLResolveInfo, q: Select, limit: int = 20, offset: int = 0) -> list[Shout]:
|
||||
"""
|
||||
получение публикаций с применением пагинации
|
||||
"""
|
||||
@@ -222,7 +222,7 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: select, limit: int = 20,
|
||||
# Обработка поля created_by
|
||||
if has_field(info, "created_by") and shout_dict.get("created_by"):
|
||||
main_author_id = shout_dict.get("created_by")
|
||||
a = session.query(Author).filter(Author.id == main_author_id).first()
|
||||
a = session.query(Author).where(Author.id == main_author_id).first()
|
||||
if a:
|
||||
shout_dict["created_by"] = {
|
||||
"id": main_author_id,
|
||||
@@ -235,7 +235,7 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: select, limit: int = 20,
|
||||
if has_field(info, "updated_by"):
|
||||
if shout_dict.get("updated_by"):
|
||||
updated_by_id = shout_dict.get("updated_by")
|
||||
updated_author = session.query(Author).filter(Author.id == updated_by_id).first()
|
||||
updated_author = session.query(Author).where(Author.id == updated_by_id).first()
|
||||
if updated_author:
|
||||
shout_dict["updated_by"] = {
|
||||
"id": updated_author.id,
|
||||
@@ -254,7 +254,7 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: select, limit: int = 20,
|
||||
if has_field(info, "deleted_by"):
|
||||
if shout_dict.get("deleted_by"):
|
||||
deleted_by_id = shout_dict.get("deleted_by")
|
||||
deleted_author = session.query(Author).filter(Author.id == deleted_by_id).first()
|
||||
deleted_author = session.query(Author).where(Author.id == deleted_by_id).first()
|
||||
if deleted_author:
|
||||
shout_dict["deleted_by"] = {
|
||||
"id": deleted_author.id,
|
||||
@@ -347,7 +347,7 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: select, limit: int = 20,
|
||||
return shouts
|
||||
|
||||
|
||||
def apply_filters(q: select, filters: dict[str, Any]) -> select:
|
||||
def apply_filters(q: Select, filters: dict[str, Any]) -> Select:
|
||||
"""
|
||||
Применение общих фильтров к запросу.
|
||||
|
||||
@@ -360,23 +360,23 @@ def apply_filters(q: select, filters: dict[str, Any]) -> select:
|
||||
featured_filter = filters.get("featured")
|
||||
featured_at_col = getattr(Shout, "featured_at", None)
|
||||
if featured_at_col is not None:
|
||||
q = q.filter(featured_at_col.is_not(None)) if featured_filter else q.filter(featured_at_col.is_(None))
|
||||
q = q.where(featured_at_col.is_not(None)) if featured_filter else q.where(featured_at_col.is_(None))
|
||||
by_layouts = filters.get("layouts")
|
||||
if by_layouts and isinstance(by_layouts, list):
|
||||
q = q.filter(Shout.layout.in_(by_layouts))
|
||||
q = q.where(Shout.layout.in_(by_layouts))
|
||||
by_author = filters.get("author")
|
||||
if by_author:
|
||||
q = q.filter(Shout.authors.any(slug=by_author))
|
||||
q = q.where(Shout.authors.any(slug=by_author))
|
||||
by_topic = filters.get("topic")
|
||||
if by_topic:
|
||||
q = q.filter(Shout.topics.any(slug=by_topic))
|
||||
q = q.where(Shout.topics.any(slug=by_topic))
|
||||
by_after = filters.get("after")
|
||||
if by_after:
|
||||
ts = int(by_after)
|
||||
q = q.filter(Shout.created_at > ts)
|
||||
q = q.where(Shout.created_at > ts)
|
||||
by_community = filters.get("community")
|
||||
if by_community:
|
||||
q = q.filter(Shout.community == by_community)
|
||||
q = q.where(Shout.community == by_community)
|
||||
|
||||
return q
|
||||
|
||||
@@ -417,7 +417,7 @@ async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id:
|
||||
return None
|
||||
|
||||
|
||||
def apply_sorting(q: select, options: dict[str, Any]) -> select:
|
||||
def apply_sorting(q: Select, options: dict[str, Any]) -> Select:
|
||||
"""
|
||||
Применение сортировки с сохранением порядка
|
||||
"""
|
||||
@@ -455,7 +455,9 @@ async def load_shouts_by(_: None, info: GraphQLResolveInfo, options: dict[str, A
|
||||
|
||||
|
||||
@query.field("load_shouts_search")
|
||||
async def load_shouts_search(_: None, info: GraphQLResolveInfo, text: str, options: dict[str, Any]) -> list[Shout]:
|
||||
async def load_shouts_search(
|
||||
_: None, info: GraphQLResolveInfo, text: str, options: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Поиск публикаций по тексту.
|
||||
|
||||
@@ -497,20 +499,22 @@ async def load_shouts_search(_: None, info: GraphQLResolveInfo, text: str, optio
|
||||
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).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.filter(Shout.id.in_(hits_ids))
|
||||
q = q.where(Shout.id.in_(hits_ids))
|
||||
q = apply_filters(q, options)
|
||||
q = apply_sorting(q, options)
|
||||
|
||||
logger.debug(f"[load_shouts_search] Executing database query for {len(hits_ids)} shout IDs")
|
||||
shouts_dicts = get_shouts_with_links(info, q, limit, offset)
|
||||
|
||||
logger.debug(f"[load_shouts_search] Database returned {len(shouts_dicts)} shouts")
|
||||
|
||||
for shout_dict in shouts_dicts:
|
||||
shout_id_str = f"{shout_dict['id']}"
|
||||
shout_dict["score"] = scores.get(shout_id_str, 0.0)
|
||||
shouts = get_shouts_with_links(info, q, limit, offset)
|
||||
logger.debug(f"[load_shouts_search] Database returned {len(shouts)} shouts")
|
||||
shouts_dicts: list[dict[str, Any]] = []
|
||||
for shout in shouts:
|
||||
shout_dict = shout.dict()
|
||||
shout_id_str = shout_dict.get("id")
|
||||
if shout_id_str:
|
||||
shout_dict["score"] = scores.get(shout_id_str, 0.0)
|
||||
shouts_dicts.append(shout_dict)
|
||||
|
||||
shouts_dicts.sort(key=lambda x: x.get("score", 0.0), reverse=True)
|
||||
|
||||
@@ -540,7 +544,7 @@ async def load_shouts_unrated(_: None, info: GraphQLResolveInfo, options: dict[s
|
||||
)
|
||||
)
|
||||
.group_by(Reaction.shout)
|
||||
.having(func.count("*") >= 3)
|
||||
.having(func.count(Reaction.id) >= 3)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
@@ -594,7 +598,51 @@ async def load_shouts_random_top(_: None, info: GraphQLResolveInfo, options: dic
|
||||
random_limit = options.get("random_limit", 100)
|
||||
subquery = subquery.limit(random_limit)
|
||||
q = query_with_stat(info)
|
||||
q = q.filter(Shout.id.in_(subquery))
|
||||
q = q.where(Shout.id.in_(subquery))
|
||||
q = q.order_by(func.random())
|
||||
limit = options.get("limit", 10)
|
||||
return get_shouts_with_links(info, q, limit)
|
||||
|
||||
|
||||
async def fetch_all_shouts(
|
||||
session: Session,
|
||||
search_service: SearchService,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
search_query: str = "",
|
||||
) -> list[Shout]:
|
||||
"""
|
||||
Получает все shout'ы с возможностью поиска и пагинации.
|
||||
|
||||
:param session: Сессия базы данных
|
||||
:param search_service: Сервис поиска
|
||||
:param limit: Максимальное количество возвращаемых shout'ов
|
||||
:param offset: Смещение для пагинации
|
||||
:param search_query: Строка поиска
|
||||
:return: Список shout'ов
|
||||
"""
|
||||
try:
|
||||
# Базовый запрос для получения shout'ов
|
||||
q = select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
|
||||
# Применяем поиск, если есть строка поиска
|
||||
if search_query:
|
||||
search_results = await search_service.search(search_query, limit=100, offset=0)
|
||||
if search_results:
|
||||
# Извлекаем ID из результатов поиска
|
||||
shout_ids = [result.get("id") for result in search_results if result.get("id")]
|
||||
if shout_ids:
|
||||
q = q.where(Shout.id.in_(shout_ids))
|
||||
|
||||
# Применяем лимит и смещение
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Выполняем запрос
|
||||
result = session.execute(q).scalars().all()
|
||||
|
||||
return list(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching shouts: {e}")
|
||||
return []
|
||||
finally:
|
||||
session.close()
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import and_, distinct, func, join, select
|
||||
@@ -7,7 +8,6 @@ from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.sql.expression import Select
|
||||
|
||||
from auth.orm import Author, AuthorFollower
|
||||
from cache.cache import cache_author
|
||||
from orm.community import Community, CommunityFollower
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
@@ -99,7 +99,7 @@ def get_topic_shouts_stat(topic_id: int) -> int:
|
||||
q = (
|
||||
select(func.count(distinct(ShoutTopic.shout)))
|
||||
.select_from(join(ShoutTopic, Shout, ShoutTopic.shout == Shout.id))
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
ShoutTopic.topic == topic_id,
|
||||
Shout.published_at.is_not(None),
|
||||
@@ -124,7 +124,7 @@ def get_topic_authors_stat(topic_id: int) -> int:
|
||||
select(func.count(distinct(ShoutAuthor.author)))
|
||||
.select_from(join(ShoutTopic, Shout, ShoutTopic.shout == Shout.id))
|
||||
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
ShoutTopic.topic == topic_id,
|
||||
Shout.published_at.is_not(None),
|
||||
@@ -147,7 +147,7 @@ def get_topic_followers_stat(topic_id: int) -> int:
|
||||
:return: Количество уникальных подписчиков темы.
|
||||
"""
|
||||
aliased_followers = aliased(TopicFollower)
|
||||
q = select(func.count(distinct(aliased_followers.follower))).filter(aliased_followers.topic == topic_id)
|
||||
q = select(func.count(distinct(aliased_followers.follower))).where(aliased_followers.topic == topic_id)
|
||||
with local_session() as session:
|
||||
result = session.execute(q).scalar()
|
||||
return int(result) if result else 0
|
||||
@@ -180,7 +180,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)).where(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).scalar()
|
||||
@@ -198,7 +198,7 @@ def get_author_shouts_stat(author_id: int) -> int:
|
||||
select(func.count(distinct(aliased_shout.id)))
|
||||
.select_from(aliased_shout)
|
||||
.join(aliased_shout_author, aliased_shout.id == aliased_shout_author.shout)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
aliased_shout_author.author == author_id,
|
||||
aliased_shout.published_at.is_not(None),
|
||||
@@ -221,7 +221,7 @@ def get_author_authors_stat(author_id: int) -> int:
|
||||
.select_from(ShoutAuthor)
|
||||
.join(Shout, ShoutAuthor.shout == Shout.id)
|
||||
.join(Reaction, Reaction.shout == Shout.id)
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Reaction.created_by == author_id,
|
||||
Shout.published_at.is_not(None),
|
||||
@@ -240,7 +240,7 @@ def get_author_followers_stat(author_id: int) -> int:
|
||||
"""
|
||||
Получает количество подписчиков для указанного автора
|
||||
"""
|
||||
q = select(func.count(AuthorFollower.follower)).filter(AuthorFollower.author == author_id)
|
||||
q = select(func.count(AuthorFollower.follower)).where(AuthorFollower.author == author_id)
|
||||
|
||||
with local_session() as session:
|
||||
result = session.execute(q).scalar()
|
||||
@@ -320,8 +320,6 @@ def get_with_stat(q: QueryType) -> list[Any]:
|
||||
entity.stat = stat
|
||||
records.append(entity)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
logger.debug(q)
|
||||
traceback.print_exc()
|
||||
logger.error(exc, exc_info=True)
|
||||
@@ -363,6 +361,9 @@ def update_author_stat(author_id: int) -> None:
|
||||
|
||||
:param author_id: Идентификатор автора.
|
||||
"""
|
||||
# Поздний импорт для избежания циклических зависимостей
|
||||
from cache.cache import cache_author
|
||||
|
||||
author_query = select(Author).where(Author.id == author_id)
|
||||
try:
|
||||
result = get_with_stat(author_query)
|
||||
@@ -373,10 +374,10 @@ def update_author_stat(author_id: int) -> None:
|
||||
# Асинхронное кэширование данных автора
|
||||
task = asyncio.create_task(cache_author(author_dict))
|
||||
# Store task reference to prevent garbage collection
|
||||
if not hasattr(update_author_stat, "_background_tasks"):
|
||||
update_author_stat._background_tasks = set() # type: ignore[attr-defined]
|
||||
update_author_stat._background_tasks.add(task) # type: ignore[attr-defined]
|
||||
task.add_done_callback(update_author_stat._background_tasks.discard) # type: ignore[attr-defined]
|
||||
if not hasattr(update_author_stat, "stat_tasks"):
|
||||
update_author_stat.stat_tasks = set() # type: ignore[attr-defined]
|
||||
update_author_stat.stat_tasks.add(task) # type: ignore[attr-defined]
|
||||
task.add_done_callback(update_author_stat.stat_tasks.discard) # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
logger.error(exc, exc_info=True)
|
||||
|
||||
@@ -387,19 +388,19 @@ def get_followers_count(entity_type: str, entity_id: int) -> int:
|
||||
with local_session() as session:
|
||||
if entity_type == "topic":
|
||||
result = (
|
||||
session.query(func.count(TopicFollower.follower)).filter(TopicFollower.topic == entity_id).scalar()
|
||||
session.query(func.count(TopicFollower.follower)).where(TopicFollower.topic == entity_id).scalar()
|
||||
)
|
||||
elif entity_type == "author":
|
||||
# Count followers of this author
|
||||
result = (
|
||||
session.query(func.count(AuthorFollower.follower))
|
||||
.filter(AuthorFollower.author == entity_id)
|
||||
.where(AuthorFollower.author == entity_id)
|
||||
.scalar()
|
||||
)
|
||||
elif entity_type == "community":
|
||||
result = (
|
||||
session.query(func.count(CommunityFollower.follower))
|
||||
.filter(CommunityFollower.community == entity_id)
|
||||
.where(CommunityFollower.community == entity_id)
|
||||
.scalar()
|
||||
)
|
||||
else:
|
||||
@@ -418,12 +419,12 @@ def get_following_count(entity_type: str, entity_id: int) -> int:
|
||||
if entity_type == "author":
|
||||
# Count what this author follows
|
||||
topic_follows = (
|
||||
session.query(func.count(TopicFollower.topic)).filter(TopicFollower.follower == entity_id).scalar()
|
||||
session.query(func.count(TopicFollower.topic)).where(TopicFollower.follower == entity_id).scalar()
|
||||
or 0
|
||||
)
|
||||
community_follows = (
|
||||
session.query(func.count(CommunityFollower.community))
|
||||
.filter(CommunityFollower.follower == entity_id)
|
||||
.where(CommunityFollower.follower == entity_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
@@ -440,15 +441,15 @@ def get_shouts_count(
|
||||
"""Получает количество публикаций"""
|
||||
try:
|
||||
with local_session() as session:
|
||||
query = session.query(func.count(Shout.id)).filter(Shout.published_at.isnot(None))
|
||||
query = session.query(func.count(Shout.id)).where(Shout.published_at.isnot(None))
|
||||
|
||||
if author_id:
|
||||
query = query.filter(Shout.created_by == author_id)
|
||||
query = query.where(Shout.created_by == author_id)
|
||||
if topic_id:
|
||||
# This would need ShoutTopic association table
|
||||
pass
|
||||
if community_id:
|
||||
query = query.filter(Shout.community == community_id)
|
||||
query = query.where(Shout.community == community_id)
|
||||
|
||||
result = query.scalar()
|
||||
return int(result) if result else 0
|
||||
@@ -465,12 +466,12 @@ def get_authors_count(community_id: Optional[int] = None) -> int:
|
||||
# Count authors in specific community
|
||||
result = (
|
||||
session.query(func.count(distinct(CommunityFollower.follower)))
|
||||
.filter(CommunityFollower.community == community_id)
|
||||
.where(CommunityFollower.community == community_id)
|
||||
.scalar()
|
||||
)
|
||||
else:
|
||||
# Count all authors
|
||||
result = session.query(func.count(Author.id)).filter(Author.deleted == False).scalar()
|
||||
result = session.query(func.count(Author.id)).where(Author.deleted_at.is_(None)).scalar()
|
||||
|
||||
return int(result) if result else 0
|
||||
except Exception as e:
|
||||
@@ -485,7 +486,7 @@ def get_topics_count(author_id: Optional[int] = None) -> int:
|
||||
if author_id:
|
||||
# Count topics followed by author
|
||||
result = (
|
||||
session.query(func.count(TopicFollower.topic)).filter(TopicFollower.follower == author_id).scalar()
|
||||
session.query(func.count(TopicFollower.topic)).where(TopicFollower.follower == author_id).scalar()
|
||||
)
|
||||
else:
|
||||
# Count all topics
|
||||
@@ -511,15 +512,13 @@ def get_communities_count() -> int:
|
||||
def get_reactions_count(shout_id: Optional[int] = None, author_id: Optional[int] = None) -> int:
|
||||
"""Получает количество реакций"""
|
||||
try:
|
||||
from orm.reaction import Reaction
|
||||
|
||||
with local_session() as session:
|
||||
query = session.query(func.count(Reaction.id))
|
||||
|
||||
if shout_id:
|
||||
query = query.filter(Reaction.shout == shout_id)
|
||||
query = query.where(Reaction.shout == shout_id)
|
||||
if author_id:
|
||||
query = query.filter(Reaction.created_by == author_id)
|
||||
query = query.where(Reaction.created_by == author_id)
|
||||
|
||||
result = query.scalar()
|
||||
return int(result) if result else 0
|
||||
@@ -531,13 +530,11 @@ def get_reactions_count(shout_id: Optional[int] = None, author_id: Optional[int]
|
||||
def get_comments_count_by_shout(shout_id: int) -> int:
|
||||
"""Получает количество комментариев к статье"""
|
||||
try:
|
||||
from orm.reaction import Reaction
|
||||
|
||||
with local_session() as session:
|
||||
# Using text() to access 'kind' column which might be enum
|
||||
result = (
|
||||
session.query(func.count(Reaction.id))
|
||||
.filter(
|
||||
.where(
|
||||
and_(
|
||||
Reaction.shout == shout_id,
|
||||
Reaction.kind == "comment", # Assuming 'comment' is a valid enum value
|
||||
@@ -555,8 +552,8 @@ def get_comments_count_by_shout(shout_id: int) -> int:
|
||||
async def get_stat_background_task() -> None:
|
||||
"""Фоновая задача для обновления статистики"""
|
||||
try:
|
||||
if not hasattr(sys.modules[__name__], "_background_tasks"):
|
||||
sys.modules[__name__]._background_tasks = set() # type: ignore[attr-defined]
|
||||
if not hasattr(sys.modules[__name__], "stat_tasks"):
|
||||
sys.modules[__name__].stat_tasks = set() # type: ignore[attr-defined]
|
||||
|
||||
# Perform background statistics calculations
|
||||
logger.info("Running background statistics update")
|
||||
|
@@ -14,6 +14,7 @@ from cache.cache import (
|
||||
invalidate_cache_by_prefix,
|
||||
invalidate_topic_followers_cache,
|
||||
)
|
||||
from orm.draft import DraftTopic
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic, TopicFollower
|
||||
@@ -100,10 +101,7 @@ async def get_topics_with_stats(
|
||||
|
||||
# Вычисляем информацию о пагинации
|
||||
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)
|
||||
total_pages = 1 if total_count is None or per_page in (None, 0) else ceil(total_count / per_page)
|
||||
current_page = (offset // per_page) + 1 if per_page > 0 else 1
|
||||
|
||||
# Применяем сортировку на основе параметра by
|
||||
@@ -263,7 +261,7 @@ async def get_topics_with_stats(
|
||||
WHERE st.topic IN ({placeholders})
|
||||
GROUP BY st.topic
|
||||
"""
|
||||
params["comment_kind"] = ReactionKind.COMMENT.value
|
||||
params["comment_kind"] = int(ReactionKind.COMMENT.value)
|
||||
comments_stats = {row[0]: row[1] for row in session.execute(text(comments_stats_query), params)}
|
||||
|
||||
# Формируем результат с добавлением статистики
|
||||
@@ -314,7 +312,7 @@ async def invalidate_topics_cache(topic_id: Optional[int] = None) -> None:
|
||||
|
||||
# Получаем slug темы, если есть
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.id == topic_id).first()
|
||||
topic = session.query(Topic).where(Topic.id == topic_id).first()
|
||||
if topic and topic.slug:
|
||||
specific_keys.append(f"topic:slug:{topic.slug}")
|
||||
|
||||
@@ -418,7 +416,7 @@ async def create_topic(_: None, _info: GraphQLResolveInfo, topic_input: dict[str
|
||||
async def update_topic(_: None, _info: GraphQLResolveInfo, topic_input: dict[str, Any]) -> dict[str, Any]:
|
||||
slug = topic_input["slug"]
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||||
topic = session.query(Topic).where(Topic.slug == slug).first()
|
||||
if not topic:
|
||||
return {"error": "topic not found"}
|
||||
old_slug = str(getattr(topic, "slug", ""))
|
||||
@@ -443,10 +441,10 @@ async def update_topic(_: None, _info: GraphQLResolveInfo, topic_input: dict[str
|
||||
async def delete_topic(_: None, info: GraphQLResolveInfo, slug: str) -> dict[str, Any]:
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||||
topic = session.query(Topic).where(Topic.slug == slug).first()
|
||||
if not topic:
|
||||
return {"error": "invalid topic slug"}
|
||||
author = session.query(Author).filter(Author.id == viewer_id).first()
|
||||
author = session.query(Author).where(Author.id == viewer_id).first()
|
||||
if author:
|
||||
if getattr(topic, "created_by", None) != author.id:
|
||||
return {"error": "access denied"}
|
||||
@@ -496,11 +494,11 @@ async def delete_topic_by_id(_: None, info: GraphQLResolveInfo, topic_id: int) -
|
||||
"""
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
with local_session() as session:
|
||||
topic = session.query(Topic).filter(Topic.id == topic_id).first()
|
||||
topic = session.query(Topic).where(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
return {"success": False, "message": "Топик не найден"}
|
||||
|
||||
author = session.query(Author).filter(Author.id == viewer_id).first()
|
||||
author = session.query(Author).where(Author.id == viewer_id).first()
|
||||
if not author:
|
||||
return {"success": False, "message": "Не авторизован"}
|
||||
|
||||
@@ -512,8 +510,8 @@ async def delete_topic_by_id(_: None, info: GraphQLResolveInfo, topic_id: int) -
|
||||
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.query(TopicFollower).where(TopicFollower.topic == topic_id).delete()
|
||||
session.query(ShoutTopic).where(ShoutTopic.topic == topic_id).delete()
|
||||
|
||||
# Удаляем сам топик
|
||||
session.delete(topic)
|
||||
@@ -573,12 +571,12 @@ async def merge_topics(_: None, info: GraphQLResolveInfo, merge_input: dict[str,
|
||||
with local_session() as session:
|
||||
try:
|
||||
# Получаем целевую тему
|
||||
target_topic = session.query(Topic).filter(Topic.id == target_topic_id).first()
|
||||
target_topic = session.query(Topic).where(Topic.id == target_topic_id).first()
|
||||
if not target_topic:
|
||||
return {"error": f"Целевая тема с ID {target_topic_id} не найдена"}
|
||||
|
||||
# Получаем исходные темы
|
||||
source_topics = session.query(Topic).filter(Topic.id.in_(source_topic_ids)).all()
|
||||
source_topics = session.query(Topic).where(Topic.id.in_(source_topic_ids)).all()
|
||||
if len(source_topics) != len(source_topic_ids):
|
||||
found_ids = [t.id for t in source_topics]
|
||||
missing_ids = [topic_id for topic_id in source_topic_ids if topic_id not in found_ids]
|
||||
@@ -591,7 +589,7 @@ async def merge_topics(_: None, info: GraphQLResolveInfo, merge_input: dict[str,
|
||||
return {"error": f"Тема '{source_topic.title}' принадлежит другому сообществу"}
|
||||
|
||||
# Получаем автора для проверки прав
|
||||
author = session.query(Author).filter(Author.id == viewer_id).first()
|
||||
author = session.query(Author).where(Author.id == viewer_id).first()
|
||||
if not author:
|
||||
return {"error": "Автор не найден"}
|
||||
|
||||
@@ -604,17 +602,17 @@ async def merge_topics(_: None, info: GraphQLResolveInfo, merge_input: dict[str,
|
||||
# Переносим подписчиков из исходных тем в целевую
|
||||
for source_topic in source_topics:
|
||||
# Получаем подписчиков исходной темы
|
||||
source_followers = session.query(TopicFollower).filter(TopicFollower.topic == source_topic.id).all()
|
||||
source_followers = session.query(TopicFollower).where(TopicFollower.topic == source_topic.id).all()
|
||||
|
||||
for follower in source_followers:
|
||||
# Проверяем, не подписан ли уже пользователь на целевую тему
|
||||
existing = (
|
||||
existing_follower = (
|
||||
session.query(TopicFollower)
|
||||
.filter(TopicFollower.topic == target_topic_id, TopicFollower.follower == follower.follower)
|
||||
.where(TopicFollower.topic == target_topic_id, TopicFollower.follower == follower.follower)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
if not existing_follower:
|
||||
# Создаем новую подписку на целевую тему
|
||||
new_follower = TopicFollower(
|
||||
topic=target_topic_id,
|
||||
@@ -629,21 +627,20 @@ async def merge_topics(_: None, info: GraphQLResolveInfo, merge_input: dict[str,
|
||||
session.delete(follower)
|
||||
|
||||
# Переносим публикации из исходных тем в целевую
|
||||
from orm.shout import ShoutTopic
|
||||
|
||||
for source_topic in source_topics:
|
||||
# Получаем связи публикаций с исходной темой
|
||||
shout_topics = session.query(ShoutTopic).filter(ShoutTopic.topic == source_topic.id).all()
|
||||
shout_topics = session.query(ShoutTopic).where(ShoutTopic.topic == source_topic.id).all()
|
||||
|
||||
for shout_topic in shout_topics:
|
||||
# Проверяем, не связана ли уже публикация с целевой темой
|
||||
existing = (
|
||||
existing_shout_topic: ShoutTopic | None = (
|
||||
session.query(ShoutTopic)
|
||||
.filter(ShoutTopic.topic == target_topic_id, ShoutTopic.shout == shout_topic.shout)
|
||||
.where(ShoutTopic.topic == target_topic_id)
|
||||
.where(ShoutTopic.shout == shout_topic.shout)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
if not existing_shout_topic:
|
||||
# Создаем новую связь с целевой темой
|
||||
new_shout_topic = ShoutTopic(
|
||||
topic=target_topic_id, shout=shout_topic.shout, main=shout_topic.main
|
||||
@@ -654,25 +651,23 @@ async def merge_topics(_: None, info: GraphQLResolveInfo, merge_input: dict[str,
|
||||
# Удаляем старую связь
|
||||
session.delete(shout_topic)
|
||||
|
||||
# Переносим черновики из исходных тем в целевую
|
||||
from orm.draft import DraftTopic
|
||||
|
||||
for source_topic in source_topics:
|
||||
# Получаем связи черновиков с исходной темой
|
||||
draft_topics = session.query(DraftTopic).filter(DraftTopic.topic == source_topic.id).all()
|
||||
draft_topics = session.query(DraftTopic).where(DraftTopic.topic == source_topic.id).all()
|
||||
|
||||
for draft_topic in draft_topics:
|
||||
# Проверяем, не связан ли уже черновик с целевой темой
|
||||
existing = (
|
||||
existing_draft_topic: DraftTopic | None = (
|
||||
session.query(DraftTopic)
|
||||
.filter(DraftTopic.topic == target_topic_id, DraftTopic.shout == draft_topic.shout)
|
||||
.where(DraftTopic.topic == target_topic_id)
|
||||
.where(DraftTopic.draft == draft_topic.draft)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
if not existing_draft_topic:
|
||||
# Создаем новую связь с целевой темой
|
||||
new_draft_topic = DraftTopic(
|
||||
topic=target_topic_id, shout=draft_topic.shout, main=draft_topic.main
|
||||
topic=target_topic_id, draft=draft_topic.draft, main=draft_topic.main
|
||||
)
|
||||
session.add(new_draft_topic)
|
||||
merge_stats["drafts_moved"] += 1
|
||||
@@ -760,7 +755,7 @@ async def set_topic_parent(
|
||||
with local_session() as session:
|
||||
try:
|
||||
# Получаем тему
|
||||
topic = session.query(Topic).filter(Topic.id == topic_id).first()
|
||||
topic = session.query(Topic).where(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
return {"error": f"Тема с ID {topic_id} не найдена"}
|
||||
|
||||
@@ -778,7 +773,7 @@ async def set_topic_parent(
|
||||
}
|
||||
|
||||
# Получаем родительскую тему
|
||||
parent_topic = session.query(Topic).filter(Topic.id == parent_id).first()
|
||||
parent_topic = session.query(Topic).where(Topic.id == parent_id).first()
|
||||
if not parent_topic:
|
||||
return {"error": f"Родительская тема с ID {parent_id} не найдена"}
|
||||
|
||||
|
Reference in New Issue
Block a user