feat: modules and module-editor pages

This commit is contained in:
2025-10-25 12:11:48 +04:00
parent 62aeebf079
commit 281600c72d
16 changed files with 751 additions and 25 deletions

View File

@ -7,7 +7,7 @@ from starlette.responses import JSONResponse
import routers
from utils.auto_include_routers import auto_include_routers
from utils.exceptions import ObjectNotFoundException
from utils.exceptions import *
origins = ["http://localhost:3000"]
@ -31,10 +31,15 @@ app.add_middleware(
@app.exception_handler(ObjectNotFoundException)
async def unicorn_exception_handler(request: Request, exc: ObjectNotFoundException):
async def not_found_exception_handler(request: Request, exc: ObjectNotFoundException):
return JSONResponse(status_code=404, content={"detail": exc.name})
@app.exception_handler(ForbiddenException)
async def forbidden_exception_handler(request: Request, exc: ForbiddenException):
return JSONResponse(status_code=403, content={"detail": exc.name})
auto_include_routers(app, routers, True)
app.mount("/static", StaticFiles(directory="static"), name="static")

View File

@ -1,13 +1,15 @@
from sqlalchemy.orm import configure_mappers
from modules.fulfillment_base.models import * # noqa: F401
from .attribute import (
AttributeType as AttributeType,
Attribute as Attribute,
AttributeValue as AttributeValue,
AttributeLabel as AttributeLabel,
module_attribute as module_attribute,
)
from .base import BaseModel as BaseModel
from .board import Board as Board
from .module import ( # noqa: F401
Module as Module,
project_module as project_module,
module_dependencies as module_dependencies,
)
from .deal import Deal as Deal
from .deal_group import DealGroup as DealGroup
from .deal_tag import (
@ -15,6 +17,11 @@ from .deal_tag import (
DealTagColor as DealTagColor,
deals_deal_tags as deals_deal_tags,
)
from .module import ( # noqa: F401
Module as Module,
project_module as project_module,
module_dependencies as module_dependencies,
)
from .project import Project as Project
from .status import Status as Status, DealStatusHistory as DealStatusHistory

139
models/attribute.py Normal file
View File

@ -0,0 +1,139 @@
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
if TYPE_CHECKING:
from models import Module, Deal
module_attribute = Table(
"module_attribute",
BaseModel.metadata,
Column("module_id", ForeignKey("modules.id"), primary_key=True),
Column("attribute_id", ForeignKey("attributes.id"), primary_key=True),
)
class AttributeType(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "attribute_types"
type: Mapped[str] = mapped_column(unique=True)
name: Mapped[str] = mapped_column(unique=True)
attributes: Mapped["Attribute"] = relationship(
back_populates="type",
lazy="noload",
)
class Attribute(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "attributes"
label: Mapped[str] = mapped_column()
is_applicable_to_group: Mapped[bool] = mapped_column(
default=False,
comment="Применять ли изменения атрибута карточки ко всем карточкам в группе",
)
is_shown_on_dashboard: Mapped[bool] = mapped_column(
default=False,
comment="Отображается ли атрибут на дашборде",
)
is_highlight_if_expired: Mapped[bool] = mapped_column(
default=False,
comment="Подсветка атрибута, если Дата/ДатаВремя просрочена",
)
is_nullable: Mapped[bool] = mapped_column(default=False)
default_value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
description: Mapped[str] = mapped_column(default="")
is_built_in: Mapped[bool] = mapped_column(default=False)
type_id: Mapped[int] = mapped_column(ForeignKey("attribute_types.id"))
type: Mapped[AttributeType] = relationship(
back_populates="attributes",
lazy="joined",
)
modules: Mapped[list["Module"]] = relationship(
secondary=module_attribute,
back_populates="attributes",
lazy="noload",
)
values: Mapped[list["AttributeValue"]] = relationship(
uselist=True,
back_populates="attribute",
lazy="noload",
)
class AttributeValue(BaseModel, IdMixin):
__tablename__ = "attribute_values"
value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
deal_id: Mapped[int] = mapped_column(
ForeignKey("deals.id"),
primary_key=True,
)
deal: Mapped["Deal"] = relationship(
back_populates="attributes_values",
lazy="noload",
)
module_id: Mapped[int] = mapped_column(
ForeignKey("modules.id"),
primary_key=True,
)
module: Mapped["Module"] = relationship(
back_populates="attribute_values",
lazy="noload",
)
attribute_id: Mapped[int] = mapped_column(
ForeignKey("attributes.id"),
primary_key=True,
)
attribute: Mapped[Attribute] = relationship(
back_populates="values",
lazy="joined",
)
def set_value(self, value: Optional[dict | str | bool | int | float]):
if value is None:
return
self.value = {"value": value}
def get_value(self) -> Optional[dict | str | bool | int | float]:
if self.value is None:
return None
return self.value["value"]
class AttributeLabel(BaseModel):
__tablename__ = "attribute_labels"
label: Mapped[str] = mapped_column()
module_id: Mapped[int] = mapped_column(
ForeignKey("modules.id"),
primary_key=True,
)
module: Mapped["Module"] = relationship(
backref="attribute_labels",
lazy="noload",
)
attribute_id: Mapped[int] = mapped_column(
ForeignKey("attributes.id"),
primary_key=True,
)
attribute: Mapped[Attribute] = relationship(
backref="attribute_labels",
lazy="joined",
)

View File

@ -7,7 +7,14 @@ from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING:
from models import Status, Board, DealStatusHistory, DealGroup, DealTag
from models import (
Status,
Board,
DealStatusHistory,
DealGroup,
DealTag,
AttributeValue,
)
from modules.clients.models import Client
@ -49,6 +56,10 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
secondaryjoin="and_(DealTag.id == deals_deal_tags.c.deal_tag_id, DealTag.is_deleted == False)",
)
attributes_values: Mapped[list["AttributeValue"]] = relationship(
back_populates="deal",
)
# module client
client_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("clients.id", ondelete="CASCADE"),

View File

@ -1,9 +1,10 @@
import enum
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import AttributeValue, Attribute
from models.base import BaseModel
if TYPE_CHECKING:
@ -31,8 +32,9 @@ class Module(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True)
label: Mapped[str] = mapped_column()
description: Mapped[str] = mapped_column()
description: Mapped[Optional[str]] = mapped_column()
is_deleted: Mapped[bool] = mapped_column(default=False)
is_built_in: Mapped[bool] = mapped_column(default=False, server_default="0")
depends_on: Mapped[list["Module"]] = relationship(
secondary=module_dependencies,
@ -61,6 +63,14 @@ class Module(BaseModel):
lazy="immediate", backref="module", cascade="all, delete-orphan"
)
attributes: Mapped[list["Attribute"]] = relationship(
secondary="module_attribute", back_populates="modules"
)
attribute_values: Mapped[list["AttributeValue"]] = relationship(
lazy="noload", back_populates="module"
)
class DeviceType(enum.StrEnum):
MOBILE = "mobile"

View File

@ -1,7 +1,8 @@
from .attribute import AttributeRepository as AttributeRepository
from .board import BoardRepository as BoardRepository
from .module import ModuleRepository as ModuleRepository
from .deal import DealRepository as DealRepository
from .deal_group import DealGroupRepository as DealGroupRepository
from .deal_tag import DealTagRepository as DealTagRepository
from .module import ModuleRepository as ModuleRepository
from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository

73
repositories/attribute.py Normal file
View File

@ -0,0 +1,73 @@
from sqlalchemy.orm import joinedload
from models import Attribute, AttributeLabel, AttributeType
from repositories.mixins import *
from schemas.attribute import CreateAttributeSchema, UpdateAttributeSchema
from utils.exceptions import ForbiddenException
class AttributeRepository(
RepCrudMixin[Attribute, CreateAttributeSchema, UpdateAttributeSchema]
):
session: AsyncSession
entity_class = Attribute
def _process_get_all_stmt(self, stmt: Select) -> Select:
return (
stmt.options(joinedload(Attribute.type))
.where(Attribute.is_deleted.is_(False))
.order_by(Attribute.id)
)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(joinedload(Attribute.type))
async def _get_attribute_type_by_id(self, type_id: int) -> AttributeType:
stmt = select(AttributeType).where(AttributeType.id == type_id)
result = (await self.session.execute(stmt)).one_or_none()
if result is None:
raise ObjectNotFoundException("Тип аттрибута не найден")
return result[0]
async def update(self, attr: Attribute, data: UpdateAttributeSchema) -> Attribute:
if data.type:
data.type = await self._get_attribute_type_by_id(data.type.id)
return await self._apply_update_data_to_model(attr, data, True)
async def _before_delete(self, attribute: Attribute) -> None:
if attribute.is_built_in:
raise ForbiddenException("Нельзя менять встроенный атрибут")
async def _get_attribute_module_label(
self, module_id: int, attribute_id: int
) -> Optional[AttributeLabel]:
stmt = select(AttributeLabel).where(
AttributeLabel.attribute_id == attribute_id,
AttributeLabel.module_id == module_id,
)
result = await self.session.execute(stmt)
row = result.one_or_none()
return row[0] if row else None
async def create_or_update_attribute_label(
self, module_id: int, attribute_id: int, label: str
):
attribute_label = await self._get_attribute_module_label(
module_id, attribute_id
)
if attribute_label:
attribute_label.label = label
else:
attribute_label = AttributeLabel(
module_id=module_id,
attribute_id=attribute_id,
label=label,
)
self.session.add(attribute_label)
await self.session.commit()
async def get_attribute_types(self) -> list[AttributeType]:
stmt = select(AttributeType).where(AttributeType.is_deleted.is_(False))
result = await self.session.execute(stmt)
return list(result.scalars().all())

View File

@ -1,18 +1,69 @@
from models import Board, Module
from sqlalchemy import and_
from sqlalchemy.orm import selectinload
from models import Module, Attribute, AttributeLabel, module_attribute
from repositories.mixins import *
from schemas.module import UpdateModuleCommonInfoSchema
class ModuleRepository(
BaseRepository,
RepGetAllMixin[Module],
RepGetByIdMixin[Module],
RepUpdateMixin[Module, UpdateModuleCommonInfoSchema],
RepDeleteMixin[Module]
):
entity_class = Module
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
project_id = args[0]
return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(Module.attributes).joinedload(Attribute.type))
async def get_by_ids(self, ids: list[int]) -> list[Module]:
stmt = select(Module).where(Module.id.in_(ids))
modules = await self.session.scalars(stmt)
return modules.all()
@staticmethod
def _get_stmt_modules_with_tuples() -> Select:
return (
select(Module, Attribute, AttributeLabel)
.join(
module_attribute,
Module.id == module_attribute.c.module_id,
isouter=True,
)
.join(
Attribute, module_attribute.c.attribute_id == Attribute.id, isouter=True
)
.join(
AttributeLabel,
and_(
Module.id == AttributeLabel.module_id,
Attribute.id == AttributeLabel.attribute_id,
),
isouter=True,
)
.where(Module.is_deleted.is_(False), Attribute.is_deleted.is_(False))
.order_by(Attribute.id)
)
async def get_with_attributes_as_tuples(
self,
) -> list[tuple[Module, Attribute, AttributeLabel]]:
stmt = self._get_stmt_modules_with_tuples()
return (await self.session.execute(stmt)).unique().all()
async def get_with_attributes_as_tuple_by_id(
self, pk: int
) -> list[tuple[Module, Attribute, AttributeLabel]]:
stmt = self._get_stmt_modules_with_tuples()
stmt = stmt.where(Module.id == pk)
return (await self.session.execute(stmt)).unique().all()
async def add_attribute_to_module(self, module: Module, attribute: Attribute):
module.attributes.append(attribute)
await self.session.commit()
async def delete_attribute_from_module(self, module: Module, attribute: Attribute):
module.attributes.remove(attribute)
await self.session.commit()

View File

@ -0,0 +1,78 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.attribute import *
from services import AttributeService
router = APIRouter(tags=["attribute"])
@router.get(
"/",
response_model=GetAllAttributesResponse,
operation_id="get_attributes",
)
async def get_attributes(
session: SessionDependency,
):
return await AttributeService(session).get_all()
@router.post(
"/",
response_model=CreateAttributeResponse,
operation_id="create_attribute",
)
async def create_attribute(
session: SessionDependency,
request: CreateAttributeRequest,
):
return await AttributeService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateAttributeResponse,
operation_id="update_attribute",
)
async def update_attribute(
session: SessionDependency,
request: UpdateAttributeRequest,
pk: int = Path(),
):
return await AttributeService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteAttributeResponse,
operation_id="delete_attribute",
)
async def delete_attribute(
session: SessionDependency,
pk: int = Path(),
):
return await AttributeService(session).delete(pk)
@router.post(
"/label",
response_model=UpdateAttributeLabelResponse,
operation_id="update_attribute_label",
)
async def update_attribute_label(
session: SessionDependency,
request: UpdateAttributeLabelRequest,
):
return await AttributeService(session).update_attribute_label(request)
@router.get(
"/type",
response_model=GetAllAttributeTypesResponse,
operation_id="get_attribute_types",
)
async def get_attribute_types(
session: SessionDependency,
):
return await AttributeService(session).get_attribute_types()

