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 import routers
from utils.auto_include_routers import auto_include_routers from utils.auto_include_routers import auto_include_routers
from utils.exceptions import ObjectNotFoundException from utils.exceptions import *
origins = ["http://localhost:3000"] origins = ["http://localhost:3000"]
@ -31,10 +31,15 @@ app.add_middleware(
@app.exception_handler(ObjectNotFoundException) @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}) 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) auto_include_routers(app, routers, True)
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")

View File

@ -1,13 +1,15 @@
from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import configure_mappers
from modules.fulfillment_base.models import * # noqa: F401 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 .base import BaseModel as BaseModel
from .board import Board as Board 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 import Deal as Deal
from .deal_group import DealGroup as DealGroup from .deal_group import DealGroup as DealGroup
from .deal_tag import ( from .deal_tag import (
@ -15,6 +17,11 @@ from .deal_tag import (
DealTagColor as DealTagColor, DealTagColor as DealTagColor,
deals_deal_tags as deals_deal_tags, 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 .project import Project as Project
from .status import Status as Status, DealStatusHistory as DealStatusHistory 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 from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING: 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 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)", 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 # module client
client_id: Mapped[Optional[int]] = mapped_column( client_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("clients.id", ondelete="CASCADE"), ForeignKey("clients.id", ondelete="CASCADE"),

View File

@ -1,9 +1,10 @@
import enum import enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from sqlalchemy import Table, Column, ForeignKey from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import AttributeValue, Attribute
from models.base import BaseModel from models.base import BaseModel
if TYPE_CHECKING: if TYPE_CHECKING:
@ -31,8 +32,9 @@ class Module(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True) key: Mapped[str] = mapped_column(unique=True)
label: Mapped[str] = mapped_column() 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_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( depends_on: Mapped[list["Module"]] = relationship(
secondary=module_dependencies, secondary=module_dependencies,
@ -61,6 +63,14 @@ class Module(BaseModel):
lazy="immediate", backref="module", cascade="all, delete-orphan" 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): class DeviceType(enum.StrEnum):
MOBILE = "mobile" MOBILE = "mobile"

View File

@ -1,7 +1,8 @@
from .attribute import AttributeRepository as AttributeRepository
from .board import BoardRepository as BoardRepository from .board import BoardRepository as BoardRepository
from .module import ModuleRepository as ModuleRepository
from .deal import DealRepository as DealRepository from .deal import DealRepository as DealRepository
from .deal_group import DealGroupRepository as DealGroupRepository from .deal_group import DealGroupRepository as DealGroupRepository
from .deal_tag import DealTagRepository as DealTagRepository from .deal_tag import DealTagRepository as DealTagRepository
from .module import ModuleRepository as ModuleRepository
from .project import ProjectRepository as ProjectRepository from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository 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 repositories.mixins import *
from schemas.module import UpdateModuleCommonInfoSchema
class ModuleRepository( class ModuleRepository(
BaseRepository, BaseRepository,
RepGetAllMixin[Module], RepGetAllMixin[Module],
RepGetByIdMixin[Module],
RepUpdateMixin[Module, UpdateModuleCommonInfoSchema],
RepDeleteMixin[Module]
): ):
entity_class = Module entity_class = Module
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select: def _process_get_by_id_stmt(self, stmt: Select) -> Select:
project_id = args[0] return stmt.options(selectinload(Module.attributes).joinedload(Attribute.type))
return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
async def get_by_ids(self, ids: list[int]) -> list[Module]: async def get_by_ids(self, ids: list[int]) -> list[Module]:
stmt = select(Module).where(Module.id.in_(ids)) stmt = select(Module).where(Module.id.in_(ids))
modules = await self.session.scalars(stmt) modules = await self.session.scalars(stmt)
return modules.all() 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 backend.dependecies import SessionDependency
from schemas.module import GetAllModulesResponse from schemas.module import *
from services.module import ModuleService from services.module import ModuleService
router = APIRouter(tags=["modules"]) router = APIRouter(tags=["modules"])
@router.get( @router.get(
"/built-in/", "/",
response_model=GetAllModulesResponse, response_model=GetAllModulesResponse,
operation_id="get_modules", operation_id="get_modules",
) )
@ -16,3 +16,76 @@ async def get_modules(
session: SessionDependency, session: SessionDependency,
): ):
return await ModuleService(session).get_all() 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 # region Entity
@ -15,11 +19,41 @@ class ModuleSchema(BaseSchema):
id: int id: int
key: str key: str
label: str label: str
description: str description: Optional[str]
is_built_in: bool
depends_on: list["ModuleSchema"] depends_on: list["ModuleSchema"]
tabs: list[ModuleTabSchema] 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 # endregion
# region Response # region Response
@ -29,4 +63,28 @@ class GetAllModulesResponse(BaseSchema):
items: list[ModuleSchema] 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 # endregion

View File

@ -4,3 +4,4 @@ from .project import ProjectService as ProjectService
from .status import StatusService as StatusService from .status import StatusService as StatusService
from .deal_group import DealGroupService as DealGroupService from .deal_group import DealGroupService as DealGroupService
from .deal_tag import DealTagService as DealTagService 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 models import Module, Attribute
from repositories import ModuleRepository from repositories import ModuleRepository, AttributeRepository
from schemas.module import ModuleSchema from schemas.module import *
from services.mixins 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 schema_class = ModuleSchema
entity_updated_msg = "Модуль успешно обновлен"
entity_deleted_msg = "Модуль успешно удален"
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.repository = ModuleRepository(session) 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): def __init__(self, name: str):
self.name = name self.name = name
class ObjectNotFoundException(BaseResponseException):
pass
class ForbiddenException(BaseResponseException):
pass