feat: deal attributes with select and options
This commit is contained in:
@ -1,6 +1,10 @@
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
|
||||
from modules.fulfillment_base.models import * # noqa: F401
|
||||
from .attr_select import (
|
||||
AttributeOption as AttributeOption,
|
||||
AttributeSelect as AttributeSelect,
|
||||
)
|
||||
from .attribute import (
|
||||
AttributeType as AttributeType,
|
||||
Attribute as Attribute,
|
||||
|
||||
46
models/attr_select.py
Normal file
46
models/attr_select.py
Normal file
@ -0,0 +1,46 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, UniqueConstraint
|
||||
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 Attribute
|
||||
|
||||
|
||||
class AttributeSelect(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attribute_selects"
|
||||
|
||||
label: Mapped[str] = mapped_column()
|
||||
is_built_in: Mapped[bool] = mapped_column(
|
||||
default=False,
|
||||
comment="Если встроенный select, то запрещено редактировать пользователю",
|
||||
)
|
||||
|
||||
options: Mapped[list["AttributeOption"]] = relationship(
|
||||
back_populates="select",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
attributes: Mapped[list["Attribute"]] = relationship(
|
||||
back_populates="select",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
|
||||
class AttributeOption(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attribute_options"
|
||||
|
||||
value: Mapped[dict[str, any]] = mapped_column(JSONB)
|
||||
label: Mapped[str] = mapped_column()
|
||||
|
||||
select_id: Mapped[int] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||
select: Mapped[AttributeSelect] = relationship(
|
||||
back_populates="options",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
__table_args__ = (UniqueConstraint("value", "select_id", name="_value_select_uc"),)
|
||||
@ -8,8 +8,7 @@ from models.base import BaseModel
|
||||
from models.mixins import IdMixin, SoftDeleteMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Module, Deal
|
||||
|
||||
from models import Module, Deal, AttributeSelect
|
||||
|
||||
module_attribute = Table(
|
||||
"module_attribute",
|
||||
@ -51,6 +50,12 @@ class Attribute(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
select_id: Mapped[Optional[int]] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||
select: Mapped[Optional["AttributeSelect"]] = relationship(
|
||||
back_populates="attributes",
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
modules: Mapped[list["Module"]] = relationship(
|
||||
secondary=module_attribute,
|
||||
back_populates="attributes",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from .attr_select import AttrSelectRepository as AttrSelectRepository
|
||||
from .attribute import AttributeRepository as AttributeRepository
|
||||
from .board import BoardRepository as BoardRepository
|
||||
from .deal import DealRepository as DealRepository
|
||||
|
||||
19
repositories/attr_select.py
Normal file
19
repositories/attr_select.py
Normal file
@ -0,0 +1,19 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeSelect, AttributeOption
|
||||
from repositories.base import BaseRepository
|
||||
from repositories.mixins import RepGetAllMixin
|
||||
|
||||
|
||||
class AttrSelectRepository(BaseRepository, RepGetAllMixin[AttributeSelect]):
|
||||
session: AsyncSession
|
||||
entity_class = AttributeSelect
|
||||
|
||||
async def get_options(self, select_id: int) -> list[AttributeOption]:
|
||||
stmt = select(AttributeOption).where(
|
||||
AttributeOption.select_id == select_id,
|
||||
AttributeOption.is_deleted.is_(False),
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
@ -31,29 +31,24 @@ class AttributeRepository(
|
||||
return (
|
||||
stmt.options(joinedload(Attribute.type))
|
||||
.where(Attribute.is_deleted.is_(False))
|
||||
.order_by(Attribute.id)
|
||||
.order_by(Attribute.is_built_in.desc(), 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)
|
||||
return await self._apply_update_data_to_model(
|
||||
attr, data, with_commit=True, set_if_value_is_not_none=False
|
||||
)
|
||||
|
||||
async def _before_delete(self, attribute: Attribute) -> None:
|
||||
if attribute.is_built_in:
|
||||
raise ForbiddenException("Нельзя менять встроенный атрибут")
|
||||
|
||||
async def _get_all_attributes_for_deal(self, project_id) -> list[tuple[Attribute, int]]:
|
||||
async def _get_all_attributes_for_deal(
|
||||
self, project_id
|
||||
) -> list[tuple[Attribute, int]]:
|
||||
stmt = (
|
||||
select(Attribute, Module.id)
|
||||
.join(Attribute.modules)
|
||||
@ -148,9 +143,8 @@ class AttributeRepository(
|
||||
AttributeLabel.module_id == module_id,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
Attribute.is_deleted.is_(False),
|
||||
)
|
||||
.where(Attribute.is_deleted.is_(False))
|
||||
.options(joinedload(Attribute.select))
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.all())
|
||||
|
||||
@ -65,13 +65,14 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
|
||||
data: UpdateSchemaType,
|
||||
with_commit: Optional[bool] = False,
|
||||
fields: Optional[list[str]] = None,
|
||||
set_if_value_is_not_none: Optional[bool] = True,
|
||||
) -> EntityType:
|
||||
if fields is None:
|
||||
fields = data.model_dump().keys()
|
||||
|
||||
for field in fields:
|
||||
value = getattr(data, field)
|
||||
if value is not None:
|
||||
if not set_if_value_is_not_none or value is not None:
|
||||
setattr(model, field, value)
|
||||
|
||||
if with_commit:
|
||||
|
||||
30
routers/crm/v1/attr_select.py
Normal file
30
routers/crm/v1/attr_select.py
Normal file
@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from backend.dependecies import SessionDependency
|
||||
from schemas.attr_select import *
|
||||
from services import AttrSelectService
|
||||
|
||||
router = APIRouter(tags=["attr_select"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=GetAllAttrSelectsResponse,
|
||||
operation_id="get_attr_selects",
|
||||
)
|
||||
async def get_attr_selects(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await AttrSelectService(session).get_all()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{selectId}",
|
||||
response_model=GetAllAttrSelectOptionsResponse,
|
||||
operation_id="get_attr_select_options",
|
||||
)
|
||||
async def get_attr_select_options(
|
||||
session: SessionDependency,
|
||||
select_id: int = Path(alias="selectId"),
|
||||
):
|
||||
return await AttrSelectService(session).get_options(select_id)
|
||||
43
schemas/attr_select.py
Normal file
43
schemas/attr_select.py
Normal file
@ -0,0 +1,43 @@
|
||||
from typing import Any
|
||||
|
||||
from schemas.base import BaseSchema
|
||||
|
||||
|
||||
# region Entity
|
||||
|
||||
|
||||
class AttrSelectSchema(BaseSchema):
|
||||
id: int
|
||||
label: str
|
||||
is_built_in: bool
|
||||
|
||||
|
||||
class AttrOptionSchema(BaseSchema):
|
||||
id: int
|
||||
label: str
|
||||
value: Any
|
||||
|
||||
|
||||
class AttrSelectWithOptionsSchema(AttrSelectSchema):
|
||||
options: list[AttrOptionSchema]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Request
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response
|
||||
|
||||
|
||||
class GetAllAttrSelectsResponse(BaseSchema):
|
||||
items: list[AttrSelectSchema]
|
||||
|
||||
|
||||
class GetAllAttrSelectOptionsResponse(BaseSchema):
|
||||
items: list[AttrOptionSchema]
|
||||
|
||||
|
||||
# endregion
|
||||
@ -12,6 +12,11 @@ class AttributeTypeSchema(BaseSchema):
|
||||
name: str
|
||||
|
||||
|
||||
class AttributeSelectSchema(BaseSchema):
|
||||
id: int
|
||||
label: str
|
||||
|
||||
|
||||
class CreateAttributeSchema(BaseSchema):
|
||||
label: str
|
||||
is_applicable_to_group: bool
|
||||
@ -19,12 +24,14 @@ class CreateAttributeSchema(BaseSchema):
|
||||
default_value: Optional[Any]
|
||||
description: str
|
||||
type_id: int
|
||||
select_id: Optional[int]
|
||||
|
||||
|
||||
class AttributeSchema(CreateAttributeSchema):
|
||||
id: int
|
||||
is_built_in: bool
|
||||
type: AttributeTypeSchema
|
||||
select: Optional[AttributeSelectSchema]
|
||||
|
||||
|
||||
class UpdateAttributeSchema(BaseSchema):
|
||||
@ -33,7 +40,6 @@ class UpdateAttributeSchema(BaseSchema):
|
||||
is_nullable: Optional[bool] = None
|
||||
default_value: Optional[Any] = None
|
||||
description: Optional[str] = None
|
||||
type: Optional[AttributeTypeSchema] = None
|
||||
|
||||
|
||||
class ModuleAttributeSchema(AttributeSchema):
|
||||
@ -46,6 +52,7 @@ class DealModuleAttributeSchema(BaseSchema):
|
||||
original_label: str
|
||||
value: Optional[Any]
|
||||
type: AttributeTypeSchema
|
||||
select: Optional[AttributeSelectSchema]
|
||||
default_value: Any
|
||||
description: str
|
||||
is_applicable_to_group: bool
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from .attr_select import AttrSelectService as AttrSelectService
|
||||
from .attribute import AttributeService as AttributeService
|
||||
from .board import BoardService as BoardService
|
||||
from .deal import DealService as DealService
|
||||
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
|
||||
from .project import ProjectService as ProjectService
|
||||
from .status import StatusService as StatusService
|
||||
|
||||
24
services/attr_select.py
Normal file
24
services/attr_select.py
Normal file
@ -0,0 +1,24 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeSelect
|
||||
from repositories import AttrSelectRepository
|
||||
from schemas.attr_select import (
|
||||
AttrSelectSchema,
|
||||
GetAllAttrSelectOptionsResponse,
|
||||
AttrOptionSchema,
|
||||
)
|
||||
from services.mixins import ServiceGetAllMixin
|
||||
|
||||
|
||||
class AttrSelectService(ServiceGetAllMixin[AttributeSelect, AttrSelectSchema]):
|
||||
schema_class = AttrSelectSchema
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = AttrSelectRepository(session)
|
||||
|
||||
async def get_options(self, select_id: int) -> GetAllAttrSelectOptionsResponse:
|
||||
options = await self.repository.get_options(select_id)
|
||||
|
||||
return GetAllAttrSelectOptionsResponse(
|
||||
items=[AttrOptionSchema.model_validate(option) for option in options]
|
||||
)
|
||||
@ -39,12 +39,19 @@ class AttributeService(
|
||||
|
||||
attributes = []
|
||||
for attr, attr_value, attr_label in deal_attributes:
|
||||
select_schema = (
|
||||
AttributeSelectSchema.model_validate(attr.select)
|
||||
if attr.select
|
||||
else None
|
||||
)
|
||||
|
||||
attribute = DealModuleAttributeSchema(
|
||||
attribute_id=attr.id,
|
||||
label=attr_label.label if attr_label else attr.label,
|
||||
original_label=attr.label,
|
||||
value=attr_value.value if attr_value else None,
|
||||
type=AttributeTypeSchema.model_validate(attr.type),
|
||||
select=select_schema,
|
||||
default_value=attr.default_value,
|
||||
description=attr.description,
|
||||
is_applicable_to_group=attr.is_applicable_to_group,
|
||||
|
||||
@ -31,8 +31,9 @@ class ModuleService(
|
||||
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
|
||||
return GetByIdWithAttributesResponse(entity=module)
|
||||
|
||||
@staticmethod
|
||||
def _build_modules_with_attributes(
|
||||
self, result: list[tuple]
|
||||
result: list[tuple],
|
||||
) -> dict[int, ModuleWithAttributesSchema]:
|
||||
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user