tests-passed
This commit is contained in:
0
orm/__init__.py
Normal file
0
orm/__init__.py
Normal file
51
orm/base.py
51
orm/base.py
@@ -1,10 +1,10 @@
|
||||
import builtins
|
||||
import logging
|
||||
from typing import Any, Callable, ClassVar, Type, Union
|
||||
from typing import Any, Type
|
||||
|
||||
import orjson
|
||||
from sqlalchemy import JSON, Column, Integer
|
||||
from sqlalchemy.orm import declarative_base, declared_attr
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,44 +14,12 @@ REGISTRY: dict[str, Type[Any]] = {}
|
||||
# Список полей для фильтрации при сериализации
|
||||
FILTERED_FIELDS: list[str] = []
|
||||
|
||||
# Создаем базовый класс для декларативных моделей
|
||||
_Base = declarative_base()
|
||||
|
||||
|
||||
class SafeColumnMixin:
|
||||
class BaseModel(DeclarativeBase):
|
||||
"""
|
||||
Миксин для безопасного присваивания значений столбцам с автоматическим преобразованием типов
|
||||
Базовая модель с методами сериализации и обновления
|
||||
"""
|
||||
|
||||
@declared_attr
|
||||
def __safe_setattr__(self) -> Callable:
|
||||
def safe_setattr(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Безопасно устанавливает атрибут для экземпляра.
|
||||
|
||||
Args:
|
||||
key (str): Имя атрибута.
|
||||
value (Any): Значение атрибута.
|
||||
"""
|
||||
setattr(self, key, value)
|
||||
|
||||
return safe_setattr
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Переопределяем __setattr__ для использования безопасного присваивания
|
||||
"""
|
||||
# Используем object.__setattr__ для избежания рекурсии
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
|
||||
class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc]
|
||||
__abstract__ = True
|
||||
__allow_unmapped__ = True
|
||||
__table_args__: ClassVar[Union[dict[str, Any], tuple]] = {"extend_existing": True}
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
REGISTRY[cls.__name__] = cls
|
||||
super().__init_subclass__(**kwargs)
|
||||
@@ -68,6 +36,7 @@ class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc]
|
||||
"""
|
||||
column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys())
|
||||
data: builtins.dict[str, Any] = {}
|
||||
logger.debug(f"Converting object to dictionary {'with access' if access else 'without access'}")
|
||||
try:
|
||||
for column_name in column_names:
|
||||
try:
|
||||
@@ -81,7 +50,7 @@ class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc]
|
||||
try:
|
||||
data[column_name] = orjson.loads(value)
|
||||
except (TypeError, orjson.JSONDecodeError) as e:
|
||||
logger.exception(f"Error decoding JSON for column '{column_name}': {e}")
|
||||
logger.warning(f"Error decoding JSON for column '{column_name}': {e}")
|
||||
data[column_name] = value
|
||||
else:
|
||||
data[column_name] = value
|
||||
@@ -91,10 +60,14 @@ class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc]
|
||||
except AttributeError as e:
|
||||
logger.warning(f"Attribute error for column '{column_name}': {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred while converting object to dictionary: {e}")
|
||||
logger.warning(f"Error occurred while converting object to dictionary {e}")
|
||||
return data
|
||||
|
||||
def update(self, values: builtins.dict[str, Any]) -> None:
|
||||
for key, value in values.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
# Alias for backward compatibility
|
||||
Base = BaseModel
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import ForeignKey, Index, Integer, PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from orm.base import BaseModel as Base
|
||||
|
||||
@@ -9,19 +9,29 @@ from orm.base import BaseModel as Base
|
||||
class ShoutCollection(Base):
|
||||
__tablename__ = "shout_collection"
|
||||
|
||||
shout = Column(ForeignKey("shout.id"), primary_key=True)
|
||||
collection = Column(ForeignKey("collection.id"), primary_key=True)
|
||||
shout: Mapped[int] = mapped_column(ForeignKey("shout.id"))
|
||||
collection: Mapped[int] = mapped_column(ForeignKey("collection.id"))
|
||||
created_at: Mapped[int] = mapped_column(Integer, default=lambda: int(time.time()))
|
||||
created_by: Mapped[int] = mapped_column(ForeignKey("author.id"), comment="Created By")
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(shout, collection),
|
||||
Index("idx_shout_collection_shout", "shout"),
|
||||
Index("idx_shout_collection_collection", "collection"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
|
||||
class Collection(Base):
|
||||
__tablename__ = "collection"
|
||||
|
||||
slug = Column(String, unique=True)
|
||||
title = Column(String, nullable=False, comment="Title")
|
||||
body = Column(String, nullable=True, comment="Body")
|
||||
pic = Column(String, nullable=True, comment="Picture")
|
||||
created_at = Column(Integer, default=lambda: int(time.time()))
|
||||
created_by = Column(ForeignKey("author.id"), comment="Created By")
|
||||
published_at = Column(Integer, default=lambda: int(time.time()))
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
slug: Mapped[str] = mapped_column(String, unique=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False, comment="Title")
|
||||
body: Mapped[str | None] = mapped_column(String, nullable=True, comment="Body")
|
||||
pic: Mapped[str | None] = mapped_column(String, nullable=True, comment="Picture")
|
||||
created_at: Mapped[int] = mapped_column(Integer, default=lambda: int(time.time()))
|
||||
created_by: Mapped[int] = mapped_column(ForeignKey("author.id"), comment="Created By")
|
||||
published_at: Mapped[int] = mapped_column(Integer, default=lambda: int(time.time()))
|
||||
|
||||
created_by_author = relationship("Author", foreign_keys=[created_by])
|
||||
|
267
orm/community.py
267
orm/community.py
@@ -1,13 +1,31 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, UniqueConstraint, distinct, func
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
distinct,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel
|
||||
from services.rbac import get_permissions_for_role
|
||||
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,
|
||||
)
|
||||
|
||||
# Словарь названий ролей
|
||||
role_names = {
|
||||
@@ -40,38 +58,35 @@ class CommunityFollower(BaseModel):
|
||||
|
||||
__tablename__ = "community_follower"
|
||||
|
||||
# Простые поля - стандартный подход
|
||||
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()))
|
||||
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)
|
||||
created_at: Mapped[int] = mapped_column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
|
||||
# Уникальность по паре сообщество-подписчик
|
||||
__table_args__ = (
|
||||
UniqueConstraint("community", "follower", name="uq_community_follower"),
|
||||
PrimaryKeyConstraint("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]
|
||||
self.community = community
|
||||
self.follower = follower
|
||||
|
||||
|
||||
class Community(BaseModel):
|
||||
__tablename__ = "community"
|
||||
|
||||
name = Column(String, nullable=False)
|
||||
slug = Column(String, nullable=False, unique=True)
|
||||
desc = Column(String, nullable=False, default="")
|
||||
pic = Column(String, nullable=False, default="")
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
settings = Column(JSON, nullable=True)
|
||||
updated_at = Column(Integer, nullable=True)
|
||||
deleted_at = Column(Integer, nullable=True)
|
||||
private = Column(Boolean, default=False)
|
||||
|
||||
followers = relationship("Author", secondary="community_follower")
|
||||
created_by_author = relationship("Author", foreign_keys=[created_by])
|
||||
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)
|
||||
|
||||
@hybrid_property
|
||||
def stat(self):
|
||||
@@ -79,12 +94,10 @@ class Community(BaseModel):
|
||||
|
||||
def is_followed_by(self, author_id: int) -> bool:
|
||||
"""Проверяет, подписан ли пользователь на сообщество"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
follower = (
|
||||
session.query(CommunityFollower)
|
||||
.filter(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
||||
.where(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
||||
.first()
|
||||
)
|
||||
return follower is not None
|
||||
@@ -99,12 +112,10 @@ class Community(BaseModel):
|
||||
Returns:
|
||||
Список ролей пользователя в сообществе
|
||||
"""
|
||||
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)
|
||||
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -132,13 +143,11 @@ class Community(BaseModel):
|
||||
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)
|
||||
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -160,12 +169,10 @@ class Community(BaseModel):
|
||||
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)
|
||||
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -186,13 +193,11 @@ class Community(BaseModel):
|
||||
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)
|
||||
.where(CommunityAuthor.community_id == self.id, CommunityAuthor.author_id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -221,10 +226,8 @@ class Community(BaseModel):
|
||||
Returns:
|
||||
Список участников с информацией о ролях
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
community_authors = session.query(CommunityAuthor).filter(CommunityAuthor.community_id == self.id).all()
|
||||
community_authors = session.query(CommunityAuthor).where(CommunityAuthor.community_id == self.id).all()
|
||||
|
||||
members = []
|
||||
for ca in community_authors:
|
||||
@@ -237,8 +240,6 @@ class Community(BaseModel):
|
||||
member_info["roles"] = ca.role_list # type: ignore[assignment]
|
||||
# Получаем разрешения синхронно
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
member_info["permissions"] = asyncio.run(ca.get_permissions()) # type: ignore[assignment]
|
||||
except Exception:
|
||||
# Если не удается получить разрешения асинхронно, используем пустой список
|
||||
@@ -287,8 +288,6 @@ class Community(BaseModel):
|
||||
Инициализирует права ролей для сообщества из дефолтных настроек.
|
||||
Вызывается при создании нового сообщества.
|
||||
"""
|
||||
from services.rbac import initialize_community_permissions
|
||||
|
||||
await initialize_community_permissions(int(self.id))
|
||||
|
||||
def get_available_roles(self) -> list[str]:
|
||||
@@ -319,34 +318,63 @@ class Community(BaseModel):
|
||||
"""Устанавливает slug сообщества"""
|
||||
self.slug = slug # type: ignore[assignment]
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class CommunityStats:
|
||||
def __init__(self, community) -> None:
|
||||
self.community = community
|
||||
|
||||
@property
|
||||
def shouts(self):
|
||||
from orm.shout import Shout
|
||||
|
||||
return self.community.session.query(func.count(Shout.id)).filter(Shout.community == self.community.id).scalar()
|
||||
def shouts(self) -> int:
|
||||
return self.community.session.query(func.count(Shout.id)).where(Shout.community == self.community.id).scalar()
|
||||
|
||||
@property
|
||||
def followers(self):
|
||||
def followers(self) -> int:
|
||||
return (
|
||||
self.community.session.query(func.count(CommunityFollower.follower))
|
||||
.filter(CommunityFollower.community == self.community.id)
|
||||
.where(CommunityFollower.community == self.community.id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@property
|
||||
def authors(self):
|
||||
from orm.shout import Shout
|
||||
|
||||
def authors(self) -> int:
|
||||
# author has a shout with community id and its featured_at is not null
|
||||
return (
|
||||
self.community.session.query(func.count(distinct(Author.id)))
|
||||
.join(Shout)
|
||||
.filter(
|
||||
.where(
|
||||
Shout.community == self.community.id,
|
||||
Shout.featured_at.is_not(None),
|
||||
Author.id.in_(Shout.authors),
|
||||
@@ -369,15 +397,11 @@ class CommunityAuthor(BaseModel):
|
||||
|
||||
__tablename__ = "community_author"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
community_id = Column(Integer, ForeignKey("community.id"), nullable=False)
|
||||
author_id = Column(Integer, ForeignKey("author.id"), nullable=False)
|
||||
roles = Column(String, 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])
|
||||
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)
|
||||
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()))
|
||||
|
||||
# Уникальность по сообществу и автору
|
||||
__table_args__ = (
|
||||
@@ -397,41 +421,40 @@ class CommunityAuthor(BaseModel):
|
||||
"""Устанавливает список ролей из списка строк"""
|
||||
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: Название роли для добавления
|
||||
role (str): Название роли
|
||||
"""
|
||||
roles = self.role_list
|
||||
if role not in roles:
|
||||
roles.append(role)
|
||||
self.role_list = roles
|
||||
if not self.roles:
|
||||
self.roles = role
|
||||
elif role not in self.role_list:
|
||||
self.roles += f",{role}"
|
||||
|
||||
def remove_role(self, role: str) -> None:
|
||||
"""
|
||||
Удаляет роль у автора
|
||||
Удаляет роль из списка ролей.
|
||||
|
||||
Args:
|
||||
role: Название роли для удаления
|
||||
role (str): Название роли
|
||||
"""
|
||||
roles = self.role_list
|
||||
if role in roles:
|
||||
roles.remove(role)
|
||||
self.role_list = roles
|
||||
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
|
||||
|
||||
def has_role(self, role: str) -> bool:
|
||||
"""
|
||||
Проверяет наличие роли.
|
||||
|
||||
Args:
|
||||
role (str): Название роли
|
||||
|
||||
Returns:
|
||||
bool: True, если роль есть, иначе False
|
||||
"""
|
||||
return bool(self.roles and role in self.role_list)
|
||||
|
||||
def set_roles(self, roles: list[str]) -> None:
|
||||
"""
|
||||
@@ -443,7 +466,7 @@ class CommunityAuthor(BaseModel):
|
||||
# Фильтруем и очищаем роли
|
||||
valid_roles = [role.strip() for role in roles if role and role.strip()]
|
||||
|
||||
# Если список пустой, устанавливаем None
|
||||
# Если список пустой, устанавливаем пустую строку
|
||||
self.roles = ",".join(valid_roles) if valid_roles else ""
|
||||
|
||||
async def get_permissions(self) -> list[str]:
|
||||
@@ -461,17 +484,30 @@ class CommunityAuthor(BaseModel):
|
||||
|
||||
return list(all_permissions)
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
def has_permission(
|
||||
self, permission: str | None = None, resource: str | None = None, operation: str | None = None
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет наличие разрешения у автора
|
||||
|
||||
Args:
|
||||
permission: Разрешение для проверки (например: "shout:create")
|
||||
resource: Опциональный ресурс (для обратной совместимости)
|
||||
operation: Опциональная операция (для обратной совместимости)
|
||||
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
return permission in self.role_list
|
||||
# Если передан полный permission, используем его
|
||||
if permission and ":" in permission:
|
||||
return any(permission == role for role in self.role_list)
|
||||
|
||||
# Если переданы resource и operation, формируем permission
|
||||
if resource and operation:
|
||||
full_permission = f"{resource}:{operation}"
|
||||
return any(full_permission == role for role in self.role_list)
|
||||
|
||||
return False
|
||||
|
||||
def dict(self, access: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -510,13 +546,11 @@ class CommunityAuthor(BaseModel):
|
||||
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()
|
||||
community_authors = session.query(cls).where(cls.author_id == author_id).all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -529,7 +563,7 @@ class CommunityAuthor(BaseModel):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def find_by_user_and_community(cls, author_id: int, community_id: int, session=None) -> "CommunityAuthor | None":
|
||||
def find_author_in_community(cls, author_id: int, community_id: int, session=None) -> "CommunityAuthor | None":
|
||||
"""
|
||||
Находит запись CommunityAuthor по ID автора и сообщества
|
||||
|
||||
@@ -541,13 +575,11 @@ class CommunityAuthor(BaseModel):
|
||||
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 ssession.query(cls).where(cls.author_id == author_id, cls.community_id == community_id).first()
|
||||
|
||||
return session.query(cls).filter(cls.author_id == author_id, cls.community_id == community_id).first()
|
||||
return session.query(cls).where(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]:
|
||||
@@ -562,13 +594,11 @@ class CommunityAuthor(BaseModel):
|
||||
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()
|
||||
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
||||
|
||||
return [ca.author_id for ca in community_authors if ca.has_role(role)]
|
||||
|
||||
@@ -584,13 +614,11 @@ class CommunityAuthor(BaseModel):
|
||||
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()
|
||||
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
||||
|
||||
role_counts: dict[str, int] = {}
|
||||
total_members = len(community_authors)
|
||||
@@ -622,10 +650,8 @@ def get_user_roles_in_community(author_id: int, community_id: int = 1) -> list[s
|
||||
Returns:
|
||||
Список ролей пользователя
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
ca = CommunityAuthor.find_by_user_and_community(author_id, community_id, session)
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community_id, session)
|
||||
return ca.role_list if ca else []
|
||||
|
||||
|
||||
@@ -641,9 +667,6 @@ async def check_user_permission_in_community(author_id: int, permission: str, co
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
# Используем новую систему RBAC с иерархией
|
||||
from services.rbac import user_has_permission
|
||||
|
||||
return await user_has_permission(author_id, permission, community_id)
|
||||
|
||||
|
||||
@@ -659,10 +682,8 @@ def assign_role_to_user(author_id: int, role: str, community_id: int = 1) -> boo
|
||||
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)
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community_id, session)
|
||||
|
||||
if ca:
|
||||
if ca.has_role(role):
|
||||
@@ -689,10 +710,8 @@ def remove_role_from_user(author_id: int, role: str, community_id: int = 1) -> b
|
||||
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)
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community_id, session)
|
||||
|
||||
if ca and ca.has_role(role):
|
||||
ca.remove_role(role)
|
||||
@@ -713,9 +732,6 @@ def migrate_old_roles_to_community_author():
|
||||
|
||||
[непроверенное] Предполагает, что старые роли хранились в 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()
|
||||
@@ -732,10 +748,7 @@ def migrate_old_roles_to_community_author():
|
||||
|
||||
# Извлекаем базовое имя роли (убираем суффикс сообщества если есть)
|
||||
role_name = role.role
|
||||
if isinstance(role_name, str) and "-" in role_name:
|
||||
base_role = role_name.split("-")[0]
|
||||
else:
|
||||
base_role = role_name
|
||||
base_role = role_name.split("-")[0] if (isinstance(role_name, str) and "-" in role_name) else role_name
|
||||
|
||||
if base_role not in user_community_roles[key]:
|
||||
user_community_roles[key].append(base_role)
|
||||
@@ -744,7 +757,7 @@ def migrate_old_roles_to_community_author():
|
||||
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)
|
||||
existing = CommunityAuthor.find_author_in_community(author_id, community_id, session)
|
||||
|
||||
if not existing:
|
||||
ca = CommunityAuthor(community_id=community_id, author_id=author_id)
|
||||
@@ -772,10 +785,8 @@ def get_all_community_members_with_roles(community_id: int = 1) -> list[dict[str
|
||||
Returns:
|
||||
Список участников с полной информацией
|
||||
"""
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
community = session.query(Community).filter(Community.id == community_id).first()
|
||||
community = session.query(Community).where(Community.id == community_id).first()
|
||||
|
||||
if not community:
|
||||
return []
|
||||
|
84
orm/draft.py
84
orm/draft.py
@@ -1,7 +1,8 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import JSON, Boolean, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel as Base
|
||||
@@ -11,45 +12,68 @@ from orm.topic import Topic
|
||||
class DraftTopic(Base):
|
||||
__tablename__ = "draft_topic"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
|
||||
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
|
||||
main = Column(Boolean, nullable=True)
|
||||
draft: Mapped[int] = mapped_column(ForeignKey("draft.id"), index=True)
|
||||
topic: Mapped[int] = mapped_column(ForeignKey("topic.id"), index=True)
|
||||
main: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(draft, topic),
|
||||
Index("idx_draft_topic_topic", "topic"),
|
||||
Index("idx_draft_topic_draft", "draft"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
|
||||
class DraftAuthor(Base):
|
||||
__tablename__ = "draft_author"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
|
||||
author = Column(ForeignKey("author.id"), primary_key=True, index=True)
|
||||
caption = Column(String, nullable=True, default="")
|
||||
draft: Mapped[int] = mapped_column(ForeignKey("draft.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__ = (
|
||||
PrimaryKeyConstraint(draft, author),
|
||||
Index("idx_draft_author_author", "author"),
|
||||
Index("idx_draft_author_draft", "draft"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
|
||||
class Draft(Base):
|
||||
__tablename__ = "draft"
|
||||
# required
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
community = Column(ForeignKey("community.id"), nullable=False, default=1)
|
||||
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)
|
||||
community: Mapped[int] = mapped_column(ForeignKey("community.id"), nullable=False, default=1)
|
||||
|
||||
# optional
|
||||
layout = Column(String, nullable=True, default="article")
|
||||
slug = Column(String, unique=True)
|
||||
title = Column(String, nullable=True)
|
||||
subtitle = Column(String, nullable=True)
|
||||
lead = Column(String, nullable=True)
|
||||
body = Column(String, nullable=False, comment="Body")
|
||||
media = Column(JSON, nullable=True)
|
||||
cover = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lang = Column(String, nullable=False, default="ru", comment="Language")
|
||||
seo = Column(String, nullable=True) # JSON
|
||||
layout: Mapped[str | None] = mapped_column(String, nullable=True, default="article")
|
||||
slug: Mapped[str | None] = mapped_column(String, unique=True)
|
||||
title: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
subtitle: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
lead: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
body: Mapped[str] = mapped_column(String, nullable=False, comment="Body")
|
||||
media: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
cover: Mapped[str | None] = mapped_column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption: Mapped[str | None] = mapped_column(String, nullable=True, comment="Cover image alt caption")
|
||||
lang: Mapped[str] = mapped_column(String, nullable=False, default="ru", comment="Language")
|
||||
seo: Mapped[str | None] = mapped_column(String, nullable=True) # JSON
|
||||
|
||||
# auto
|
||||
updated_at = Column(Integer, nullable=True, index=True)
|
||||
deleted_at = Column(Integer, nullable=True, index=True)
|
||||
updated_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
deleted_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
authors = relationship(Author, secondary="draft_author")
|
||||
topics = relationship(Topic, secondary="draft_topic")
|
||||
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__)
|
||||
topics = relationship(Topic, secondary=DraftTopic.__table__)
|
||||
|
||||
# shout/publication
|
||||
# Временно закомментировано для совместимости с тестами
|
||||
# shout: Mapped[int | None] = mapped_column(ForeignKey("shout.id"), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_draft_created_by", "created_by"),
|
||||
Index("idx_draft_community", "community"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import ForeignKey, Index, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from orm.base import BaseModel as Base
|
||||
|
||||
@@ -12,24 +12,33 @@ class InviteStatus(enum.Enum):
|
||||
REJECTED = "REJECTED"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value: str) -> "Invite":
|
||||
def from_string(cls, value: str) -> "InviteStatus":
|
||||
return cls(value)
|
||||
|
||||
|
||||
class Invite(Base):
|
||||
__tablename__ = "invite"
|
||||
|
||||
inviter_id = Column(ForeignKey("author.id"), primary_key=True)
|
||||
author_id = Column(ForeignKey("author.id"), primary_key=True)
|
||||
shout_id = Column(ForeignKey("shout.id"), primary_key=True)
|
||||
status = Column(String, default=InviteStatus.PENDING.value)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
inviter_id: Mapped[int] = mapped_column(ForeignKey("author.id"))
|
||||
author_id: Mapped[int] = mapped_column(ForeignKey("author.id"))
|
||||
shout_id: Mapped[int] = mapped_column(ForeignKey("shout.id"))
|
||||
status: Mapped[str] = mapped_column(String, default=InviteStatus.PENDING.value)
|
||||
|
||||
inviter = relationship("Author", foreign_keys=[inviter_id])
|
||||
author = relationship("Author", foreign_keys=[author_id])
|
||||
shout = relationship("Shout")
|
||||
|
||||
def set_status(self, status: InviteStatus):
|
||||
self.status = status.value # type: ignore[assignment]
|
||||
__table_args__ = (
|
||||
UniqueConstraint(inviter_id, author_id, shout_id),
|
||||
Index("idx_invite_inviter_id", "inviter_id"),
|
||||
Index("idx_invite_author_id", "author_id"),
|
||||
Index("idx_invite_shout_id", "shout_id"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
def set_status(self, status: InviteStatus) -> None:
|
||||
self.status = status.value
|
||||
|
||||
def get_status(self) -> InviteStatus:
|
||||
return InviteStatus.from_string(self.status)
|
||||
return InviteStatus.from_string(str(self.status))
|
||||
|
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy import Enum as SQLAlchemyEnum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import JSON, DateTime, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel as Base
|
||||
@@ -21,6 +21,7 @@ class NotificationEntity(Enum):
|
||||
SHOUT = "shout"
|
||||
AUTHOR = "author"
|
||||
COMMUNITY = "community"
|
||||
REACTION = "reaction"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value: str) -> "NotificationEntity":
|
||||
@@ -80,27 +81,41 @@ NotificationKind = NotificationAction # Для совместимости со
|
||||
class NotificationSeen(Base):
|
||||
__tablename__ = "notification_seen"
|
||||
|
||||
viewer = Column(ForeignKey("author.id"), primary_key=True)
|
||||
notification = Column(ForeignKey("notification.id"), primary_key=True)
|
||||
viewer: Mapped[int] = mapped_column(ForeignKey("author.id"))
|
||||
notification: Mapped[int] = mapped_column(ForeignKey("notification.id"))
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(viewer, notification),
|
||||
Index("idx_notification_seen_viewer", "viewer"),
|
||||
Index("idx_notification_seen_notification", "notification"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notification"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
entity = Column(String, nullable=False)
|
||||
action = Column(String, nullable=False)
|
||||
payload = Column(JSON, nullable=True)
|
||||
entity: Mapped[str] = mapped_column(String, nullable=False)
|
||||
action: Mapped[str] = mapped_column(String, nullable=False)
|
||||
payload: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
status = Column(SQLAlchemyEnum(NotificationStatus), default=NotificationStatus.UNREAD)
|
||||
kind = Column(SQLAlchemyEnum(NotificationKind), nullable=False)
|
||||
status: Mapped[NotificationStatus] = mapped_column(default=NotificationStatus.UNREAD)
|
||||
kind: Mapped[NotificationKind] = mapped_column(nullable=False)
|
||||
|
||||
seen = relationship(Author, secondary="notification_seen")
|
||||
|
||||
def set_entity(self, entity: NotificationEntity):
|
||||
__table_args__ = (
|
||||
Index("idx_notification_created_at", "created_at"),
|
||||
Index("idx_notification_status", "status"),
|
||||
Index("idx_notification_kind", "kind"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
def set_entity(self, entity: NotificationEntity) -> None:
|
||||
"""Устанавливает сущность уведомления."""
|
||||
self.entity = entity.value
|
||||
|
||||
@@ -108,7 +123,7 @@ class Notification(Base):
|
||||
"""Возвращает сущность уведомления."""
|
||||
return NotificationEntity.from_string(self.entity)
|
||||
|
||||
def set_action(self, action: NotificationAction):
|
||||
def set_action(self, action: NotificationAction) -> None:
|
||||
"""Устанавливает действие уведомления."""
|
||||
self.action = action.value
|
||||
|
||||
|
@@ -10,21 +10,14 @@ PROPOSAL_REACTIONS = [
|
||||
]
|
||||
|
||||
PROOF_REACTIONS = [ReactionKind.PROOF.value, ReactionKind.DISPROOF.value]
|
||||
|
||||
RATING_REACTIONS = [ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]
|
||||
POSITIVE_REACTIONS = [ReactionKind.ACCEPT.value, ReactionKind.LIKE.value, ReactionKind.PROOF.value]
|
||||
NEGATIVE_REACTIONS = [ReactionKind.REJECT.value, ReactionKind.DISLIKE.value, ReactionKind.DISPROOF.value]
|
||||
|
||||
|
||||
def is_negative(x):
|
||||
return x in [
|
||||
ReactionKind.DISLIKE.value,
|
||||
ReactionKind.DISPROOF.value,
|
||||
ReactionKind.REJECT.value,
|
||||
]
|
||||
def is_negative(x: ReactionKind) -> bool:
|
||||
return x.value in NEGATIVE_REACTIONS
|
||||
|
||||
|
||||
def is_positive(x):
|
||||
return x in [
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.PROOF.value,
|
||||
]
|
||||
def is_positive(x: ReactionKind) -> bool:
|
||||
return x.value in POSITIVE_REACTIONS
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import time
|
||||
from enum import Enum as Enumeration
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
from sqlalchemy import ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel as Base
|
||||
|
||||
|
||||
@@ -44,15 +46,24 @@ REACTION_KINDS = ReactionKind.__members__.keys()
|
||||
class Reaction(Base):
|
||||
__tablename__ = "reaction"
|
||||
|
||||
body = Column(String, default="", comment="Reaction Body")
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()), index=True)
|
||||
updated_at = Column(Integer, nullable=True, comment="Updated at", index=True)
|
||||
deleted_at = Column(Integer, nullable=True, comment="Deleted at", index=True)
|
||||
deleted_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
reply_to = Column(ForeignKey("reaction.id"), nullable=True)
|
||||
quote = Column(String, nullable=True, comment="Original quoted text")
|
||||
shout = Column(ForeignKey("shout.id"), nullable=False, index=True)
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
kind = Column(String, nullable=False, index=True)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
body: Mapped[str] = mapped_column(String, default="", comment="Reaction Body")
|
||||
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)
|
||||
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)
|
||||
kind: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
||||
|
||||
oid = Column(String)
|
||||
oid: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_reaction_created_at", "created_at"),
|
||||
Index("idx_reaction_created_by", "created_by"),
|
||||
Index("idx_reaction_shout", "shout"),
|
||||
Index("idx_reaction_kind", "kind"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
86
orm/shout.py
86
orm/shout.py
@@ -1,7 +1,8 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import JSON, Boolean, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel as Base
|
||||
@@ -21,13 +22,13 @@ class ShoutTopic(Base):
|
||||
|
||||
__tablename__ = "shout_topic"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
|
||||
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
|
||||
main = Column(Boolean, nullable=True)
|
||||
shout: Mapped[int] = mapped_column(ForeignKey("shout.id"), index=True)
|
||||
topic: Mapped[int] = mapped_column(ForeignKey("topic.id"), index=True)
|
||||
main: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
|
||||
# Определяем дополнительные индексы
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(shout, topic),
|
||||
# Оптимизированный составной индекс для запросов, которые ищут публикации по теме
|
||||
Index("idx_shout_topic_topic_shout", "topic", "shout"),
|
||||
)
|
||||
@@ -36,12 +37,18 @@ class ShoutTopic(Base):
|
||||
class ShoutReactionsFollower(Base):
|
||||
__tablename__ = "shout_reactions_followers"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
|
||||
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
|
||||
auto = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
deleted_at = Column(Integer, nullable=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()))
|
||||
deleted_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(follower, shout),
|
||||
Index("idx_shout_reactions_followers_follower", "follower"),
|
||||
Index("idx_shout_reactions_followers_shout", "shout"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
|
||||
class ShoutAuthor(Base):
|
||||
@@ -56,13 +63,13 @@ class ShoutAuthor(Base):
|
||||
|
||||
__tablename__ = "shout_author"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
|
||||
author = Column(ForeignKey("author.id"), primary_key=True, index=True)
|
||||
caption = Column(String, nullable=True, default="")
|
||||
shout: Mapped[int] = mapped_column(ForeignKey("shout.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__ = (
|
||||
PrimaryKeyConstraint(shout, author),
|
||||
# Оптимизированный индекс для запросов, которые ищут публикации по автору
|
||||
Index("idx_shout_author_author_shout", "author", "shout"),
|
||||
)
|
||||
@@ -75,37 +82,36 @@ class Shout(Base):
|
||||
|
||||
__tablename__ = "shout"
|
||||
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
updated_at = Column(Integer, nullable=True, index=True)
|
||||
published_at = Column(Integer, nullable=True, index=True)
|
||||
featured_at = Column(Integer, nullable=True, index=True)
|
||||
deleted_at = Column(Integer, nullable=True, index=True)
|
||||
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()))
|
||||
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
published_at: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
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 = Column(ForeignKey("author.id"), nullable=False)
|
||||
updated_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
deleted_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
community = Column(ForeignKey("community.id"), nullable=False)
|
||||
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 = Column(String, nullable=False, comment="Body")
|
||||
slug = Column(String, unique=True)
|
||||
cover = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lead = Column(String, nullable=True)
|
||||
title = Column(String, nullable=False)
|
||||
subtitle = Column(String, nullable=True)
|
||||
layout = Column(String, nullable=False, default="article")
|
||||
media = Column(JSON, nullable=True)
|
||||
body: Mapped[str] = mapped_column(String, nullable=False, comment="Body")
|
||||
slug: Mapped[str | None] = mapped_column(String, unique=True)
|
||||
cover: Mapped[str | None] = mapped_column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption: Mapped[str | None] = mapped_column(String, nullable=True, comment="Cover image alt caption")
|
||||
lead: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
subtitle: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
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)
|
||||
|
||||
lang = Column(String, nullable=False, default="ru", comment="Language")
|
||||
version_of = Column(ForeignKey("shout.id"), nullable=True)
|
||||
oid = Column(String, nullable=True)
|
||||
seo = Column(String, nullable=True) # JSON
|
||||
|
||||
draft = Column(ForeignKey("draft.id"), nullable=True)
|
||||
lang: Mapped[str] = mapped_column(String, nullable=False, default="ru", comment="Language")
|
||||
version_of: Mapped[int | None] = mapped_column(ForeignKey("shout.id"), nullable=True)
|
||||
oid: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
seo: Mapped[str | None] = mapped_column(String, nullable=True) # JSON
|
||||
|
||||
# Определяем индексы
|
||||
__table_args__ = (
|
||||
|
37
orm/topic.py
37
orm/topic.py
@@ -1,7 +1,17 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.base import BaseModel as Base
|
||||
|
||||
|
||||
@@ -18,14 +28,14 @@ class TopicFollower(Base):
|
||||
|
||||
__tablename__ = "topic_followers"
|
||||
|
||||
id = None # type: ignore[misc]
|
||||
follower = Column(Integer, ForeignKey("author.id"), primary_key=True)
|
||||
topic = Column(Integer, ForeignKey("topic.id"), primary_key=True)
|
||||
created_at = Column(Integer, nullable=False, default=int(time.time()))
|
||||
auto = Column(Boolean, nullable=False, default=False)
|
||||
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)
|
||||
|
||||
# Определяем индексы
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(topic, follower),
|
||||
# Индекс для быстрого поиска всех подписчиков топика
|
||||
Index("idx_topic_followers_topic", "topic"),
|
||||
# Индекс для быстрого поиска всех топиков, на которые подписан автор
|
||||
@@ -49,13 +59,14 @@ class Topic(Base):
|
||||
|
||||
__tablename__ = "topic"
|
||||
|
||||
slug = Column(String, unique=True)
|
||||
title = Column(String, nullable=False, comment="Title")
|
||||
body = Column(String, nullable=True, comment="Body")
|
||||
pic = Column(String, nullable=True, comment="Picture")
|
||||
community = Column(ForeignKey("community.id"), default=1)
|
||||
oid = Column(String, nullable=True, comment="Old ID")
|
||||
parent_ids = Column(JSON, nullable=True, comment="Parent Topic IDs")
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
slug: Mapped[str] = mapped_column(String, unique=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False, comment="Title")
|
||||
body: Mapped[str | None] = mapped_column(String, nullable=True, comment="Body")
|
||||
pic: Mapped[str | None] = mapped_column(String, nullable=True, comment="Picture")
|
||||
community: Mapped[int] = mapped_column(ForeignKey("community.id"), default=1)
|
||||
oid: Mapped[str | None] = mapped_column(String, nullable=True, comment="Old ID")
|
||||
parent_ids: Mapped[list[int] | None] = mapped_column(JSON, nullable=True, comment="Parent Topic IDs")
|
||||
|
||||
# Определяем индексы
|
||||
__table_args__ = (
|
||||
|
Reference in New Issue
Block a user