2025-07-31 18:55:59 +03:00
|
|
|
|
import asyncio
|
2024-10-21 11:29:57 +03:00
|
|
|
|
import time
|
2025-07-02 22:30:21 +03:00
|
|
|
|
from typing import Any, Dict
|
2023-10-23 17:47:11 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
from sqlalchemy import (
|
|
|
|
|
|
JSON,
|
|
|
|
|
|
Boolean,
|
|
|
|
|
|
ForeignKey,
|
|
|
|
|
|
Index,
|
|
|
|
|
|
Integer,
|
|
|
|
|
|
PrimaryKeyConstraint,
|
|
|
|
|
|
String,
|
|
|
|
|
|
UniqueConstraint,
|
|
|
|
|
|
distinct,
|
|
|
|
|
|
func,
|
2025-08-17 17:56:31 +03:00
|
|
|
|
text,
|
2025-07-31 18:55:59 +03:00
|
|
|
|
)
|
2024-10-21 11:48:51 +03:00
|
|
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
2025-07-31 18:55:59 +03:00
|
|
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
2024-10-21 11:48:51 +03:00
|
|
|
|
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`
### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода
### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки
### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации
### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
|
|
|
|
from orm.author import Author
|
2025-07-25 01:04:15 +03:00
|
|
|
|
from orm.base import BaseModel
|
2025-08-17 17:56:31 +03:00
|
|
|
|
from rbac.interface import get_rbac_operations
|
|
|
|
|
|
from storage.db import local_session
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
# Словарь названий ролей
|
|
|
|
|
|
role_names = {
|
|
|
|
|
|
"reader": "Читатель",
|
|
|
|
|
|
"author": "Автор",
|
|
|
|
|
|
"artist": "Художник",
|
|
|
|
|
|
"expert": "Эксперт",
|
|
|
|
|
|
"editor": "Редактор",
|
|
|
|
|
|
"admin": "Администратор",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Словарь описаний ролей
|
|
|
|
|
|
role_descriptions = {
|
|
|
|
|
|
"reader": "Может читать и комментировать",
|
|
|
|
|
|
"author": "Может создавать публикации",
|
|
|
|
|
|
"artist": "Может быть credited artist",
|
|
|
|
|
|
"expert": "Может добавлять доказательства",
|
|
|
|
|
|
"editor": "Может модерировать контент",
|
|
|
|
|
|
"admin": "Полные права",
|
|
|
|
|
|
}
|
2023-10-23 17:47:11 +03:00
|
|
|
|
|
2021-08-27 00:14:20 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
class CommunityFollower(BaseModel):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Простая подписка пользователя на сообщество.
|
2024-10-21 11:29:57 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
Использует обычный id как первичный ключ для простоты и производительности.
|
|
|
|
|
|
Уникальность обеспечивается индексом по (community, follower).
|
|
|
|
|
|
"""
|
2022-06-12 10:51:22 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
__tablename__ = "community_follower"
|
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
community: Mapped[int] = mapped_column(Integer, ForeignKey("community.id"), nullable=False, index=True)
|
2025-08-17 16:33:54 +03:00
|
|
|
|
follower: Mapped[int] = mapped_column(Integer, ForeignKey("author.id"), nullable=False, index=True)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
# Уникальность по паре сообщество-подписчик
|
|
|
|
|
|
__table_args__ = (
|
2025-07-31 18:55:59 +03:00
|
|
|
|
PrimaryKeyConstraint("community", "follower"),
|
2025-07-02 22:30:21 +03:00
|
|
|
|
{"extend_existing": True},
|
|
|
|
|
|
)
|
2024-10-21 11:48:51 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
def __init__(self, community: int, follower: int) -> None:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
self.community = community
|
|
|
|
|
|
self.follower = follower
|
2022-06-12 10:51:22 +03:00
|
|
|
|
|
2021-08-27 00:14:20 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
class Community(BaseModel):
|
2024-04-17 18:32:23 +03:00
|
|
|
|
__tablename__ = "community"
|
2022-09-03 13:50:14 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
|
|
|
|
slug: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
|
|
|
|
|
desc: Mapped[str] = mapped_column(String, nullable=False, default="")
|
|
|
|
|
|
pic: Mapped[str | None] = mapped_column(String, nullable=False, default="")
|
|
|
|
|
|
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
|
|
|
|
|
|
created_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
settings: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
|
|
|
|
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
deleted_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
private: Mapped[bool] = mapped_column(Boolean, default=False)
|
2024-10-21 10:52:23 +03:00
|
|
|
|
|
2024-10-21 11:08:16 +03:00
|
|
|
|
@hybrid_property
|
|
|
|
|
|
def stat(self):
|
|
|
|
|
|
return CommunityStats(self)
|
2024-10-21 10:52:23 +03:00
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
def is_followed_by(self, author_id: int) -> bool:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""Проверяет, подписан ли пользователь на сообщество"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
follower = (
|
|
|
|
|
|
session.query(CommunityFollower)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
.where(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
return follower is not None
|
|
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
def get_user_roles(self, user_id: int) -> list[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает роли пользователя в данном сообществе через CommunityAuthor
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список ролей пользователя в сообществе
|
|
|
|
|
|
"""
|
2025-06-02 02:56:11 +03:00
|
|
|
|
with local_session() as session:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
community_author = (
|
|
|
|
|
|
session.query(CommunityAuthor)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return community_author.role_list if community_author else []
|
|
|
|
|
|
|
|
|
|
|
|
def has_user_role(self, user_id: int, role_id: str) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Проверяет, есть ли у пользователя указанная роль в этом сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
role_id: ID роли
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
True если роль есть, False если нет
|
|
|
|
|
|
"""
|
|
|
|
|
|
user_roles = self.get_user_roles(user_id)
|
|
|
|
|
|
return role_id in user_roles
|
|
|
|
|
|
|
|
|
|
|
|
def add_user_role(self, user_id: int, role: str) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Добавляет роль пользователю в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
role: Название роли
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
# Ищем существующую запись
|
|
|
|
|
|
community_author = (
|
|
|
|
|
|
session.query(CommunityAuthor)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if community_author:
|
|
|
|
|
|
# Добавляем роль к существующей записи
|
|
|
|
|
|
community_author.add_role(role)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Создаем новую запись
|
|
|
|
|
|
community_author = CommunityAuthor(community_id=self.id, author_id=user_id, roles=role)
|
|
|
|
|
|
session.add(community_author)
|
|
|
|
|
|
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
def remove_user_role(self, user_id: int, role: str) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Удаляет роль у пользователя в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
role: Название роли
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
community_author = (
|
|
|
|
|
|
session.query(CommunityAuthor)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if community_author:
|
|
|
|
|
|
community_author.remove_role(role)
|
|
|
|
|
|
|
|
|
|
|
|
# Если ролей не осталось, удаляем запись
|
|
|
|
|
|
if not community_author.role_list:
|
|
|
|
|
|
session.delete(community_author)
|
|
|
|
|
|
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
def set_user_roles(self, user_id: int, roles: list[str]) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Устанавливает полный список ролей пользователя в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
roles: Список ролей для установки
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
# Ищем существующую запись
|
|
|
|
|
|
community_author = (
|
|
|
|
|
|
session.query(CommunityAuthor)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
.first()
|
|
|
|
|
|
)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
if community_author:
|
|
|
|
|
|
if roles:
|
|
|
|
|
|
# Обновляем роли
|
|
|
|
|
|
community_author.set_roles(roles)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Если ролей нет, удаляем запись
|
|
|
|
|
|
session.delete(community_author)
|
|
|
|
|
|
elif roles:
|
|
|
|
|
|
# Создаем новую запись, если есть роли
|
|
|
|
|
|
community_author = CommunityAuthor(community_id=self.id, author_id=user_id)
|
|
|
|
|
|
community_author.set_roles(roles)
|
|
|
|
|
|
session.add(community_author)
|
|
|
|
|
|
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
def get_community_members(self, with_roles: bool = False) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список участников сообщества
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
with_roles: Если True, включает информацию о ролях
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список участников с информацией о ролях
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
community_authors = session.query(CommunityAuthor).where(CommunityAuthor.community_id == self.id).all()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
members = []
|
|
|
|
|
|
for ca in community_authors:
|
2025-09-01 00:13:46 +03:00
|
|
|
|
member_info: dict[str, Any] = {
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"author_id": ca.author_id,
|
|
|
|
|
|
"joined_at": ca.joined_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if with_roles:
|
2025-09-01 00:13:46 +03:00
|
|
|
|
member_info["roles"] = ca.role_list
|
2025-07-02 22:49:20 +03:00
|
|
|
|
# Получаем разрешения синхронно
|
|
|
|
|
|
try:
|
2025-09-01 00:13:46 +03:00
|
|
|
|
member_info["permissions"] = asyncio.run(ca.get_permissions())
|
2025-07-02 22:49:20 +03:00
|
|
|
|
except Exception:
|
|
|
|
|
|
# Если не удается получить разрешения асинхронно, используем пустой список
|
2025-09-01 00:13:46 +03:00
|
|
|
|
member_info["permissions"] = []
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
members.append(member_info)
|
|
|
|
|
|
|
|
|
|
|
|
return members
|
|
|
|
|
|
|
|
|
|
|
|
def assign_default_roles_to_user(self, user_id: int) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Назначает дефолтные роли новому пользователю в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
|
"""
|
|
|
|
|
|
default_roles = self.get_default_roles()
|
|
|
|
|
|
self.set_user_roles(user_id, default_roles)
|
|
|
|
|
|
|
|
|
|
|
|
def get_default_roles(self) -> list[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список дефолтных ролей для новых пользователей в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список ID ролей, которые назначаются новым пользователям по умолчанию
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.settings:
|
|
|
|
|
|
return ["reader", "author"] # По умолчанию базовые роли
|
|
|
|
|
|
|
|
|
|
|
|
return self.settings.get("default_roles", ["reader", "author"])
|
|
|
|
|
|
|
|
|
|
|
|
def set_default_roles(self, roles: list[str]) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Устанавливает дефолтные роли для новых пользователей в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
roles: Список ID ролей для назначения по умолчанию
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.settings:
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.settings = {}
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.settings["default_roles"] = roles
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
async def initialize_role_permissions(self) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Инициализирует права ролей для сообщества из дефолтных настроек.
|
|
|
|
|
|
Вызывается при создании нового сообщества.
|
|
|
|
|
|
"""
|
2025-08-17 16:33:54 +03:00
|
|
|
|
rbac_ops = get_rbac_operations()
|
|
|
|
|
|
await rbac_ops.initialize_community_permissions(int(self.id))
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
def get_available_roles(self) -> list[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список доступных ролей в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список ID ролей, которые могут быть назначены в этом сообществе
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.settings:
|
|
|
|
|
|
return ["reader", "author", "artist", "expert", "editor", "admin"] # Все стандартные роли
|
|
|
|
|
|
|
|
|
|
|
|
return self.settings.get("available_roles", ["reader", "author", "artist", "expert", "editor", "admin"])
|
|
|
|
|
|
|
|
|
|
|
|
def set_available_roles(self, roles: list[str]) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Устанавливает список доступных ролей в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
roles: Список ID ролей, доступных в сообществе
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.settings:
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.settings = {}
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.settings["available_roles"] = roles
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
def set_slug(self, slug: str) -> None:
|
|
|
|
|
|
"""Устанавливает slug сообщества"""
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.update({"slug": slug})
|
2024-10-21 16:59:25 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def get_followers(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список подписчиков сообщества.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
list: Список ID авторов, подписанных на сообщество
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
return [
|
|
|
|
|
|
follower.id
|
|
|
|
|
|
for follower in session.query(Author)
|
|
|
|
|
|
.join(CommunityFollower, Author.id == CommunityFollower.follower)
|
|
|
|
|
|
.where(CommunityFollower.community == self.id)
|
|
|
|
|
|
.all()
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def add_community_creator(self, author_id: int) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Создатель сообщества
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
author_id: ID пользователя, которому назначаются права
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
# Проверяем существование связи
|
|
|
|
|
|
existing = CommunityAuthor.find_author_in_community(author_id, self.id, session)
|
|
|
|
|
|
|
|
|
|
|
|
if not existing:
|
|
|
|
|
|
# Создаем нового CommunityAuthor с ролью редактора
|
|
|
|
|
|
community_author = CommunityAuthor(community_id=self.id, author_id=author_id, roles="editor")
|
|
|
|
|
|
session.add(community_author)
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
2024-10-21 10:52:23 +03:00
|
|
|
|
|
2024-10-21 11:08:16 +03:00
|
|
|
|
class CommunityStats:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
def __init__(self, community) -> None:
|
2024-10-21 11:08:16 +03:00
|
|
|
|
self.community = community
|
2024-10-21 10:52:23 +03:00
|
|
|
|
|
2024-10-21 11:08:16 +03:00
|
|
|
|
@property
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def shouts(self) -> int:
|
2025-08-17 17:56:31 +03:00
|
|
|
|
return (
|
|
|
|
|
|
self.community.session.query(func.count(1))
|
|
|
|
|
|
.select_from(text("shout"))
|
|
|
|
|
|
.filter(text("shout.community_id = :community_id"))
|
|
|
|
|
|
.params(community_id=self.community.id)
|
|
|
|
|
|
.scalar()
|
|
|
|
|
|
)
|
2024-10-21 11:08:16 +03:00
|
|
|
|
|
|
|
|
|
|
@property
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def followers(self) -> int:
|
2024-10-21 11:29:57 +03:00
|
|
|
|
return (
|
2025-06-02 02:56:11 +03:00
|
|
|
|
self.community.session.query(func.count(CommunityFollower.follower))
|
2025-08-01 05:17:01 +03:00
|
|
|
|
.filter(CommunityFollower.community == self.community.id)
|
2024-10-21 11:08:16 +03:00
|
|
|
|
.scalar()
|
2024-10-21 11:29:57 +03:00
|
|
|
|
)
|
2024-10-21 16:42:30 +03:00
|
|
|
|
|
|
|
|
|
|
@property
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def authors(self) -> int:
|
2024-10-21 16:57:03 +03:00
|
|
|
|
# author has a shout with community id and its featured_at is not null
|
2024-10-21 16:42:30 +03:00
|
|
|
|
return (
|
|
|
|
|
|
self.community.session.query(func.count(distinct(Author.id)))
|
2025-08-17 17:56:31 +03:00
|
|
|
|
.select_from(text("author"))
|
|
|
|
|
|
.join(text("shout"), text("author.id IN (SELECT author_id FROM shout_author WHERE shout_id = shout.id)"))
|
|
|
|
|
|
.filter(text("shout.community_id = :community_id"), text("shout.featured_at IS NOT NULL"))
|
|
|
|
|
|
.params(community_id=self.community.id)
|
2024-10-21 16:42:30 +03:00
|
|
|
|
.scalar()
|
|
|
|
|
|
)
|
2024-10-21 16:59:25 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
class CommunityAuthor(BaseModel):
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Связь автора с сообществом и его ролями.
|
|
|
|
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
|
id: Уникальный ID записи
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
author_id: ID автора
|
|
|
|
|
|
roles: CSV строка с ролями (например: "reader,author,editor")
|
|
|
|
|
|
joined_at: Время присоединения к сообществу (unix timestamp)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2024-10-21 16:59:25 +03:00
|
|
|
|
__tablename__ = "community_author"
|
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
|
|
community_id: Mapped[int] = mapped_column(Integer, ForeignKey("community.id"), nullable=False)
|
2025-08-17 16:33:54 +03:00
|
|
|
|
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("author.id"), nullable=False)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
roles: Mapped[str | None] = mapped_column(String, nullable=True, comment="Roles (comma-separated)")
|
|
|
|
|
|
joined_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
# Уникальность по сообществу и автору
|
|
|
|
|
|
__table_args__ = (
|
|
|
|
|
|
Index("idx_community_author_community", "community_id"),
|
|
|
|
|
|
Index("idx_community_author_author", "author_id"),
|
|
|
|
|
|
UniqueConstraint("community_id", "author_id", name="uq_community_author"),
|
|
|
|
|
|
{"extend_existing": True},
|
|
|
|
|
|
)
|
2024-10-21 16:59:25 +03:00
|
|
|
|
|
|
|
|
|
|
@property
|
2025-07-02 22:30:21 +03:00
|
|
|
|
def role_list(self) -> list[str]:
|
|
|
|
|
|
"""Получает список ролей как список строк"""
|
|
|
|
|
|
return [role.strip() for role in self.roles.split(",") if role.strip()] if self.roles else []
|
2024-10-21 16:59:25 +03:00
|
|
|
|
|
|
|
|
|
|
@role_list.setter
|
2025-07-02 22:30:21 +03:00
|
|
|
|
def role_list(self, value: list[str]) -> None:
|
|
|
|
|
|
"""Устанавливает список ролей из списка строк"""
|
2025-09-01 00:13:46 +03:00
|
|
|
|
self.update({"roles": ",".join(value) if value else None})
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def add_role(self, role: str) -> None:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
Добавляет роль в список ролей.
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
role (str): Название роли
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
if not self.roles:
|
|
|
|
|
|
self.roles = role
|
|
|
|
|
|
elif role not in self.role_list:
|
|
|
|
|
|
self.roles += f",{role}"
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def remove_role(self, role: str) -> None:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
Удаляет роль из списка ролей.
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
role (str): Название роли
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
if self.roles and role in self.role_list:
|
|
|
|
|
|
roles_list = [r for r in self.role_list if r != role]
|
|
|
|
|
|
self.roles = ",".join(roles_list) if roles_list else None
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def has_role(self, role: str) -> bool:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
Проверяет наличие роли.
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
role (str): Название роли
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True, если роль есть, иначе False
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-07-31 18:55:59 +03:00
|
|
|
|
return bool(self.roles and role in self.role_list)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
def set_roles(self, roles: list[str]) -> None:
|
|
|
|
|
|
"""
|
2025-07-25 01:04:15 +03:00
|
|
|
|
Устанавливает роли для CommunityAuthor.
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
roles: Список ролей для установки
|
|
|
|
|
|
"""
|
2025-07-25 01:04:15 +03:00
|
|
|
|
# Фильтруем и очищаем роли
|
|
|
|
|
|
valid_roles = [role.strip() for role in roles if role and role.strip()]
|
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
# Если список пустой, устанавливаем пустую строку
|
2025-07-25 01:04:15 +03:00
|
|
|
|
self.roles = ",".join(valid_roles) if valid_roles else ""
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
async def get_permissions(self) -> list[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает все разрешения автора на основе его ролей в конкретном сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список разрешений (permissions)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
all_permissions = set()
|
2025-08-17 16:33:54 +03:00
|
|
|
|
rbac_ops = get_rbac_operations()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
for role in self.role_list:
|
2025-08-17 16:33:54 +03:00
|
|
|
|
role_perms = await rbac_ops.get_permissions_for_role(role, int(self.community_id))
|
2025-07-02 22:30:21 +03:00
|
|
|
|
all_permissions.update(role_perms)
|
|
|
|
|
|
|
|
|
|
|
|
return list(all_permissions)
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
def has_permission(self, permission: str) -> bool:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-08-17 16:33:54 +03:00
|
|
|
|
Проверяет, есть ли у пользователя указанное право
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-08-17 16:33:54 +03:00
|
|
|
|
permission: Право для проверки (например, "community:create")
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-08-17 16:33:54 +03:00
|
|
|
|
True если право есть, False если нет
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
2025-08-17 16:33:54 +03:00
|
|
|
|
# Проверяем права через синхронную функцию
|
|
|
|
|
|
try:
|
|
|
|
|
|
# В синхронном контексте не можем использовать await
|
|
|
|
|
|
# Используем fallback на проверку ролей
|
|
|
|
|
|
return permission in self.role_list
|
|
|
|
|
|
except Exception:
|
2025-08-17 17:56:31 +03:00
|
|
|
|
# TODO: Fallback: проверяем роли (старый способ)
|
2025-08-17 16:33:54 +03:00
|
|
|
|
return any(permission == role for role in self.role_list)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
def dict(self, access: bool = False) -> dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Сериализует объект в словарь
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
access: Если True, включает дополнительную информацию
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Словарь с данными объекта
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = {
|
|
|
|
|
|
"id": self.id,
|
|
|
|
|
|
"community_id": self.community_id,
|
|
|
|
|
|
"author_id": self.author_id,
|
|
|
|
|
|
"roles": self.role_list,
|
|
|
|
|
|
"joined_at": self.joined_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if access:
|
|
|
|
|
|
# Note: permissions должны быть получены заранее через await
|
|
|
|
|
|
# Здесь мы не можем использовать await в sync методе
|
|
|
|
|
|
result["permissions"] = [] # Placeholder - нужно получить асинхронно
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_user_communities_with_roles(cls, author_id: int, session=None) -> list[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает все сообщества пользователя с его ролями
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
author_id: ID автора
|
|
|
|
|
|
session: Сессия БД (опционально)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список словарей с информацией о сообществах и ролях
|
|
|
|
|
|
"""
|
|
|
|
|
|
if session is None:
|
|
|
|
|
|
with local_session() as ssession:
|
2025-08-12 16:40:34 +03:00
|
|
|
|
community_authors = ssession.query(cls).where(cls.author_id == author_id).all()
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
"community_id": ca.community_id,
|
|
|
|
|
|
"roles": ca.role_list,
|
|
|
|
|
|
"permissions": [], # Нужно получить асинхронно
|
|
|
|
|
|
"joined_at": ca.joined_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
for ca in community_authors
|
|
|
|
|
|
]
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
community_authors = session.query(cls).where(cls.author_id == author_id).all()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
"community_id": ca.community_id,
|
|
|
|
|
|
"roles": ca.role_list,
|
|
|
|
|
|
"permissions": [], # Нужно получить асинхронно
|
|
|
|
|
|
"joined_at": ca.joined_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
for ca in community_authors
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-07-31 18:55:59 +03:00
|
|
|
|
def find_author_in_community(cls, author_id: int, community_id: int, session=None) -> "CommunityAuthor | None":
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Находит запись CommunityAuthor по ID автора и сообщества
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
author_id: ID автора
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
session: Сессия БД (опционально)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
CommunityAuthor или None
|
|
|
|
|
|
"""
|
|
|
|
|
|
if session is None:
|
|
|
|
|
|
with local_session() as ssession:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
return ssession.query(cls).where(cls.author_id == author_id, cls.community_id == community_id).first()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
return session.query(cls).where(cls.author_id == author_id, cls.community_id == community_id).first()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_users_with_role(cls, community_id: int, role: str, session=None) -> list[int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает список ID пользователей с указанной ролью в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
role: Название роли
|
|
|
|
|
|
session: Сессия БД (опционально)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список ID пользователей
|
|
|
|
|
|
"""
|
|
|
|
|
|
if session is None:
|
|
|
|
|
|
with local_session() as ssession:
|
2025-08-12 16:40:34 +03:00
|
|
|
|
community_authors = ssession.query(cls).where(cls.community_id == community_id).all()
|
|
|
|
|
|
return [ca.author_id for ca in community_authors if ca.has_role(role)]
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
return [ca.author_id for ca in community_authors if ca.has_role(role)]
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_community_stats(cls, community_id: int, session=None) -> Dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает статистику ролей в сообществе
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
session: Сессия БД (опционально)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Словарь со статистикой ролей
|
|
|
|
|
|
"""
|
2025-08-12 18:23:53 +03:00
|
|
|
|
# Загружаем список авторов сообщества (одним способом вне зависимости от сессии)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
if session is None:
|
|
|
|
|
|
with local_session() as s:
|
2025-08-12 16:40:34 +03:00
|
|
|
|
community_authors = s.query(cls).where(cls.community_id == community_id).all()
|
2025-08-12 18:23:53 +03:00
|
|
|
|
else:
|
|
|
|
|
|
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
role_counts: dict[str, int] = {}
|
|
|
|
|
|
total_members = len(community_authors)
|
|
|
|
|
|
|
|
|
|
|
|
for ca in community_authors:
|
|
|
|
|
|
for role in ca.role_list:
|
|
|
|
|
|
role_counts[role] = role_counts.get(role, 0) + 1
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"total_members": total_members,
|
|
|
|
|
|
"role_counts": role_counts,
|
|
|
|
|
|
"roles_distribution": {
|
|
|
|
|
|
role: count / total_members if total_members > 0 else 0 for role, count in role_counts.items()
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === CRUD ОПЕРАЦИИ ДЛЯ RBAC ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_community_members_with_roles(community_id: int = 1) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Получает всех участников сообщества с их ролями и разрешениями
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Список участников с полной информацией
|
|
|
|
|
|
"""
|
|
|
|
|
|
with local_session() as session:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
community = session.query(Community).where(Community.id == community_id).first()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
|
|
|
|
|
|
if not community:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
return community.get_community_members(with_roles=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bulk_assign_roles(user_role_pairs: list[tuple[int, str]], community_id: int = 1) -> dict[str, int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Массовое назначение ролей пользователям
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_role_pairs: Список кортежей (author_id, role)
|
|
|
|
|
|
community_id: ID сообщества
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Статистика операции в формате {"success": int, "failed": int}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
success_count = 0
|
|
|
|
|
|
failed_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
for author_id, role in user_role_pairs:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if assign_role_to_user(author_id, role, community_id):
|
|
|
|
|
|
success_count += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Если роль уже была, считаем это успехом
|
|
|
|
|
|
success_count += 1
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[ошибка] Не удалось назначить роль {role} пользователю {author_id}: {e}")
|
|
|
|
|
|
failed_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
return {"success": success_count, "failed": failed_count}
|
2025-08-20 18:33:58 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Алиасы для обратной совместимости (избегаем циклических импортов)
|
|
|
|
|
|
def get_user_roles_in_community(author_id: int, community_id: int = 1, session: Any = None) -> list[str]:
|
|
|
|
|
|
"""Алиас для rbac.api.get_user_roles_in_community"""
|
|
|
|
|
|
from rbac.api import get_user_roles_in_community as _get_user_roles_in_community
|
|
|
|
|
|
|
|
|
|
|
|
return _get_user_roles_in_community(author_id, community_id, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assign_role_to_user(author_id: int, role: str, community_id: int = 1, session: Any = None) -> bool:
|
|
|
|
|
|
"""Алиас для rbac.api.assign_role_to_user"""
|
|
|
|
|
|
from rbac.api import assign_role_to_user as _assign_role_to_user
|
|
|
|
|
|
|
|
|
|
|
|
return _assign_role_to_user(author_id, role, community_id, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_role_from_user(author_id: int, role: str, community_id: int = 1, session: Any = None) -> bool:
|
|
|
|
|
|
"""Алиас для rbac.api.remove_role_from_user"""
|
|
|
|
|
|
from rbac.api import remove_role_from_user as _remove_role_from_user
|
|
|
|
|
|
|
|
|
|
|
|
return _remove_role_from_user(author_id, role, community_id, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def check_user_permission_in_community(
|
|
|
|
|
|
author_id: int, permission: str, community_id: int = 1, session: Any = None
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""Алиас для rbac.api.check_user_permission_in_community"""
|
|
|
|
|
|
from rbac.api import check_user_permission_in_community as _check_user_permission_in_community
|
|
|
|
|
|
|
|
|
|
|
|
return await _check_user_permission_in_community(author_id, permission, community_id, session)
|