View File

@ -1,14 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.module import GetAllModulesResponse
from schemas.module import *
from services.module import ModuleService
router = APIRouter(tags=["modules"])
@router.get(
"/built-in/",
"/",
response_model=GetAllModulesResponse,
operation_id="get_modules",
)
@ -16,3 +16,76 @@ async def get_modules(
session: SessionDependency,
):
return await ModuleService(session).get_all()
@router.get(
"/with-attributes",
response_model=GetAllWithAttributesResponse,
operation_id="get_modules_with_attributes",
)
async def get_modules_with_attributes(
session: SessionDependency,
):
return await ModuleService(session).get_with_attributes()
@router.get(
"/{pk}/with-attributes",
response_model=GetByIdWithAttributesResponse,
operation_id="get_module_with_attributes",
)
async def get_module_with_attributes(
session: SessionDependency,
pk: int = Path(),
):
return await ModuleService(session).get_by_id_with_attributes(pk)
@router.patch(
"/{pk}/common-info",
response_model=UpdateModuleCommonInfoResponse,
operation_id="update_module",
)
async def update_module_common_info(
session: SessionDependency,
request: UpdateModuleCommonInfoRequest,
pk: int = Path(),
):
return await ModuleService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteModuleResponse,
operation_id="delete_module",
)
async def delete_module(
session: SessionDependency,
pk: int = Path(),
):
return await ModuleService(session).delete(pk)
@router.post(
"/attribute",
response_model=AddAttributeResponse,
operation_id="add_attribute_to_module",
)
async def add_attribute_to_module(
session: SessionDependency,
request: AddAttributeRequest,
):
return await ModuleService(session).add_attribute(request)
@router.delete(
"/attribute",
response_model=DeleteAttributeResponse,
operation_id="remove_attribute_from_module",
)
async def remove_attribute_from_module(
session: SessionDependency,
request: DeleteAttributeRequest,
):
return await ModuleService(session).delete_attribute(request)

