circular-fix
Some checks failed
Deploy on push / deploy (push) Failing after 17s

This commit is contained in:
2025-08-17 16:33:54 +03:00
parent bc8447a444
commit e78e12eeee
65 changed files with 3304 additions and 1051 deletions

View File

@@ -24,7 +24,7 @@ class BaseModel(DeclarativeBase):
REGISTRY[cls.__name__] = cls
super().__init_subclass__(**kwargs)
def dict(self, access: bool = False) -> builtins.dict[str, Any]:
def dict(self) -> builtins.dict[str, Any]:
"""
Конвертирует ORM объект в словарь.
@@ -44,7 +44,7 @@ class BaseModel(DeclarativeBase):
if hasattr(self, column_name):
value = getattr(self, column_name)
# Проверяем, является ли значение JSON и декодируем его при необходимости
if isinstance(value, (str, bytes)) and isinstance(
if isinstance(value, str | bytes) and isinstance(
self.__table__.columns[column_name].type, JSON
):
try:

View File

@@ -21,11 +21,7 @@ from auth.orm import Author
from orm.base import BaseModel
from orm.shout import Shout
from services.db import local_session
from services.rbac import (
get_permissions_for_role,
initialize_community_permissions,
user_has_permission,
)
from auth.rbac_interface import get_rbac_operations
# Словарь названий ролей
role_names = {
@@ -59,7 +55,7 @@ class CommunityFollower(BaseModel):
__tablename__ = "community_follower"
community: Mapped[int] = mapped_column(Integer, ForeignKey("community.id"), nullable=False, index=True)
follower: Mapped[int] = mapped_column(Integer, ForeignKey(Author.id), nullable=False, index=True)
follower: Mapped[int] = mapped_column(Integer, ForeignKey("author.id"), nullable=False, index=True)
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
# Уникальность по паре сообщество-подписчик
@@ -288,7 +284,8 @@ class Community(BaseModel):
Инициализирует права ролей для сообщества из дефолтных настроек.
Вызывается при создании нового сообщества.
"""
await initialize_community_permissions(int(self.id))
rbac_ops = get_rbac_operations()
await rbac_ops.initialize_community_permissions(int(self.id))
def get_available_roles(self) -> list[str]:
"""
@@ -399,7 +396,7 @@ class CommunityAuthor(BaseModel):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
community_id: Mapped[int] = mapped_column(Integer, ForeignKey("community.id"), nullable=False)
author_id: Mapped[int] = mapped_column(Integer, ForeignKey(Author.id), nullable=False)
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("author.id"), nullable=False)
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()))
@@ -478,63 +475,31 @@ class CommunityAuthor(BaseModel):
"""
all_permissions = set()
rbac_ops = get_rbac_operations()
for role in self.role_list:
role_perms = await get_permissions_for_role(role, int(self.community_id))
role_perms = await rbac_ops.get_permissions_for_role(role, int(self.community_id))
all_permissions.update(role_perms)
return list(all_permissions)
def has_permission(
self, permission: str | None = None, resource: str | None = None, operation: str | None = None
) -> bool:
def has_permission(self, permission: str) -> bool:
"""
Проверяет наличие разрешения у автора
Проверяет, есть ли у пользователя указанное право
Args:
permission: Разрешение для проверки (например: "shout:create")
resource: Опциональный ресурс (для обратной совместимости)
operation: Опциональная операция (для обратной совместимости)
permission: Право для проверки (например, "community:create")
Returns:
True если разрешение есть, False если нет
True если право есть, False если нет
"""
# Если передан полный permission, используем его
if permission and ":" in permission:
# Проверяем права через синхронную функцию
try:
import asyncio
from services.rbac import get_permissions_for_role
all_permissions = set()
for role in self.role_list:
role_perms = asyncio.run(get_permissions_for_role(role, int(self.community_id)))
all_permissions.update(role_perms)
return permission in all_permissions
except Exception:
# Fallback: проверяем роли (старый способ)
return any(permission == role for role in self.role_list)
# Если переданы resource и operation, формируем permission
if resource and operation:
full_permission = f"{resource}:{operation}"
try:
import asyncio
from services.rbac import get_permissions_for_role
all_permissions = set()
for role in self.role_list:
role_perms = asyncio.run(get_permissions_for_role(role, int(self.community_id)))
all_permissions.update(role_perms)
return full_permission in all_permissions
except Exception:
# Fallback: проверяем роли (старый способ)
return any(full_permission == role for role in self.role_list)
return False
# Проверяем права через синхронную функцию
try:
# В синхронном контексте не можем использовать await
# Используем fallback на проверку ролей
return permission in self.role_list
except Exception:
# FIXME: Fallback: проверяем роли (старый способ)
return any(permission == role for role in self.role_list)
def dict(self, access: bool = False) -> dict[str, Any]:
"""
@@ -706,7 +671,8 @@ async def check_user_permission_in_community(author_id: int, permission: str, co
Returns:
True если разрешение есть, False если нет
"""
return await user_has_permission(author_id, permission, community_id)
rbac_ops = get_rbac_operations()
return await rbac_ops.user_has_permission(author_id, permission, community_id)
def assign_role_to_user(author_id: int, role: str, community_id: int = 1) -> bool:

