This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
Админ-резолверы - тонкие GraphQL обёртки над AdminService
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLError, GraphQLResolveInfo
|
||||
from sqlalchemy import and_, case, func, or_
|
||||
@@ -21,6 +22,7 @@ from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_
|
||||
from services.admin import AdminService
|
||||
from services.common_result import handle_error
|
||||
from services.db import local_session
|
||||
from services.rbac import update_all_communities_permissions
|
||||
from services.redis import redis
|
||||
from services.schema import mutation, query
|
||||
from utils.logger import root_logger as logger
|
||||
@@ -66,7 +68,7 @@ async def admin_get_shouts(
|
||||
offset: int = 0,
|
||||
search: str = "",
|
||||
status: str = "all",
|
||||
community: Optional[int] = None,
|
||||
community: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Получает список публикаций"""
|
||||
try:
|
||||
@@ -85,7 +87,8 @@ async def admin_update_shout(_: None, info: GraphQLResolveInfo, shout: dict[str,
|
||||
return {"success": False, "error": "ID публикации не указан"}
|
||||
|
||||
shout_input = {k: v for k, v in shout.items() if k != "id"}
|
||||
result = await update_shout(None, info, shout_id, shout_input)
|
||||
title = shout_input.get("title")
|
||||
result = await update_shout(None, info, shout_id, title)
|
||||
|
||||
if result.error:
|
||||
return {"success": False, "error": result.error}
|
||||
@@ -464,8 +467,6 @@ async def admin_get_roles(_: None, _info: GraphQLResolveInfo, community: int | N
|
||||
|
||||
# Если указано сообщество, добавляем кастомные роли из Redis
|
||||
if community:
|
||||
import json
|
||||
|
||||
custom_roles_data = await redis.execute("HGETALL", f"community:custom_roles:{community}")
|
||||
|
||||
for role_id, role_json in custom_roles_data.items():
|
||||
@@ -841,8 +842,6 @@ async def admin_create_custom_role(_: None, _info: GraphQLResolveInfo, role: dic
|
||||
}
|
||||
|
||||
# Сохраняем роль в Redis
|
||||
import json
|
||||
|
||||
await redis.execute("HSET", f"community:custom_roles:{community_id}", role_id, json.dumps(role_data))
|
||||
|
||||
logger.info(f"Создана новая роль {role_id} для сообщества {community_id}")
|
||||
@@ -887,8 +886,6 @@ async def admin_delete_custom_role(
|
||||
async def admin_update_permissions(_: None, _info: GraphQLResolveInfo) -> dict[str, Any]:
|
||||
"""Обновляет права для всех сообществ с новыми дефолтными настройками"""
|
||||
try:
|
||||
from services.rbac import update_all_communities_permissions
|
||||
|
||||
await update_all_communities_permissions()
|
||||
|
||||
logger.info("Права для всех сообществ обновлены")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Auth резолверы - тонкие GraphQL обёртки над AuthService
|
||||
"""
|
||||
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from starlette.responses import JSONResponse
|
||||
@@ -16,7 +16,7 @@ from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@type_author.field("roles")
|
||||
def resolve_roles(obj: Union[dict, Any], info: GraphQLResolveInfo) -> list[str]:
|
||||
def resolve_roles(obj: dict | Any, info: GraphQLResolveInfo) -> list[str]:
|
||||
"""Резолвер для поля roles автора"""
|
||||
try:
|
||||
if hasattr(obj, "get_roles"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Optional, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import and_, asc, func, select, text
|
||||
@@ -46,18 +46,18 @@ class AuthorsBy(TypedDict, total=False):
|
||||
stat: Поле статистики
|
||||
"""
|
||||
|
||||
last_seen: Optional[int]
|
||||
created_at: Optional[int]
|
||||
slug: Optional[str]
|
||||
name: Optional[str]
|
||||
topic: Optional[str]
|
||||
order: Optional[str]
|
||||
after: Optional[int]
|
||||
stat: Optional[str]
|
||||
last_seen: int | None
|
||||
created_at: int | None
|
||||
slug: str | None
|
||||
name: str | None
|
||||
topic: str | None
|
||||
order: str | None
|
||||
after: int | None
|
||||
stat: str | None
|
||||
|
||||
|
||||
# Вспомогательная функция для получения всех авторов без статистики
|
||||
async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
|
||||
async def get_all_authors(current_user_id: int | None = None) -> list[Any]:
|
||||
"""
|
||||
Получает всех авторов без статистики.
|
||||
Используется для случаев, когда нужен полный список авторов без дополнительной информации.
|
||||
@@ -92,7 +92,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
|
||||
limit: int = 10, offset: int = 0, by: AuthorsBy | None = None, current_user_id: int | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Получает авторов со статистикой с пагинацией.
|
||||
@@ -367,7 +367,7 @@ async def get_authors_all(_: None, info: GraphQLResolveInfo) -> list[Any]:
|
||||
|
||||
@query.field("get_author")
|
||||
async def get_author(
|
||||
_: None, info: GraphQLResolveInfo, slug: Optional[str] = None, author_id: Optional[int] = None
|
||||
_: None, info: GraphQLResolveInfo, slug: str | None = None, author_id: int | None = None
|
||||
) -> dict[str, Any] | None:
|
||||
"""Get specific author by slug or ID"""
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
@@ -451,8 +451,8 @@ async def load_authors_search(_: None, info: GraphQLResolveInfo, **kwargs: Any)
|
||||
|
||||
|
||||
def get_author_id_from(
|
||||
slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
|
||||
) -> Optional[int]:
|
||||
slug: str | None = None, user: str | None = None, author_id: int | None = None
|
||||
) -> int | None:
|
||||
"""Get author ID from different identifiers"""
|
||||
try:
|
||||
if author_id:
|
||||
@@ -474,7 +474,7 @@ def get_author_id_from(
|
||||
|
||||
@query.field("get_author_follows")
|
||||
async def get_author_follows(
|
||||
_, info: GraphQLResolveInfo, slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
|
||||
_, info: GraphQLResolveInfo, slug: str | None = None, user: str | None = None, author_id: int | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Get entities followed by author"""
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
@@ -519,9 +519,9 @@ async def get_author_follows(
|
||||
async def get_author_follows_topics(
|
||||
_,
|
||||
_info: GraphQLResolveInfo,
|
||||
slug: Optional[str] = None,
|
||||
user: Optional[str] = None,
|
||||
author_id: Optional[int] = None,
|
||||
slug: str | None = None,
|
||||
user: str | None = None,
|
||||
author_id: int | None = None,
|
||||
) -> list[Any]:
|
||||
"""Get topics followed by author"""
|
||||
logger.debug(f"getting followed topics for @{slug}")
|
||||
@@ -537,7 +537,7 @@ async def get_author_follows_topics(
|
||||
|
||||
@query.field("get_author_follows_authors")
|
||||
async def get_author_follows_authors(
|
||||
_, info: GraphQLResolveInfo, slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
|
||||
_, info: GraphQLResolveInfo, slug: str | None = None, user: str | None = None, author_id: int | None = None
|
||||
) -> list[Any]:
|
||||
"""Get authors followed by author"""
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
|
||||
@@ -40,8 +40,7 @@ def load_shouts_bookmarked(_: None, info, options) -> list[Shout]:
|
||||
)
|
||||
)
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
shouts = get_shouts_with_links(info, q, limit, offset)
|
||||
return shouts
|
||||
return get_shouts_with_links(info, q, limit, offset)
|
||||
|
||||
|
||||
@mutation.field("toggle_bookmark_shout")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
import orjson
|
||||
from graphql import GraphQLResolveInfo
|
||||
@@ -8,6 +8,12 @@ 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
|
||||
@@ -383,16 +389,15 @@ def patch_topics(session: Any, shout: Any, topics_input: list[Any]) -> None:
|
||||
# @mutation.field("update_shout")
|
||||
# @login_required
|
||||
async def update_shout(
|
||||
_: None, info: GraphQLResolveInfo, shout_id: int, shout_input: dict | None = None, *, publish: bool = False
|
||||
_: None,
|
||||
info: GraphQLResolveInfo,
|
||||
shout_id: int,
|
||||
title: str | None = None,
|
||||
body: str | None = None,
|
||||
topics: List[str] | None = None,
|
||||
collections: List[int] | 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}")
|
||||
|
||||
@@ -403,12 +408,9 @@ async def update_shout(
|
||||
return CommonResult(error="unauthorized", shout=None)
|
||||
|
||||
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
|
||||
logger.debug(f"Full shout_input: {shout_input}") # DraftInput
|
||||
roles = info.context.get("roles", [])
|
||||
current_time = int(time.time())
|
||||
shout_input = shout_input or {}
|
||||
shout_id = shout_id or shout_input.get("id", shout_id)
|
||||
slug = shout_input.get("slug")
|
||||
slug = title # Используем title как slug если он передан
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
@@ -442,17 +444,18 @@ async def update_shout(
|
||||
c += 1
|
||||
same_slug_shout.slug = f"{slug}-{c}" # type: ignore[assignment]
|
||||
same_slug_shout = session.query(Shout).where(Shout.slug == slug).first()
|
||||
shout_input["slug"] = slug
|
||||
shout_by_id.slug = slug
|
||||
logger.info(f"shout#{shout_id} slug patched")
|
||||
|
||||
if filter(lambda x: x.id == author_id, list(shout_by_id.authors)) or "editor" in roles:
|
||||
logger.info(f"Author #{author_id} has permission to edit shout#{shout_id}")
|
||||
|
||||
# topics patch
|
||||
topics_input = shout_input.get("topics")
|
||||
if topics_input:
|
||||
logger.info(f"Received topics_input for shout#{shout_id}: {topics_input}")
|
||||
if topics:
|
||||
logger.info(f"Received topics for shout#{shout_id}: {topics}")
|
||||
try:
|
||||
# Преобразуем topics в формат для patch_topics
|
||||
topics_input = [{"id": int(t)} for t in topics if t.isdigit()]
|
||||
patch_topics(session, shout_by_id, topics_input)
|
||||
logger.info(f"Successfully patched topics for shout#{shout_id}")
|
||||
|
||||
@@ -463,17 +466,16 @@ async def update_shout(
|
||||
logger.error(f"Error patching topics: {e}", exc_info=True)
|
||||
return CommonResult(error=f"Failed to update topics: {e!s}", shout=None)
|
||||
|
||||
del shout_input["topics"]
|
||||
for tpc in topics_input:
|
||||
await cache_by_id(Topic, tpc["id"], cache_topic)
|
||||
else:
|
||||
logger.warning(f"No topics_input received for shout#{shout_id}")
|
||||
logger.warning(f"No topics received for shout#{shout_id}")
|
||||
|
||||
# main topic
|
||||
main_topic = shout_input.get("main_topic")
|
||||
if main_topic:
|
||||
logger.info(f"Updating main topic for shout#{shout_id} to {main_topic}")
|
||||
patch_main_topic(session, main_topic, shout_by_id)
|
||||
# Обновляем title и body если переданы
|
||||
if title:
|
||||
shout_by_id.title = title
|
||||
if body:
|
||||
shout_by_id.body = body
|
||||
|
||||
shout_by_id.updated_at = current_time # type: ignore[assignment]
|
||||
if publish:
|
||||
@@ -497,8 +499,8 @@ async def update_shout(
|
||||
logger.info("Author link already exists")
|
||||
|
||||
# Логируем финальное состояние перед сохранением
|
||||
logger.info(f"Final shout_input for update: {shout_input}")
|
||||
Shout.update(shout_by_id, shout_input)
|
||||
logger.info(f"Final shout_input for update: {shout_by_id.dict()}")
|
||||
Shout.update(shout_by_id, shout_by_id.dict())
|
||||
session.add(shout_by_id)
|
||||
|
||||
try:
|
||||
@@ -572,11 +574,6 @@ 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:
|
||||
@@ -667,12 +664,6 @@ 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", [])
|
||||
|
||||
@@ -6,6 +6,12 @@ from graphql import GraphQLResolveInfo
|
||||
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
|
||||
@@ -36,14 +42,6 @@ async def follow(
|
||||
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),
|
||||
@@ -173,14 +171,6 @@ async def unfollow(
|
||||
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),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from graphql import GraphQLResolveInfo
|
||||
@@ -400,7 +400,7 @@ def apply_filters(q: Select, filters: dict[str, Any]) -> Select:
|
||||
|
||||
|
||||
@query.field("get_shout")
|
||||
async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id: int = 0) -> Optional[Shout]:
|
||||
async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id: int = 0) -> Shout | None:
|
||||
"""
|
||||
Получение публикации по slug или id.
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, distinct, func, join, select
|
||||
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
|
||||
@@ -362,10 +363,8 @@ 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:
|
||||
author_query = select(Author).where(Author.id == author_id)
|
||||
result = get_with_stat(author_query)
|
||||
if result:
|
||||
author_with_stat = result[0]
|
||||
@@ -436,7 +435,7 @@ def get_following_count(entity_type: str, entity_id: int) -> int:
|
||||
|
||||
|
||||
def get_shouts_count(
|
||||
author_id: Optional[int] = None, topic_id: Optional[int] = None, community_id: Optional[int] = None
|
||||
author_id: int | None = None, topic_id: int | None = None, community_id: int | None = None
|
||||
) -> int:
|
||||
"""Получает количество публикаций"""
|
||||
try:
|
||||
@@ -458,7 +457,7 @@ def get_shouts_count(
|
||||
return 0
|
||||
|
||||
|
||||
def get_authors_count(community_id: Optional[int] = None) -> int:
|
||||
def get_authors_count(community_id: int | None = None) -> int:
|
||||
"""Получает количество авторов"""
|
||||
try:
|
||||
with local_session() as session:
|
||||
@@ -479,7 +478,7 @@ def get_authors_count(community_id: Optional[int] = None) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def get_topics_count(author_id: Optional[int] = None) -> int:
|
||||
def get_topics_count(author_id: int | None = None) -> int:
|
||||
"""Получает количество топиков"""
|
||||
try:
|
||||
with local_session() as session:
|
||||
@@ -509,7 +508,7 @@ def get_communities_count() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def get_reactions_count(shout_id: Optional[int] = None, author_id: Optional[int] = None) -> int:
|
||||
def get_reactions_count(shout_id: int | None = None, author_id: int | None = None) -> int:
|
||||
"""Получает количество реакций"""
|
||||
try:
|
||||
with local_session() as session:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from math import ceil
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from sqlalchemy import desc, func, select, text
|
||||
@@ -55,7 +55,7 @@ async def get_all_topics() -> list[Any]:
|
||||
|
||||
# Вспомогательная функция для получения тем со статистикой с пагинацией
|
||||
async def get_topics_with_stats(
|
||||
limit: int = 100, offset: int = 0, community_id: Optional[int] = None, by: Optional[str] = None
|
||||
limit: int = 100, offset: int = 0, community_id: int | None = None, by: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Получает темы со статистикой с пагинацией.
|
||||
@@ -292,7 +292,7 @@ async def get_topics_with_stats(
|
||||
|
||||
|
||||
# Функция для инвалидации кеша тем
|
||||
async def invalidate_topics_cache(topic_id: Optional[int] = None) -> None:
|
||||
async def invalidate_topics_cache(topic_id: int | None = None) -> None:
|
||||
"""
|
||||
Инвалидирует кеши тем при изменении данных.
|
||||
|
||||
@@ -350,7 +350,7 @@ async def get_topics_all(_: None, _info: GraphQLResolveInfo) -> list[Any]:
|
||||
# Запрос на получение тем по сообществу
|
||||
@query.field("get_topics_by_community")
|
||||
async def get_topics_by_community(
|
||||
_: None, _info: GraphQLResolveInfo, community_id: int, limit: int = 100, offset: int = 0, by: Optional[str] = None
|
||||
_: None, _info: GraphQLResolveInfo, community_id: int, limit: int = 100, offset: int = 0, by: str | None = None
|
||||
) -> list[Any]:
|
||||
"""
|
||||
Получает список тем, принадлежащих указанному сообществу с пагинацией и статистикой.
|
||||
@@ -386,7 +386,7 @@ async def get_topics_by_author(
|
||||
|
||||
# Запрос на получение одной темы по её slug
|
||||
@query.field("get_topic")
|
||||
async def get_topic(_: None, _info: GraphQLResolveInfo, slug: str) -> Optional[Any]:
|
||||
async def get_topic(_: None, _info: GraphQLResolveInfo, slug: str) -> Any | None:
|
||||
topic = await get_cached_topic_by_slug(slug, get_with_stat)
|
||||
if topic:
|
||||
return topic
|
||||
|
||||
Reference in New Issue
Block a user