95
schemas/attribute.py Normal file
View File

@ -0,0 +1,95 @@
from typing import Optional, Any
from schemas.base import BaseSchema, BaseResponse
# region Entity
class AttributeTypeSchema(BaseSchema):
id: int
type: str
name: str
class CreateAttributeSchema(BaseSchema):
label: str
is_applicable_to_group: bool
is_shown_on_dashboard: bool
is_highlight_if_expired: bool
is_nullable: bool
default_value: Optional[dict[str, Any]]
description: str
type_id: int
class AttributeSchema(CreateAttributeSchema):
id: int
is_built_in: bool
type: AttributeTypeSchema
class UpdateAttributeSchema(BaseSchema):
label: Optional[str] = None
is_applicable_to_group: Optional[bool] = None
is_shown_on_dashboard: Optional[bool] = None
is_highlight_if_expired: Optional[bool] = None
is_nullable: Optional[bool] = None
default_value: Optional[dict[str, Any]] = None
description: Optional[str] = None
type: Optional[AttributeTypeSchema] = None
class ModuleAttributeSchema(AttributeSchema):
original_label: str
# endregion
# region Request
class CreateAttributeRequest(BaseSchema):
entity: CreateAttributeSchema
class UpdateAttributeRequest(BaseSchema):
entity: UpdateAttributeSchema
class UpdateAttributeLabelRequest(BaseSchema):
module_id: int
attribute_id: int
label: str
# endregion
# region Response
class GetAllAttributesResponse(BaseSchema):
items: list[AttributeSchema]
class CreateAttributeResponse(BaseResponse):
pass
class UpdateAttributeResponse(BaseResponse):
pass
class DeleteAttributeResponse(BaseResponse):
pass
class UpdateAttributeLabelResponse(BaseResponse):
pass
class GetAllAttributeTypesResponse(BaseSchema):
items: list[AttributeTypeSchema]
# endregion