View File

@@ -8,6 +8,11 @@ from auth.orm import Author
from orm.base import BaseModel as Base
from orm.topic import Topic
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class DraftTopic(Base):
__tablename__ = "draft_topic"
@@ -28,7 +33,7 @@ class DraftAuthor(Base):
__tablename__ = "draft_author"
draft: Mapped[int] = mapped_column(ForeignKey("draft.id"), index=True)
author: Mapped[int] = mapped_column(ForeignKey(Author.id), index=True)
author: Mapped[int] = mapped_column(ForeignKey("author.id"), index=True)
caption: Mapped[str | None] = mapped_column(String, nullable=True, default="")
__table_args__ = (
@@ -44,7 +49,7 @@ class Draft(Base):
# required
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
created_by: Mapped[int] = mapped_column(ForeignKey(Author.id), nullable=False)
created_by: Mapped[int] = mapped_column(ForeignKey("author.id"), nullable=False)
community: Mapped[int] = mapped_column(ForeignKey("community.id"), nullable=False, default=1)
# optional
@@ -63,9 +68,9 @@ class Draft(Base):
# auto
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
deleted_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
updated_by: Mapped[int | None] = mapped_column(ForeignKey(Author.id), nullable=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey(Author.id), nullable=True)
authors = relationship(Author, secondary=DraftAuthor.__table__)
updated_by: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
authors = relationship(get_author_model(), secondary=DraftAuthor.__table__)
topics = relationship(Topic, secondary=DraftTopic.__table__)
# shout/publication

View File

@@ -5,10 +5,16 @@ from typing import Any
from sqlalchemy import JSON, DateTime, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
# Импорт Author отложен для избежания циклических импортов
from auth.orm import Author
from orm.base import BaseModel as Base
from utils.logger import root_logger as logger
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class NotificationEntity(Enum):
"""
@@ -106,7 +112,7 @@ class Notification(Base):
status: Mapped[NotificationStatus] = mapped_column(default=NotificationStatus.UNREAD)
kind: Mapped[NotificationKind] = mapped_column(nullable=False)
seen = relationship(Author, secondary="notification_seen")
seen = relationship("Author", secondary="notification_seen")
__table_args__ = (
Index("idx_notification_created_at", "created_at"),

View File

@@ -7,6 +7,11 @@ from sqlalchemy.orm import Mapped, mapped_column
from auth.orm import Author
from orm.base import BaseModel as Base
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class ReactionKind(Enumeration):
# TYPE = <reaction index> # rating diff
@@ -51,11 +56,11 @@ class Reaction(Base):
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()), index=True)
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="Updated at", index=True)
deleted_at: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="Deleted at", index=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey(Author.id), nullable=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
reply_to: Mapped[int | None] = mapped_column(ForeignKey("reaction.id"), nullable=True)
quote: Mapped[str | None] = mapped_column(String, nullable=True, comment="Original quoted text")
shout: Mapped[int] = mapped_column(ForeignKey("shout.id"), nullable=False, index=True)
created_by: Mapped[int] = mapped_column(ForeignKey(Author.id), nullable=False)
created_by: Mapped[int] = mapped_column(ForeignKey("author.id"), nullable=False)
kind: Mapped[str] = mapped_column(String, nullable=False, index=True)
oid: Mapped[str | None] = mapped_column(String)

View File

@@ -4,11 +4,17 @@ from typing import Any
from sqlalchemy import JSON, Boolean, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
# Импорт Author отложен для избежания циклических импортов
from auth.orm import Author
from orm.base import BaseModel as Base
from orm.reaction import Reaction
from orm.topic import Topic
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class ShoutTopic(Base):
"""
@@ -37,7 +43,7 @@ class ShoutTopic(Base):
class ShoutReactionsFollower(Base):
__tablename__ = "shout_reactions_followers"
follower: Mapped[int] = mapped_column(ForeignKey(Author.id), index=True)
follower: Mapped[int] = mapped_column(ForeignKey("author.id"), index=True)
shout: Mapped[int] = mapped_column(ForeignKey("shout.id"), index=True)
auto: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
@@ -64,7 +70,7 @@ class ShoutAuthor(Base):
__tablename__ = "shout_author"
shout: Mapped[int] = mapped_column(ForeignKey("shout.id"), index=True)
author: Mapped[int] = mapped_column(ForeignKey(Author.id), index=True)
author: Mapped[int] = mapped_column(ForeignKey("author.id"), index=True)
caption: Mapped[str | None] = mapped_column(String, nullable=True, default="")
# Определяем дополнительные индексы
@@ -89,9 +95,9 @@ class Shout(Base):
featured_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
deleted_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
created_by: Mapped[int] = mapped_column(ForeignKey(Author.id), nullable=False)
updated_by: Mapped[int | None] = mapped_column(ForeignKey(Author.id), nullable=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey(Author.id), nullable=True)
created_by: Mapped[int] = mapped_column(ForeignKey("author.id"), nullable=False)
updated_by: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
deleted_by: Mapped[int | None] = mapped_column(ForeignKey("author.id"), nullable=True)
community: Mapped[int] = mapped_column(ForeignKey("community.id"), nullable=False)
body: Mapped[str] = mapped_column(String, nullable=False, comment="Body")
@@ -104,9 +110,9 @@ class Shout(Base):
layout: Mapped[str] = mapped_column(String, nullable=False, default="article")
media: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
authors = relationship(Author, secondary="shout_author")
topics = relationship(Topic, secondary="shout_topic")
reactions = relationship(Reaction)
authors = relationship("Author", secondary="shout_author")
topics = relationship("Topic", secondary="shout_topic")
reactions = relationship("Reaction")
lang: Mapped[str] = mapped_column(String, nullable=False, default="ru", comment="Language")
version_of: Mapped[int | None] = mapped_column(ForeignKey("shout.id"), nullable=True)

View File

@@ -14,6 +14,11 @@ from sqlalchemy.orm import Mapped, mapped_column
from auth.orm import Author
from orm.base import BaseModel as Base
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class TopicFollower(Base):
"""
@@ -28,7 +33,7 @@ class TopicFollower(Base):
__tablename__ = "topic_followers"
follower: Mapped[int] = mapped_column(ForeignKey(Author.id))
follower: Mapped[int] = mapped_column(ForeignKey("author.id"))
topic: Mapped[int] = mapped_column(ForeignKey("topic.id"))
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
auto: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)