This commit is contained in:
748
orm/community.py
748
orm/community.py
@@ -1,47 +1,59 @@
|
||||
import enum
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text, distinct, func
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, Text, UniqueConstraint, distinct, func
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from services.db import BaseModel
|
||||
from services.rbac import get_permissions_for_role
|
||||
|
||||
# Словарь названий ролей
|
||||
role_names = {
|
||||
"reader": "Читатель",
|
||||
"author": "Автор",
|
||||
"artist": "Художник",
|
||||
"expert": "Эксперт",
|
||||
"editor": "Редактор",
|
||||
"admin": "Администратор",
|
||||
}
|
||||
|
||||
class CommunityRole(enum.Enum):
|
||||
READER = "reader" # can read and comment
|
||||
AUTHOR = "author" # + can vote and invite collaborators
|
||||
ARTIST = "artist" # + can be credited as featured artist
|
||||
EXPERT = "expert" # + can add proof or disproof to shouts, can manage topics
|
||||
EDITOR = "editor" # + can manage topics, comments and community settings
|
||||
ADMIN = "admin"
|
||||
|
||||
@classmethod
|
||||
def as_string_array(cls, roles) -> list[str]:
|
||||
return [role.value for role in roles]
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value: str) -> "CommunityRole":
|
||||
return cls(value)
|
||||
# Словарь описаний ролей
|
||||
role_descriptions = {
|
||||
"reader": "Может читать и комментировать",
|
||||
"author": "Может создавать публикации",
|
||||
"artist": "Может быть credited artist",
|
||||
"expert": "Может добавлять доказательства",
|
||||
"editor": "Может модерировать контент",
|
||||
"admin": "Полные права",
|
||||
}
|
||||
|
||||
|
||||
class CommunityFollower(BaseModel):
|
||||
"""
|
||||
Простая подписка пользователя на сообщество.
|
||||
|
||||
Использует обычный id как первичный ключ для простоты и производительности.
|
||||
Уникальность обеспечивается индексом по (community, follower).
|
||||
"""
|
||||
|
||||
__tablename__ = "community_follower"
|
||||
|
||||
community = Column(ForeignKey("community.id"), primary_key=True)
|
||||
follower = Column(ForeignKey("author.id"), primary_key=True)
|
||||
roles = Column(String, nullable=True)
|
||||
# Простые поля - стандартный подход
|
||||
community = Column(ForeignKey("community.id"), nullable=False, index=True)
|
||||
follower = Column(ForeignKey("author.id"), nullable=False, index=True)
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
|
||||
def __init__(self, community: int, follower: int, roles: list[str] | None = None) -> None:
|
||||
# Уникальность по паре сообщество-подписчик
|
||||
__table_args__ = (
|
||||
UniqueConstraint("community", "follower", name="uq_community_follower"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
def __init__(self, community: int, follower: int) -> None:
|
||||
self.community = community # type: ignore[assignment]
|
||||
self.follower = follower # type: ignore[assignment]
|
||||
if roles:
|
||||
self.roles = ",".join(roles) # type: ignore[assignment]
|
||||
|
||||
def get_roles(self) -> list[CommunityRole]:
|
||||
roles_str = getattr(self, "roles", "")
|
||||
return [CommunityRole(role) for role in roles_str.split(",")] if roles_str else []
|
||||
|
||||
|
||||
class Community(BaseModel):
|
||||
@@ -65,16 +77,8 @@ class Community(BaseModel):
|
||||
def stat(self):
|
||||
return CommunityStats(self)
|
||||
|
||||
@property
|
||||
def role_list(self):
|
||||
return self.roles.split(",") if self.roles else []
|
||||
|
||||
@role_list.setter
|
||||
def role_list(self, value) -> None:
|
||||
self.roles = ",".join(value) if value else None # type: ignore[assignment]
|
||||
|
||||
def is_followed_by(self, author_id: int) -> bool:
|
||||
# Check if the author follows this community
|
||||
"""Проверяет, подписан ли пользователь на сообщество"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
@@ -85,20 +89,228 @@ class Community(BaseModel):
|
||||
)
|
||||
return follower is not None
|
||||
|
||||
def get_role(self, author_id: int) -> CommunityRole | None:
|
||||
# Get the role of the author in this community
|
||||
def get_user_roles(self, user_id: int) -> list[str]:
|
||||
"""
|
||||
Получает роли пользователя в данном сообществе через CommunityAuthor
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
|
||||
Returns:
|
||||
Список ролей пользователя в сообществе
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
follower = (
|
||||
session.query(CommunityFollower)
|
||||
.filter(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
||||
community_author = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if follower and follower.roles:
|
||||
roles = follower.roles.split(",")
|
||||
return CommunityRole.from_string(roles[0]) if roles else None
|
||||
return None
|
||||
|
||||
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: Название роли
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Ищем существующую запись
|
||||
community_author = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.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: Название роли
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
community_author = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.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: Список ролей для установки
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Ищем существующую запись
|
||||
community_author = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
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:
|
||||
Список участников с информацией о ролях
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
community_authors = session.query(CommunityAuthor).filter(CommunityAuthor.community_id == self.id).all()
|
||||
|
||||
members = []
|
||||
for ca in community_authors:
|
||||
member_info = {
|
||||
"author_id": ca.author_id,
|
||||
"joined_at": ca.joined_at,
|
||||
}
|
||||
|
||||
if with_roles:
|
||||
member_info["roles"] = ca.role_list # type: ignore[assignment]
|
||||
member_info["permissions"] = ca.get_permissions() # type: ignore[assignment]
|
||||
|
||||
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:
|
||||
self.settings = {} # type: ignore[assignment]
|
||||
|
||||
self.settings["default_roles"] = roles # type: ignore[index]
|
||||
|
||||
async def initialize_role_permissions(self) -> None:
|
||||
"""
|
||||
Инициализирует права ролей для сообщества из дефолтных настроек.
|
||||
Вызывается при создании нового сообщества.
|
||||
"""
|
||||
from services.rbac import initialize_community_permissions
|
||||
|
||||
await initialize_community_permissions(int(self.id))
|
||||
|
||||
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:
|
||||
self.settings = {} # type: ignore[assignment]
|
||||
|
||||
self.settings["available_roles"] = roles # type: ignore[index]
|
||||
|
||||
def set_slug(self, slug: str) -> None:
|
||||
"""Устанавливает slug сообщества"""
|
||||
self.slug = slug # type: ignore[assignment]
|
||||
|
||||
|
||||
class CommunityStats:
|
||||
@@ -137,17 +349,453 @@ class CommunityStats:
|
||||
|
||||
|
||||
class CommunityAuthor(BaseModel):
|
||||
"""
|
||||
Связь автора с сообществом и его ролями.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID записи
|
||||
community_id: ID сообщества
|
||||
author_id: ID автора
|
||||
roles: CSV строка с ролями (например: "reader,author,editor")
|
||||
joined_at: Время присоединения к сообществу (unix timestamp)
|
||||
"""
|
||||
|
||||
__tablename__ = "community_author"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
community_id = Column(Integer, ForeignKey("community.id"))
|
||||
author_id = Column(Integer, ForeignKey("author.id"))
|
||||
community_id = Column(Integer, ForeignKey("community.id"), nullable=False)
|
||||
author_id = Column(Integer, ForeignKey("author.id"), nullable=False)
|
||||
roles = Column(Text, nullable=True, comment="Roles (comma-separated)")
|
||||
joined_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
|
||||
# Связи
|
||||
community = relationship("Community", foreign_keys=[community_id])
|
||||
author = relationship("Author", foreign_keys=[author_id])
|
||||
|
||||
# Уникальность по сообществу и автору
|
||||
__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},
|
||||
)
|
||||
|
||||
@property
|
||||
def role_list(self):
|
||||
return self.roles.split(",") if self.roles else []
|
||||
def role_list(self) -> list[str]:
|
||||
"""Получает список ролей как список строк"""
|
||||
return [role.strip() for role in self.roles.split(",") if role.strip()] if self.roles else []
|
||||
|
||||
@role_list.setter
|
||||
def role_list(self, value) -> None:
|
||||
def role_list(self, value: list[str]) -> None:
|
||||
"""Устанавливает список ролей из списка строк"""
|
||||
self.roles = ",".join(value) if value else None # type: ignore[assignment]
|
||||
|
||||
def has_role(self, role: str) -> bool:
|
||||
"""
|
||||
Проверяет наличие роли у автора в сообществе
|
||||
|
||||
Args:
|
||||
role: Название роли для проверки
|
||||
|
||||
Returns:
|
||||
True если роль есть, False если нет
|
||||
"""
|
||||
return role in self.role_list
|
||||
|
||||
def add_role(self, role: str) -> None:
|
||||
"""
|
||||
Добавляет роль автору (если её ещё нет)
|
||||
|
||||
Args:
|
||||
role: Название роли для добавления
|
||||
"""
|
||||
roles = self.role_list
|
||||
if role not in roles:
|
||||
roles.append(role)
|
||||
self.role_list = roles
|
||||
|
||||
def remove_role(self, role: str) -> None:
|
||||
"""
|
||||
Удаляет роль у автора
|
||||
|
||||
Args:
|
||||
role: Название роли для удаления
|
||||
"""
|
||||
roles = self.role_list
|
||||
if role in roles:
|
||||
roles.remove(role)
|
||||
self.role_list = roles
|
||||
|
||||
def set_roles(self, roles: list[str]) -> None:
|
||||
"""
|
||||
Устанавливает полный список ролей (заменяет текущие)
|
||||
|
||||
Args:
|
||||
roles: Список ролей для установки
|
||||
"""
|
||||
self.role_list = roles
|
||||
|
||||
async def get_permissions(self) -> list[str]:
|
||||
"""
|
||||
Получает все разрешения автора на основе его ролей в конкретном сообществе
|
||||
|
||||
Returns:
|
||||
Список разрешений (permissions)
|
||||
"""
|
||||
|
||||
all_permissions = set()
|
||||
for role in self.role_list:
|
||||
role_perms = await get_permissions_for_role(role, int(self.community_id))
|
||||
all_permissions.update(role_perms)
|
||||
|
||||
return list(all_permissions)
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""
|
||||
Проверяет наличие разрешения у автора
|
||||
|
||||
Args:
|
||||
permission: Разрешение для проверки (например: "shout:create")
|
||||
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
return permission in self.role_list
|
||||
|
||||
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:
|
||||
Список словарей с информацией о сообществах и ролях
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
if session is None:
|
||||
with local_session() as ssession:
|
||||
return cls.get_user_communities_with_roles(author_id, ssession)
|
||||
|
||||
community_authors = session.query(cls).filter(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
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def find_by_user_and_community(cls, author_id: int, community_id: int, session=None) -> "CommunityAuthor | None":
|
||||
"""
|
||||
Находит запись CommunityAuthor по ID автора и сообщества
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
community_id: ID сообщества
|
||||
session: Сессия БД (опционально)
|
||||
|
||||
Returns:
|
||||
CommunityAuthor или None
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
if session is None:
|
||||
with local_session() as ssession:
|
||||
return cls.find_by_user_and_community(author_id, community_id, ssession)
|
||||
|
||||
return session.query(cls).filter(cls.author_id == author_id, cls.community_id == community_id).first()
|
||||
|
||||
@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 пользователей
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
if session is None:
|
||||
with local_session() as ssession:
|
||||
return cls.get_users_with_role(community_id, role, ssession)
|
||||
|
||||
community_authors = session.query(cls).filter(cls.community_id == community_id).all()
|
||||
|
||||
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:
|
||||
Словарь со статистикой ролей
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
if session is None:
|
||||
with local_session() as s:
|
||||
return cls.get_community_stats(community_id, s)
|
||||
|
||||
community_authors = session.query(cls).filter(cls.community_id == community_id).all()
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# === HELPER ФУНКЦИИ ДЛЯ РАБОТЫ С РОЛЯМИ ===
|
||||
|
||||
|
||||
def get_user_roles_in_community(author_id: int, community_id: int = 1) -> list[str]:
|
||||
"""
|
||||
Удобная функция для получения ролей пользователя в сообществе
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
community_id: ID сообщества (по умолчанию 1)
|
||||
|
||||
Returns:
|
||||
Список ролей пользователя
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
ca = CommunityAuthor.find_by_user_and_community(author_id, community_id, session)
|
||||
return ca.role_list if ca else []
|
||||
|
||||
|
||||
async def check_user_permission_in_community(author_id: int, permission: str, community_id: int = 1) -> bool:
|
||||
"""
|
||||
Проверяет разрешение пользователя в сообществе с учетом иерархии ролей
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
permission: Разрешение для проверки
|
||||
community_id: ID сообщества (по умолчанию 1)
|
||||
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
# Используем новую систему RBAC с иерархией
|
||||
from services.rbac import user_has_permission
|
||||
|
||||
return await user_has_permission(author_id, permission, community_id)
|
||||
|
||||
|
||||
def assign_role_to_user(author_id: int, role: str, community_id: int = 1) -> bool:
|
||||
"""
|
||||
Назначает роль пользователю в сообществе
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
role: Название роли
|
||||
community_id: ID сообщества (по умолчанию 1)
|
||||
|
||||
Returns:
|
||||
True если роль была добавлена, False если уже была
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
ca = CommunityAuthor.find_by_user_and_community(author_id, community_id, session)
|
||||
|
||||
if ca:
|
||||
if ca.has_role(role):
|
||||
return False # Роль уже есть
|
||||
ca.add_role(role)
|
||||
else:
|
||||
# Создаем новую запись
|
||||
ca = CommunityAuthor(community_id=community_id, author_id=author_id, roles=role)
|
||||
session.add(ca)
|
||||
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def remove_role_from_user(author_id: int, role: str, community_id: int = 1) -> bool:
|
||||
"""
|
||||
Удаляет роль у пользователя в сообществе
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
role: Название роли
|
||||
community_id: ID сообщества (по умолчанию 1)
|
||||
|
||||
Returns:
|
||||
True если роль была удалена, False если её не было
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
ca = CommunityAuthor.find_by_user_and_community(author_id, community_id, session)
|
||||
|
||||
if ca and ca.has_role(role):
|
||||
ca.remove_role(role)
|
||||
|
||||
# Если ролей не осталось, удаляем запись
|
||||
if not ca.role_list:
|
||||
session.delete(ca)
|
||||
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def migrate_old_roles_to_community_author():
|
||||
"""
|
||||
Функция миграции для переноса ролей из старой системы в CommunityAuthor
|
||||
|
||||
[непроверенное] Предполагает, что старые роли хранились в auth.orm.AuthorRole
|
||||
"""
|
||||
from auth.orm import AuthorRole
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
# Получаем все старые роли
|
||||
old_roles = session.query(AuthorRole).all()
|
||||
|
||||
print(f"[миграция] Найдено {len(old_roles)} старых записей ролей")
|
||||
|
||||
# Группируем по автору и сообществу
|
||||
user_community_roles = {}
|
||||
|
||||
for role in old_roles:
|
||||
key = (role.author, role.community)
|
||||
if key not in user_community_roles:
|
||||
user_community_roles[key] = []
|
||||
|
||||
# Извлекаем базовое имя роли (убираем суффикс сообщества если есть)
|
||||
role_name = role.role
|
||||
if isinstance(role_name, str) and "-" in role_name:
|
||||
base_role = role_name.split("-")[0]
|
||||
else:
|
||||
base_role = role_name
|
||||
|
||||
if base_role not in user_community_roles[key]:
|
||||
user_community_roles[key].append(base_role)
|
||||
|
||||
# Создаем новые записи CommunityAuthor
|
||||
migrated_count = 0
|
||||
for (author_id, community_id), roles in user_community_roles.items():
|
||||
# Проверяем, есть ли уже запись
|
||||
existing = CommunityAuthor.find_by_user_and_community(author_id, community_id, session)
|
||||
|
||||
if not existing:
|
||||
ca = CommunityAuthor(community_id=community_id, author_id=author_id)
|
||||
ca.set_roles(roles)
|
||||
session.add(ca)
|
||||
migrated_count += 1
|
||||
else:
|
||||
print(f"[миграция] Запись для автора {author_id} в сообществе {community_id} уже существует")
|
||||
|
||||
session.commit()
|
||||
print(f"[миграция] Создано {migrated_count} новых записей CommunityAuthor")
|
||||
print("[миграция] Миграция завершена. Проверьте результаты перед удалением старых таблиц.")
|
||||
|
||||
|
||||
# === CRUD ОПЕРАЦИИ ДЛЯ RBAC ===
|
||||
|
||||
|
||||
def get_all_community_members_with_roles(community_id: int = 1) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Получает всех участников сообщества с их ролями и разрешениями
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Список участников с полной информацией
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
community = session.query(Community).filter(Community.id == community_id).first()
|
||||
|
||||
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}
|
||||
|
@@ -9,24 +9,37 @@ from services.db import BaseModel as Base
|
||||
class ReactionKind(Enumeration):
|
||||
# TYPE = <reaction index> # rating diff
|
||||
|
||||
# editor mode
|
||||
# editor specials
|
||||
AGREE = "AGREE" # +1
|
||||
DISAGREE = "DISAGREE" # -1
|
||||
ASK = "ASK" # +0
|
||||
PROPOSE = "PROPOSE" # +0
|
||||
|
||||
# coauthor specials
|
||||
ASK = "ASK" # 0
|
||||
PROPOSE = "PROPOSE" # 0
|
||||
|
||||
# generic internal reactions
|
||||
ACCEPT = "ACCEPT" # +1
|
||||
REJECT = "REJECT" # -1
|
||||
|
||||
# expert mode
|
||||
# experts speacials
|
||||
PROOF = "PROOF" # +1
|
||||
DISPROOF = "DISPROOF" # -1
|
||||
|
||||
# public feed
|
||||
QUOTE = "QUOTE" # +0 TODO: use to bookmark in collection
|
||||
COMMENT = "COMMENT" # +0
|
||||
# comment and quote
|
||||
QUOTE = "QUOTE" # 0
|
||||
COMMENT = "COMMENT" # 0
|
||||
|
||||
# generic rating
|
||||
LIKE = "LIKE" # +1
|
||||
DISLIKE = "DISLIKE" # -1
|
||||
|
||||
# credit artist or researcher
|
||||
CREDIT = "CREDIT" # +1
|
||||
SILENT = "SILENT" # 0
|
||||
|
||||
|
||||
REACTION_KINDS = ReactionKind.__members__.keys()
|
||||
|
||||
|
||||
class Reaction(Base):
|
||||
__tablename__ = "reaction"
|
||||
|
Reference in New Issue
Block a user