View File

@ -1,4 +1,8 @@
from schemas.base import BaseSchema
from typing import Optional
from schemas.attribute import ModuleAttributeSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
@ -15,11 +19,41 @@ class ModuleSchema(BaseSchema):
id: int
key: str
label: str
description: str
description: Optional[str]
is_built_in: bool
depends_on: list["ModuleSchema"]
tabs: list[ModuleTabSchema]
class ModuleWithAttributesSchema(ModuleSchema):
attributes: list[ModuleAttributeSchema]
class UpdateModuleCommonInfoSchema(BaseSchema):
label: str
description: Optional[str]
# endregion
# region Requests
class AddAttributeRequest(BaseSchema):
attribute_id: int
module_id: int
class DeleteAttributeRequest(BaseSchema):
attribute_id: int
module_id: int
class UpdateModuleCommonInfoRequest(BaseSchema):
entity: UpdateModuleCommonInfoSchema
# endregion
# region Response
@ -29,4 +63,28 @@ class GetAllModulesResponse(BaseSchema):
items: list[ModuleSchema]
class GetAllWithAttributesResponse(BaseSchema):
items: list[ModuleWithAttributesSchema]
class GetByIdWithAttributesResponse(BaseSchema):
entity: ModuleWithAttributesSchema
class UpdateModuleCommonInfoResponse(BaseResponse):
pass
class DeleteModuleResponse(BaseResponse):
pass
class AddAttributeResponse(BaseResponse):
pass
class DeleteAttributeResponse(BaseResponse):
pass
# endregion

