feat: deal attributes with select and options
This commit is contained in:
@ -1,6 +1,10 @@
|
|||||||
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 .attr_select import (
|
||||||
|
AttributeOption as AttributeOption,
|
||||||
|
AttributeSelect as AttributeSelect,
|
||||||
|
)
|
||||||
from .attribute import (
|
from .attribute import (
|
||||||
AttributeType as AttributeType,
|
AttributeType as AttributeType,
|
||||||
Attribute as Attribute,
|
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
|
from models.mixins import IdMixin, SoftDeleteMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import Module, Deal
|
from models import Module, Deal, AttributeSelect
|
||||||
|
|
||||||
|
|
||||||
module_attribute = Table(
|
module_attribute = Table(
|
||||||
"module_attribute",
|
"module_attribute",
|
||||||
@ -51,6 +50,12 @@ class Attribute(BaseModel, IdMixin, SoftDeleteMixin):
|
|||||||
lazy="joined",
|
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(
|
modules: Mapped[list["Module"]] = relationship(
|
||||||
secondary=module_attribute,
|
secondary=module_attribute,
|
||||||
back_populates="attributes",
|
back_populates="attributes",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from .attr_select import AttrSelectRepository as AttrSelectRepository
|
||||||
from .attribute import AttributeRepository as AttributeRepository
|
from .attribute import AttributeRepository as AttributeRepository
|
||||||
from .board import BoardRepository as BoardRepository
|
from .board import BoardRepository as BoardRepository
|
||||||
from .deal import DealRepository as DealRepository
|
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 (
|
return (
|
||||||
stmt.options(joinedload(Attribute.type))
|
stmt.options(joinedload(Attribute.type))
|
||||||
.where(Attribute.is_deleted.is_(False))
|
.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:
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.options(joinedload(Attribute.type))
|
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:
|
async def update(self, attr: Attribute, data: UpdateAttributeSchema) -> Attribute:
|
||||||
if data.type:
|
return await self._apply_update_data_to_model(
|
||||||
data.type = await self._get_attribute_type_by_id(data.type.id)
|
attr, data, with_commit=True, set_if_value_is_not_none=False
|
||||||
return await self._apply_update_data_to_model(attr, data, True)
|
)
|
||||||
|
|
||||||
async def _before_delete(self, attribute: Attribute) -> None:
|
async def _before_delete(self, attribute: Attribute) -> None:
|
||||||
if attribute.is_built_in:
|
if attribute.is_built_in:
|
||||||
raise ForbiddenException("Нельзя менять встроенный атрибут")
|
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 = (
|
stmt = (
|
||||||
select(Attribute, Module.id)
|
select(Attribute, Module.id)
|
||||||
.join(Attribute.modules)
|
.join(Attribute.modules)
|
||||||
@ -148,9 +143,8 @@ class AttributeRepository(
|
|||||||
AttributeLabel.module_id == module_id,
|
AttributeLabel.module_id == module_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where(
|
.where(Attribute.is_deleted.is_(False))
|
||||||
Attribute.is_deleted.is_(False),
|
.options(joinedload(Attribute.select))
|
||||||
)
|
|
||||||
)
|
)
|
||||||
result = await self.session.execute(stmt)
|
result = await self.session.execute(stmt)
|
||||||
return list(result.all())
|
return list(result.all())
|
||||||
|
|||||||
@ -65,13 +65,14 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
|
|||||||
data: UpdateSchemaType,
|
data: UpdateSchemaType,
|
||||||
with_commit: Optional[bool] = False,
|
with_commit: Optional[bool] = False,
|
||||||
fields: Optional[list[str]] = None,
|
fields: Optional[list[str]] = None,
|
||||||
|
set_if_value_is_not_none: Optional[bool] = True,
|
||||||
) -> EntityType:
|
) -> EntityType:
|
||||||
if fields is None:
|
if fields is None:
|
||||||
fields = data.model_dump().keys()
|
fields = data.model_dump().keys()
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
value = getattr(data, field)
|
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)
|
setattr(model, field, value)
|
||||||
|
|
||||||
if with_commit:
|
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
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeSelectSchema(BaseSchema):
|
||||||
|
id: int
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
class CreateAttributeSchema(BaseSchema):
|
class CreateAttributeSchema(BaseSchema):
|
||||||
label: str
|
label: str
|
||||||
is_applicable_to_group: bool
|
is_applicable_to_group: bool
|
||||||
@ -19,12 +24,14 @@ class CreateAttributeSchema(BaseSchema):
|
|||||||
default_value: Optional[Any]
|
default_value: Optional[Any]
|
||||||
description: str
|
description: str
|
||||||
type_id: int
|
type_id: int
|
||||||
|
select_id: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class AttributeSchema(CreateAttributeSchema):
|
class AttributeSchema(CreateAttributeSchema):
|
||||||
id: int
|
id: int
|
||||||
is_built_in: bool
|
is_built_in: bool
|
||||||
type: AttributeTypeSchema
|
type: AttributeTypeSchema
|
||||||
|
select: Optional[AttributeSelectSchema]
|
||||||
|
|
||||||
|
|
||||||
class UpdateAttributeSchema(BaseSchema):
|
class UpdateAttributeSchema(BaseSchema):
|
||||||
@ -33,7 +40,6 @@ class UpdateAttributeSchema(BaseSchema):
|
|||||||
is_nullable: Optional[bool] = None
|
is_nullable: Optional[bool] = None
|
||||||
default_value: Optional[Any] = None
|
default_value: Optional[Any] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
type: Optional[AttributeTypeSchema] = None
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleAttributeSchema(AttributeSchema):
|
class ModuleAttributeSchema(AttributeSchema):
|
||||||
@ -46,6 +52,7 @@ class DealModuleAttributeSchema(BaseSchema):
|
|||||||
original_label: str
|
original_label: str
|
||||||
value: Optional[Any]
|
value: Optional[Any]
|
||||||
type: AttributeTypeSchema
|
type: AttributeTypeSchema
|
||||||
|
select: Optional[AttributeSelectSchema]
|
||||||
default_value: Any
|
default_value: Any
|
||||||
description: str
|
description: str
|
||||||
is_applicable_to_group: bool
|
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 .board import BoardService as BoardService
|
||||||
from .deal import DealService as DealService
|
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_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
|
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 = []
|
attributes = []
|
||||||
for attr, attr_value, attr_label in deal_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 = DealModuleAttributeSchema(
|
||||||
attribute_id=attr.id,
|
attribute_id=attr.id,
|
||||||
label=attr_label.label if attr_label else attr.label,
|
label=attr_label.label if attr_label else attr.label,
|
||||||
original_label=attr.label,
|
original_label=attr.label,
|
||||||
value=attr_value.value if attr_value else None,
|
value=attr_value.value if attr_value else None,
|
||||||
type=AttributeTypeSchema.model_validate(attr.type),
|
type=AttributeTypeSchema.model_validate(attr.type),
|
||||||
|
select=select_schema,
|
||||||
default_value=attr.default_value,
|
default_value=attr.default_value,
|
||||||
description=attr.description,
|
description=attr.description,
|
||||||
is_applicable_to_group=attr.is_applicable_to_group,
|
is_applicable_to_group=attr.is_applicable_to_group,
|
||||||
|
|||||||
@ -31,8 +31,9 @@ class ModuleService(
|
|||||||
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
|
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
|
||||||
return GetByIdWithAttributesResponse(entity=module)
|
return GetByIdWithAttributesResponse(entity=module)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _build_modules_with_attributes(
|
def _build_modules_with_attributes(
|
||||||
self, result: list[tuple]
|
result: list[tuple],
|
||||||
) -> dict[int, ModuleWithAttributesSchema]:
|
) -> dict[int, ModuleWithAttributesSchema]:
|
||||||
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
|
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user