View File

@ -4,3 +4,4 @@ from .project import ProjectService as ProjectService
from .status import StatusService as StatusService
from .deal_group import DealGroupService as DealGroupService
from .deal_tag import DealTagService as DealTagService
from .attribute import AttributeService as AttributeService

31
services/attribute.py Normal file
View File

@ -0,0 +1,31 @@
from models import Attribute
from repositories import AttributeRepository
from schemas.attribute import *
from services.mixins import *
class AttributeService(
ServiceCrudMixin[
Attribute, AttributeSchema, CreateAttributeRequest, UpdateAttributeRequest
]
):
schema_class = AttributeSchema
def __init__(self, session: AsyncSession):
self.repository = AttributeRepository(session)
async def update_attribute_label(
self, request: UpdateAttributeLabelRequest
) -> UpdateAttributeLabelResponse:
await self.repository.create_or_update_attribute_label(
request.module_id, request.attribute_id, request.label
)
return UpdateAttributeLabelResponse(
message="Название атрибута в модуле изменено"
)
async def get_attribute_types(self) -> GetAllAttributeTypesResponse:
types = await self.repository.get_attribute_types()
return GetAllAttributeTypesResponse(
items=[AttributeTypeSchema.model_validate(t) for t in types]
)

View File

@ -1,11 +1,96 @@
from models import Module
from repositories import ModuleRepository
from schemas.module import ModuleSchema
from models import Module, Attribute
from repositories import ModuleRepository, AttributeRepository
from schemas.module import *
from services.mixins import *
from utils.exceptions import ForbiddenException
class ModuleService(ServiceGetAllMixin[Module, ModuleSchema]):
class ModuleService(
ServiceGetAllMixin[Module, ModuleSchema],
ServiceUpdateMixin[Module, UpdateModuleCommonInfoRequest],
ServiceDeleteMixin[Module],
):
schema_class = ModuleSchema
entity_updated_msg = "Модуль успешно обновлен"
entity_deleted_msg = "Модуль успешно удален"
def __init__(self, session: AsyncSession):
self.repository = ModuleRepository(session)
async def get_with_attributes(self) -> GetAllWithAttributesResponse:
result = await self.repository.get_with_attributes_as_tuples()
modules = self._build_modules_with_attributes(result)
return GetAllWithAttributesResponse(items=list(modules.values()))
async def get_by_id_with_attributes(self, pk: int) -> GetByIdWithAttributesResponse:
result = await self.repository.get_with_attributes_as_tuple_by_id(pk)
modules = self._build_modules_with_attributes(result)
module = next(iter(modules.values()), None)
if module is None:
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
return GetByIdWithAttributesResponse(entity=module)
async def update_module_common_info(
self, module_id: int, request: UpdateModuleCommonInfoRequest
) -> UpdateModuleCommonInfoResponse:
module = await self.repository.get_by_id(module_id)
await self.repository.update_(module, request)
return UpdateModuleCommonInfoResponse(message="Данные модуля успешно сохранены")
def _build_modules_with_attributes(
self, result: list[tuple]
) -> dict[int, ModuleWithAttributesSchema]:
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
for module, attribute, attribute_label in result:
new_attr = None
if attribute:
original_label = attribute.label
label = attribute_label.label if attribute_label else original_label
attr_values = {
**attribute.__dict__,
"label": label,
"original_label": original_label,
}
new_attr = ModuleAttributeSchema(**attr_values)
module_schema = module_attrs_dict.get(module.id)
if not module_schema:
module_schema = ModuleWithAttributesSchema(
**module.__dict__,
attributes=[new_attr] if new_attr else [],
)
module_attrs_dict[module.id] = module_schema
elif new_attr:
module_schema.attributes.append(new_attr)
return module_attrs_dict
async def is_soft_delete(self, module: Module) -> bool:
if module.is_built_in:
raise ForbiddenException("Нельзя менять встроенный модуль")
return True
async def add_attribute(self, request: AddAttributeRequest) -> AddAttributeResponse:
module, attribute = await self._get_module_and_attr_from_request(request)
await self.repository.add_attribute_to_module(module, attribute)
return AddAttributeResponse(message="Аттрибут успешно добавлен к модулю")
async def delete_attribute(
self, request: DeleteAttributeRequest
) -> DeleteAttributeResponse:
module, attribute = await self._get_module_and_attr_from_request(request)
await self.repository.delete_attribute_from_module(module, attribute)
return DeleteAttributeResponse(message="Аттрибут успешно удален из модуля")
async def _get_module_and_attr_from_request(
self, request: AddAttributeRequest | DeleteAttributeRequest
) -> tuple[Module, Attribute]:
module = await self.repository.get_by_id(request.module_id)
if module.is_built_in:
raise ForbiddenException("Нельзя менять встроенный модуль")
attr_repo = AttributeRepository(self.repository.session)
attribute = await attr_repo.get_by_id(request.attribute_id)
return module, attribute

View File

@ -1,3 +1,11 @@
class ObjectNotFoundException(Exception):
class BaseResponseException(Exception):
def __init__(self, name: str):
self.name = name
class ObjectNotFoundException(BaseResponseException):
pass
class ForbiddenException(BaseResponseException):
pass