feat: modules, products, services, services kits

This commit is contained in:
2025-09-16 10:54:10 +04:00
parent be8052848c
commit 276626d6f7
55 changed files with 1791 additions and 34 deletions

View File

@ -1,18 +1,19 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import ORJSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
from fastapi_endpoints import auto_include_routers
import modules
import routers
from utils.auto_include_routers import auto_include_routers
origins = ["http://localhost:3000"]
app = FastAPI(
separate_input_output_schemas=True,
default_response_class=ORJSONResponse,
root_path="/api"
root_path="/api",
)
app.add_middleware(
@ -28,5 +29,6 @@ app.add_middleware(
)
auto_include_routers(app, routers)
auto_include_routers(app, modules, True)
app.mount("/static", StaticFiles(directory="static"), name="static")

View File

@ -1,7 +1,13 @@
from sqlalchemy.orm import configure_mappers
from modules.fulfillment_base.models import * # noqa: F401
from .base import BaseModel as BaseModel
from .board import Board as Board
from .built_in_module import ( # noqa: F401
BuiltInModule as BuiltInModule,
project_built_in_module as project_built_in_module,
built_in_module_dependencies as built_in_module_dependencies,
)
from .deal import Deal as Deal
from .project import Project as Project
from .status import Status as Status, CardStatusHistory as CardStatusHistory

58
models/built_in_module.py Normal file
View File

@ -0,0 +1,58 @@
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
if TYPE_CHECKING:
from models import Project
project_built_in_module = Table(
"project_built_in_module",
BaseModel.metadata,
Column("project_id", ForeignKey("projects.id"), primary_key=True),
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
built_in_module_dependencies = Table(
"built_in_module_dependencies",
BaseModel.metadata,
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
Column("depends_on_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
class BuiltInModule(BaseModel):
__tablename__ = "built_in_modules"
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()
icon_name: Mapped[Optional[str]] = mapped_column(unique=True)
is_deleted: Mapped[bool] = mapped_column(default=False)
depends_on: Mapped[list["BuiltInModule"]] = relationship(
secondary=built_in_module_dependencies,
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
back_populates="depended_on_by",
lazy="immediate",
)
depended_on_by: Mapped[list["BuiltInModule"]] = relationship(
secondary="built_in_module_dependencies",
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
back_populates="depends_on",
lazy="noload",
)
projects: Mapped[list["Project"]] = relationship(
uselist=True,
secondary="project_built_in_module",
back_populates="built_in_modules",
lazy="noload",
)

View File

@ -1,6 +1,6 @@
from datetime import datetime, timezone
from sqlalchemy import DateTime
from sqlalchemy import DateTime, Numeric
from sqlalchemy.orm import Mapped, mapped_column
@ -21,3 +21,11 @@ class CreatedAtMixin:
default=lambda: datetime.now(timezone.utc),
nullable=False,
)
class PriceMixin:
price: Mapped[float] = mapped_column(Numeric(12, 2), comment="Стоимость")
class CostMixin:
cost: Mapped[float] = mapped_column(Numeric(12, 2), comment="Себестоимость")

View File

@ -6,7 +6,7 @@ from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING:
from models import Board
from models import Board, BuiltInModule
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
@ -18,3 +18,10 @@ class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
back_populates="project",
lazy="noload",
)
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
secondary="project_built_in_module",
back_populates="projects",
lazy="selectin",
order_by="asc(BuiltInModule.id)",
)

0
modules/__init__.py Normal file
View File

View File

View File

@ -0,0 +1 @@
from .service import *

View File

@ -0,0 +1,7 @@
from enum import IntEnum, unique
@unique
class ServiceType(IntEnum):
DEAL_SERVICE = 0
PRODUCT_SERVICE = 1

View File

@ -0,0 +1,6 @@
from .deal_product import DealProduct as DealProduct, DealProductService as DealProductService
from .deal_service import (
DealService as DealService,
)
from .product import Product as Product
from .service import Service as Service

View File

@ -0,0 +1,60 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, ForeignKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal, Service, Product
class DealProduct(BaseModel):
__tablename__ = "fulfillment_base_deal_products"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
product_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_products.id"), primary_key=True
)
quantity: Mapped[int] = mapped_column(default=1)
comment: Mapped[str] = mapped_column(comment="Комментарий к товару")
deal: Mapped["Deal"] = relationship(backref="deal_products")
product: Mapped["Product"] = relationship()
product_services: Mapped[list["DealProductService"]] = relationship(
back_populates="deal_product",
primaryjoin="and_(DealProduct.deal_id==DealProductService.deal_id, DealProduct.product_id==DealProductService.product_id)",
)
class DealProductService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_products_services"
deal_id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(primary_key=True)
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)
deal_product: Mapped["DealProduct"] = relationship(
back_populates="product_services",
primaryjoin="and_(DealProductService.deal_id==DealProduct.deal_id, DealProductService.product_id==DealProduct.product_id)",
)
service: Mapped["Service"] = relationship()
__table_args__ = (
ForeignKeyConstraint(
["deal_id", "product_id"],
[
"fulfillment_base_deal_products.deal_id",
"fulfillment_base_deal_products.product_id",
],
),
)

View File

@ -0,0 +1,28 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal
from modules.fulfillment_base.models import Service
class DealService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_services"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
deal: Mapped["Deal"] = relationship(backref="deal_services")
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
service: Mapped["Service"] = relationship()
quantity: Mapped[int] = mapped_column(default=1)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)

View File

@ -0,0 +1,40 @@
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
class Product(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "fulfillment_base_products"
name: Mapped[str] = mapped_column()
article: Mapped[str] = mapped_column(index=True)
factory_article: Mapped[str] = mapped_column(
index=True, default="", server_default=""
)
brand: Mapped[Optional[str]] = mapped_column(comment="Бренд")
color: Mapped[Optional[str]] = mapped_column(comment="Цвет")
composition: Mapped[Optional[str]] = mapped_column(comment="Состав")
size: Mapped[Optional[str]] = mapped_column(comment="Размер")
additional_info: Mapped[Optional[str]] = mapped_column(
comment="Дополнительная информация"
)
images: Mapped[list["ProductImage"]] = relationship(
"ProductImage",
back_populates="product",
lazy="selectin",
cascade="all, delete-orphan",
)
class ProductImage(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_product_images"
product_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_products.id"))
product: Mapped["Product"] = relationship(back_populates="images")
image_url: Mapped[str] = mapped_column(nullable=False)

View File

@ -0,0 +1,73 @@
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin, CostMixin, PriceMixin
from modules.fulfillment_base import enums
services_kit_services = Table(
"fulfillment_base_services_kit_services",
BaseModel.metadata,
Column("services_kit_id", ForeignKey("fulfillment_base_services_kits.id")),
Column("service_id", ForeignKey("fulfillment_base_services.id")),
)
class Service(BaseModel, IdMixin, SoftDeleteMixin, PriceMixin, CostMixin):
__tablename__ = "fulfillment_base_services"
name: Mapped[str] = mapped_column(index=True)
price_ranges: Mapped[list["ServicePriceRange"]] = relationship(
back_populates="service",
lazy="selectin",
order_by="asc(ServicePriceRange.from_quantity)",
cascade="all, delete-orphan",
)
category_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_service_categories.id"),
comment="ID категории услуги",
)
category: Mapped["ServiceCategory"] = relationship("ServiceCategory", lazy="joined")
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.service.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
lexorank: Mapped[str] = mapped_column(comment="Ранг услуги")
class ServiceCategory(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_service_categories"
name: Mapped[str] = mapped_column()
is_deleted: Mapped[bool] = mapped_column(
default=False, comment="Удалена ли категория"
)
deal_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для сделки")
product_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для товара")
class ServicesKit(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_services_kits"
name: Mapped[str] = mapped_column()
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
services: Mapped[list["Service"]] = relationship(
secondary=services_kit_services, lazy="selectin"
)
class ServicePriceRange(BaseModel, IdMixin, PriceMixin):
__tablename__ = "fulfillment_base_service_price_ranges"
service_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_services.id"))
service: Mapped[Service] = relationship(back_populates="price_ranges")
from_quantity: Mapped[int] = mapped_column(comment="От количества")
to_quantity: Mapped[int] = mapped_column(comment="До количества")

View File

@ -0,0 +1,6 @@
from .deal_product import DealProductRepository as DealProductRepository
from .product_service import ProductServiceRepository as ProductServiceRepository
from .deal_service import DealServiceRepository as DealServiceRepository
from .product import ProductRepository as ProductRepository
from .service import ServiceRepository as ServiceRepository
from .services_kit import ServicesKitRepository as ServicesKitRepository

View File

@ -0,0 +1,71 @@
from typing import Optional
from sqlalchemy import Select, select
from sqlalchemy.orm import joinedload, selectinload
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.models.deal_product import DealProduct
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.deal_product import (
UpdateDealProductSchema,
CreateDealProductSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
class DealProductRepository(
BaseRepository,
RepGetAllMixin[DealProduct],
RepUpdateMixin[DealProduct, UpdateDealProductSchema],
):
entity_class = DealProduct
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealProduct.product),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id)
.order_by(DealProduct.product_id)
)
async def get_by_id(self, deal_id: int, product_id: int) -> Optional[DealProduct]:
stmt = (
select(DealProduct)
.options(
joinedload(DealProduct.product),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateDealProductSchema):
deal_product = DealProduct(**data.model_dump())
self.session.add(deal_product)
await self.session.commit()
async def delete(self, obj: DealProduct):
await self.session.delete(obj)
await self.session.commit()
async def add_services_kit(
self, deal_product: DealProduct, services_kit: ServicesKit
):
for service in services_kit.services:
deal_product_service = DealProductService(
deal_id=deal_product.deal_id,
product_id=deal_product.product_id,
service_id=service.id,
price=service.price,
)
self.session.add(deal_product_service)
await self.session.commit()

View File

@ -0,0 +1,48 @@
from typing import Optional
from sqlalchemy import Select, select
from sqlalchemy.orm import joinedload
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.schemas.deal_service import (
UpdateDealServiceSchema,
CreateDealServiceSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
class DealServiceRepository(
BaseRepository,
RepGetAllMixin[DealService],
RepUpdateMixin[DealService, UpdateDealServiceSchema],
):
entity_class = DealService
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealService.service),
)
.where(DealService.deal_id == deal_id)
.order_by(DealService.service_id)
)
async def get_by_id(self, deal_id: int, service_id: int) -> Optional[DealService]:
stmt = (
select(DealService)
.options(joinedload(DealService.service))
.where(DealService.deal_id == deal_id, DealService.service_id == service_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateDealServiceSchema):
deal_service = DealService(**data.model_dump())
self.session.add(deal_service)
await self.session.commit()
async def delete(self, obj: DealService):
await self.session.delete(obj)
await self.session.commit()

View File

@ -0,0 +1,33 @@
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.schemas.product import (
CreateProductSchema,
UpdateProductSchema,
)
from repositories.mixins import *
from schemas.base import PaginationSchema
class ProductRepository(
BaseRepository,
RepGetAllMixin[Product],
RepDeleteMixin[Product],
RepCreateMixin[Product, CreateProductSchema],
RepUpdateMixin[Product, UpdateProductSchema],
RepGetByIdMixin[Product],
):
entity_class = Product
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
search_input = args[0]
pagination: PaginationSchema = args[1]
if search_input:
stmt = stmt.where(Product.name.ilike(f"%{search_input}%"))
if pagination.items_per_page and pagination.page:
stmt = self._apply_pagination(
stmt, pagination.page, pagination.items_per_page
)
return stmt
async def update(self, product: Product, data: UpdateProductSchema) -> Product:
return await self._apply_update_data_to_model(product, data, True)

View File

@ -0,0 +1,84 @@
from typing import Optional
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.schemas.product_service import *
from repositories.base import BaseRepository
from repositories.mixins import RepUpdateMixin
class ProductServiceRepository(
BaseRepository,
RepUpdateMixin[DealProductService, UpdateProductServiceSchema],
):
entity_class = DealProductService
session: AsyncSession
async def get_by_id(
self, deal_id: int, product_id: int, service_id: int
) -> Optional[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
DealProductService.service_id == service_id,
)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateProductServiceSchema):
deal_product_service = DealProductService(**data.model_dump())
self.session.add(deal_product_service)
await self.session.commit()
async def delete(self, obj: DealProductService):
await self.session.delete(obj)
await self.session.commit()
async def get_product_services(
self, deal_id: int, product_id: int
) -> list[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
)
)
return list(await self.session.scalars(stmt))
async def delete_product_services(self, deal_id: int, product_ids: list[int]):
stmt = delete(DealProductService).where(
DealProductService.deal_id == deal_id,
DealProductService.product_id.in_(product_ids),
)
await self.session.execute(stmt)
await self.session.flush()
async def duplicate_services(
self, deal_id: int, product_ids: list[int], services: list[DealProductService]
):
await self.delete_product_services(deal_id, product_ids)
for product_id in product_ids:
for prod_service in services:
product_service = DealProductService(
deal_id=deal_id,
product_id=product_id,
service_id=prod_service.service.id,
price=prod_service.price,
is_fixed_price=prod_service.is_fixed_price,
)
self.session.add(product_service)
await self.session.commit()

View File

@ -0,0 +1,20 @@
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.schemas.service import (
CreateServiceSchema,
UpdateServiceSchema,
)
from repositories.mixins import *
class ServiceRepository(
BaseRepository,
RepGetAllMixin[Service],
RepDeleteMixin[Service],
RepCreateMixin[Service, CreateServiceSchema],
RepUpdateMixin[Service, UpdateServiceSchema],
RepGetByIdMixin[Service],
):
entity_class = Service
async def update(self, service: Service, data: UpdateServiceSchema) -> Service:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -0,0 +1,22 @@
from sqlalchemy.orm import selectinload
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.services_kit import (
CreateServicesKitSchema,
UpdateServicesKitSchema,
)
from repositories.mixins import *
class ServicesKitRepository(
RepCrudMixin[ServicesKit, CreateServicesKitSchema, UpdateServicesKitSchema],
):
entity_class = ServicesKit
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(ServicesKit.services))
async def update(
self, service: ServicesKit, data: UpdateServicesKitSchema
) -> ServicesKit:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -0,0 +1,137 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_product import *
from modules.fulfillment_base.schemas.product_service import *
from modules.fulfillment_base.services import DealProductService, ProductServiceService
router = APIRouter(tags=["deal-product"])
# region DealProduct
@router.get(
"/{dealId}",
response_model=GetDealProductsResponse,
operation_id="get_deal_products",
)
async def get_deal_products(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealProductService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealProductResponse,
operation_id="create_deal_product",
)
async def create_deal_product(
session: SessionDependency,
request: CreateDealProductRequest,
):
return await DealProductService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}",
response_model=UpdateDealProductResponse,
operation_id="update_deal_product",
)
async def update_deal_product(
session: SessionDependency,
request: UpdateDealProductRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).update(deal_id, product_id, request)
@router.delete(
"/{dealId}/product/{productId}",
response_model=DeleteDealProductResponse,
operation_id="delete_deal_product",
)
async def delete_deal_product(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).delete(deal_id, product_id)
@router.post(
"/add-services-kit",
response_model=DealProductAddKitResponse,
operation_id="add_kit_to_deal_product",
)
async def add_kit_to_deal_product(
session: SessionDependency,
request: DealProductAddKitRequest,
):
return await DealProductService(session).add_services_kit(request)
# endregion
# region DealProductService
@router.post(
"/service",
response_model=CreateProductServiceResponse,
operation_id="create_deal_product_service",
)
async def create_deal_product_service(
session: SessionDependency,
request: CreateProductServiceRequest,
):
return await ProductServiceService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=UpdateProductServiceResponse,
operation_id="update_deal_product_service",
)
async def update_deal_product_service(
session: SessionDependency,
request: UpdateProductServiceRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).update(
deal_id, product_id, service_id, request
)
@router.delete(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=DeleteProductServiceResponse,
operation_id="delete_deal_product_service",
)
async def delete_deal_product_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).delete(deal_id, product_id, service_id)
@router.post(
"/services/duplicate",
response_model=ProductServicesDuplicateResponse,
operation_id="duplicate_product_services",
)
async def copy_product_services(
session: SessionDependency,
request: ProductServicesDuplicateRequest,
):
return await ProductServiceService(session).duplicate_product_services(request)
# endregion

View File

@ -0,0 +1,58 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_service import *
from modules.fulfillment_base.services import DealServiceService
router = APIRouter(tags=["deal-service"])
@router.get(
"/{dealId}",
response_model=GetDealServicesResponse,
operation_id="get_deal_services",
)
async def get_deal_services(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealServiceService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealServiceResponse,
operation_id="create_deal_service",
)
async def create_deal_service(
session: SessionDependency,
request: CreateDealServiceRequest,
):
return await DealServiceService(session).create(request)
@router.patch(
"/{dealId}/service/{serviceId}",
response_model=UpdateDealServiceResponse,
operation_id="update_deal_service",
)
async def update_deal_service(
session: SessionDependency,
request: UpdateDealServiceRequest,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).update(deal_id, service_id, request)
@router.delete(
"/{dealId}/service/{serviceId}",
response_model=DeleteDealServiceResponse,
operation_id="delete_deal_service",
)
async def delete_deal_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).delete(deal_id, service_id)

View File

@ -0,0 +1,57 @@
from fastapi import APIRouter, Path, Query
from backend.dependecies import SessionDependency, PaginationDependency
from modules.fulfillment_base.schemas.product import *
from modules.fulfillment_base.services import ProductService
router = APIRouter(tags=["product"])
@router.get(
"/",
response_model=GetProductsResponse,
operation_id="get_products",
)
async def get_products(
session: SessionDependency,
pagination: PaginationDependency,
search_input: Optional[str] = Query(alias="searchInput", default=None),
):
return await ProductService(session).get_all(search_input, pagination)
@router.post(
"/",
response_model=CreateProductResponse,
operation_id="create_product",
)
async def create_product(
session: SessionDependency,
request: CreateProductRequest,
):
return await ProductService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateProductResponse,
operation_id="update_product",
)
async def update_product(
session: SessionDependency,
request: UpdateProductRequest,
pk: int = Path(),
):
return await ProductService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteProductResponse,
operation_id="delete_product",
)
async def delete_product(
session: SessionDependency,
pk: int = Path(),
):
return await ProductService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.service import *
from modules.fulfillment_base.services import ServiceModelService
router = APIRouter(tags=["service"])
@router.get(
"/",
response_model=GetServicesResponse,
operation_id="get_services",
)
async def get_services(
session: SessionDependency,
):
return await ServiceModelService(session).get_all()
@router.post(
"/",
response_model=CreateServiceResponse,
operation_id="create_service",
)
async def create_service(
session: SessionDependency,
request: CreateServiceRequest,
):
return await ServiceModelService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServiceResponse,
operation_id="update_service",
)
async def update_service(
session: SessionDependency,
request: UpdateServiceRequest,
pk: int = Path(),
):
return await ServiceModelService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServiceResponse,
operation_id="delete_service",
)
async def delete_service(
session: SessionDependency,
pk: int = Path(),
):
return await ServiceModelService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.services_kit import *
from modules.fulfillment_base.services import ServicesKitService
router = APIRouter(tags=["services-kit"])
@router.get(
"/",
response_model=GetServicesKitResponse,
operation_id="get_services_kits",
)
async def get_services_kits(
session: SessionDependency,
):
return await ServicesKitService(session).get_all()
@router.post(
"/",
response_model=CreateServicesKitResponse,
operation_id="create_services_kit",
)
async def create_services_kit(
session: SessionDependency,
request: CreateServicesKitRequest,
):
return await ServicesKitService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServicesKitResponse,
operation_id="update_services_kit",
)
async def update_services_kit(
session: SessionDependency,
request: UpdateServicesKitRequest,
pk: int = Path(),
):
return await ServicesKitService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServicesKitResponse,
operation_id="delete_services_kit",
)
async def delete_services_kit(
session: SessionDependency,
pk: int = Path(),
):
return await ServicesKitService(session).delete(pk)

View File

@ -0,0 +1,74 @@
from modules.fulfillment_base.schemas.product import ProductSchema
from modules.fulfillment_base.schemas.product_service import ProductServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealProductSchema(BaseSchema):
deal_id: int
product_id: int
product: ProductSchema
quantity: int
comment: str
product_services: list[ProductServiceSchema]
class CreateDealProductSchema(BaseSchema):
deal_id: int
product_id: int
quantity: int
comment: str
class UpdateDealProductSchema(BaseSchema):
quantity: int
comment: str
# endregion
# region Request
class CreateDealProductRequest(BaseSchema):
entity: CreateDealProductSchema
class UpdateDealProductRequest(BaseSchema):
entity: UpdateDealProductSchema
class DealProductAddKitRequest(BaseSchema):
deal_id: int
product_id: int
kit_id: int
# endregion
# region Response
class GetDealProductsResponse(BaseSchema):
items: list[DealProductSchema]
class CreateDealProductResponse(BaseResponse):
entity: DealProductSchema
class UpdateDealProductResponse(BaseResponse):
pass
class DeleteDealProductResponse(BaseResponse):
pass
class DealProductAddKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,64 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealServiceSchema(BaseSchema):
deal_id: int
service_id: int
service: ServiceSchema
quantity: int
price: float
is_fixed_price: bool
class CreateDealServiceSchema(BaseSchema):
deal_id: int
service_id: int
quantity: int
price: float
class UpdateDealServiceSchema(BaseSchema):
quantity: int
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateDealServiceRequest(BaseSchema):
entity: CreateDealServiceSchema
class UpdateDealServiceRequest(BaseSchema):
entity: UpdateDealServiceSchema
# endregion
# region Response
class GetDealServicesResponse(BaseSchema):
items: list[DealServiceSchema]
class CreateDealServiceResponse(BaseResponse):
entity: DealServiceSchema
class UpdateDealServiceResponse(BaseResponse):
pass
class DeleteDealServiceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,77 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ProductImageSchema(BaseSchema):
id: int
product_id: int
image_url: str
class CreateProductSchema(BaseSchema):
name: str
article: str
factory_article: str
brand: Optional[str]
color: Optional[str]
composition: Optional[str]
size: Optional[str]
additional_info: Optional[str]
class ProductSchema(CreateProductSchema):
id: int
class UpdateProductSchema(BaseSchema):
name: Optional[str] = None
article: Optional[str] = None
factory_article: Optional[str] = None
brand: Optional[str] = None
color: Optional[str] = None
composition: Optional[str] = None
size: Optional[str] = None
additional_info: Optional[str] = None
images: list[ProductImageSchema] | None = []
# endregion
# region Request
class CreateProductRequest(BaseSchema):
entity: CreateProductSchema
class UpdateProductRequest(BaseSchema):
entity: UpdateProductSchema
# endregion
# region Response
class GetProductsResponse(BaseSchema):
items: list[ProductSchema]
class CreateProductResponse(BaseResponse):
entity: ProductSchema
class UpdateProductResponse(BaseResponse):
pass
class DeleteProductResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,69 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
service: ServiceSchema
price: float
is_fixed_price: bool
class CreateProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
price: float
class UpdateProductServiceSchema(BaseSchema):
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateProductServiceRequest(BaseSchema):
entity: CreateProductServiceSchema
class UpdateProductServiceRequest(BaseSchema):
entity: UpdateProductServiceSchema
class ProductServicesDuplicateRequest(BaseSchema):
deal_id: int
source_deal_product_id: int
target_deal_product_ids: list[int]
# endregion
# region Response
class CreateProductServiceResponse(BaseResponse):
entity: ProductServiceSchema
class UpdateProductServiceResponse(BaseResponse):
pass
class DeleteProductServiceResponse(BaseResponse):
pass
class ProductServicesDuplicateResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,76 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ServicePriceRangeSchema(BaseSchema):
id: int | None
from_quantity: int
to_quantity: int
price: float
class ServiceCategorySchema(BaseSchema):
id: int
name: str
deal_service_rank: str
product_service_rank: str
class ServiceSchema(BaseSchema):
id: int
name: str
category: ServiceCategorySchema
price: float
service_type: int
price_ranges: list[ServicePriceRangeSchema]
cost: Optional[float]
lexorank: str
class UpdateServiceSchema(ServiceSchema):
pass
class CreateServiceSchema(ServiceSchema):
pass
# endregion
# region Request
class CreateServiceRequest(BaseSchema):
entity: CreateServiceSchema
class UpdateServiceRequest(BaseSchema):
entity: UpdateServiceSchema
# endregion
# region Response
class GetServicesResponse(BaseSchema):
items: list[ServiceSchema]
class CreateServiceResponse(BaseResponse):
entity: ServiceSchema
class UpdateServiceResponse(BaseResponse):
pass
class DeleteServiceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,60 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class BaseServicesKitSchema(BaseSchema):
name: str
service_type: int
class ServicesKitSchema(BaseServicesKitSchema):
id: int
services: list[ServiceSchema]
class CreateServicesKitSchema(BaseServicesKitSchema):
services_ids: list[int]
class UpdateServicesKitSchema(BaseServicesKitSchema):
services_ids: list[int]
# endregion
# region Request
class CreateServicesKitRequest(BaseSchema):
entity: CreateServicesKitSchema
class UpdateServicesKitRequest(BaseSchema):
entity: UpdateServicesKitSchema
# endregion
# region Response
class GetServicesKitResponse(BaseSchema):
items: list[ServicesKitSchema]
class CreateServicesKitResponse(BaseResponse):
entity: ServicesKitSchema
class UpdateServicesKitResponse(BaseResponse):
pass
class DeleteServicesKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,6 @@
from .deal_product import DealProductService as DealProductService
from .deal_service import DealServiceService as DealServiceService
from .product import ProductService as ProductService
from .product_service import ProductServiceService as ProductServiceService
from .service import ServiceModelService as ServiceModelService
from .services_kit import ServicesKitService as ServicesKitService

View File

@ -0,0 +1,72 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProduct
from modules.fulfillment_base.repositories import (
DealProductRepository,
ServicesKitRepository,
ProductServiceRepository,
)
from modules.fulfillment_base.schemas.deal_product import *
from services.mixins import ServiceGetAllMixin
class DealProductService(ServiceGetAllMixin[DealProduct, DealProductSchema]):
schema_class = DealProductSchema
entity_not_found_msg = "Связь товара со сделкой не найдена"
def __init__(self, session: AsyncSession):
self.repository = DealProductRepository(session)
async def create(
self, request: CreateDealProductRequest
) -> CreateDealProductResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id, request.entity.product_id
)
return CreateDealProductResponse(
entity=DealProductSchema.model_validate(deal_product),
message="Товар добавлен в сделку",
)
async def update(
self, deal_id: int, product_id: int, data: UpdateDealProductRequest
) -> UpdateDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateDealProductResponse(message="Товар сделки обновлен")
async def delete(self, deal_id: int, product_id: int) -> DeleteDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteDealProductResponse(message="Товар удален из сделки")
async def add_services_kit(
self, request: DealProductAddKitRequest
) -> DealProductAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id)
if not services_kit:
raise HTTPException(status_code=404, detail="Набор услуг не найден")
deal_product = await self.repository.get_by_id(
request.deal_id, request.product_id
)
if not deal_product:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
product_service_repo = ProductServiceRepository(self.repository.session)
await product_service_repo.delete_product_services(
request.deal_id, [request.product_id]
)
await self.repository.add_services_kit(deal_product, services_kit)
return DealProductAddKitResponse(message="Комплект добавлен в товар")

View File

@ -0,0 +1,45 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.repositories import DealServiceRepository
from modules.fulfillment_base.schemas.deal_service import *
from services.mixins import ServiceGetAllMixin
class DealServiceService(ServiceGetAllMixin[DealService, DealServiceSchema]):
schema_class = DealServiceSchema
entity_not_found_msg = "Связь услуги со сделкой не найдена"
def __init__(self, session: AsyncSession):
self.repository = DealServiceRepository(session)
async def create(
self, request: CreateDealServiceRequest
) -> CreateDealServiceResponse:
await self.repository.create(request.entity)
deal_service = await self.repository.get_by_id(
request.entity.deal_id, request.entity.service_id
)
return CreateDealServiceResponse(
entity=DealServiceSchema.model_validate(deal_service),
message="Услуга добавлена в сделку",
)
async def update(
self, deal_id: int, service_id: int, data: UpdateDealServiceRequest
) -> UpdateDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateDealServiceResponse(message="Услуга сделки обновлена")
async def delete(self, deal_id: int, service_id: int) -> DeleteDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteDealServiceResponse(message="Услуга удалена из сделки")

View File

@ -0,0 +1,27 @@
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.repositories import ProductRepository
from modules.fulfillment_base.schemas.product import (
CreateProductRequest,
ProductSchema,
UpdateProductRequest,
)
from services.mixins import *
class ProductService(
ServiceGetAllMixin[Product, ProductSchema],
ServiceCreateMixin[Product, CreateProductRequest, ProductSchema],
ServiceUpdateMixin[Product, UpdateProductRequest],
ServiceDeleteMixin[Product],
):
schema_class = ProductSchema
entity_not_found_msg = "Товар не найден"
entity_deleted_msg = "Товар успешно удален"
entity_updated_msg = "Товар успешно обновлен"
entity_created_msg = "Товар успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ProductRepository(session)
async def is_soft_delete(self, product: ProductSchema) -> bool:
return True

View File

@ -0,0 +1,66 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.repositories import ProductServiceRepository
from modules.fulfillment_base.schemas.product_service import *
class ProductServiceService:
schema_class = ProductServiceSchema
entity_not_found_msg = "Связь услуги с товаром не найдена"
def __init__(self, session: AsyncSession):
self.repository = ProductServiceRepository(session)
async def create(
self, request: CreateProductServiceRequest
) -> CreateProductServiceResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id,
request.entity.product_id,
request.entity.service_id,
)
return CreateProductServiceResponse(
entity=ProductServiceSchema.model_validate(deal_product),
message="Услуга добавлена к товару",
)
async def update(
self,
deal_id: int,
product_id: int,
service_id: int,
data: UpdateProductServiceRequest,
) -> UpdateProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateProductServiceResponse(message="Услуга обновлена")
async def delete(
self, deal_id: int, product_id: int, service_id: int
) -> DeleteProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteProductServiceResponse(message="Товар удален из сделки")
async def duplicate_product_services(
self, request: ProductServicesDuplicateRequest
) -> ProductServicesDuplicateResponse:
services_to_copy: list[
DealProductService
] = await self.repository.get_product_services(
request.deal_id, request.source_deal_product_id
)
await self.repository.duplicate_services(
request.deal_id, request.target_deal_product_ids, services_to_copy
)
return ProductServicesDuplicateResponse(message="Услуги продублированы")

View File

@ -0,0 +1,27 @@
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.repositories import ServiceRepository
from modules.fulfillment_base.schemas.service import (
ServiceSchema,
CreateServiceRequest,
UpdateServiceRequest,
)
from services.mixins import *
class ServiceModelService(
ServiceGetAllMixin[Service, ServiceSchema],
ServiceCreateMixin[Service, CreateServiceRequest, ServiceSchema],
ServiceUpdateMixin[Service, UpdateServiceRequest],
ServiceDeleteMixin[Service],
):
schema_class = ServiceSchema
entity_not_found_msg = "Услуга не найдена"
entity_deleted_msg = "Услуга успешно удалена"
entity_updated_msg = "Услуга успешно обновлена"
entity_created_msg = "Услуга успешно создана"
def __init__(self, session: AsyncSession):
self.repository = ServiceRepository(session)
async def is_soft_delete(self, service: ServiceSchema) -> bool:
return True

View File

@ -0,0 +1,24 @@
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.repositories import ServicesKitRepository
from modules.fulfillment_base.schemas.services_kit import (
ServicesKitSchema,
CreateServicesKitRequest,
UpdateServicesKitRequest,
)
from services.mixins import *
class ServicesKitService(
ServiceCrudMixin[ServicesKit, ServicesKitSchema, CreateServicesKitRequest, UpdateServicesKitRequest]
):
schema_class = ServicesKitSchema
entity_not_found_msg = "Набор услуг не найден"
entity_deleted_msg = "Набор услуг успешно удален"
entity_updated_msg = "Набор услуг успешно обновлен"
entity_created_msg = "Набор услуг успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ServicesKitRepository(session)
async def is_soft_delete(self, service: ServicesKitSchema) -> bool:
return False

View File

@ -2,3 +2,4 @@ from .board import BoardRepository as BoardRepository
from .deal import DealRepository as DealRepository
from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository

View File

@ -3,6 +3,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
class BaseRepository:
session: AsyncSession
def __init__(self, session: AsyncSession):
self.session = session

View File

@ -0,0 +1,18 @@
from models import Board, BuiltInModule
from repositories.mixins import *
class BuiltInModuleRepository(
BaseRepository,
RepGetAllMixin[BuiltInModule],
):
entity_class = BuiltInModule
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)
async def get_by_ids(self, ids: list[int]) -> list[BuiltInModule]:
stmt = select(BuiltInModule).where(BuiltInModule.id.in_(ids))
built_in_modules = await self.session.scalars(stmt)
return built_in_modules.all()

View File

@ -65,7 +65,7 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
return model
async def update(self, entity: EntityType, data: UpdateSchemaType) -> EntityType:
pass
return await self._apply_update_data_to_model(entity, data, True)
class RepGetByIdMixin(Generic[EntityType], RepBaseMixin[EntityType]):

View File

@ -1,6 +1,7 @@
from sqlalchemy.orm import selectinload
from models.project import Project
from repositories.built_in_module import BuiltInModuleRepository
from repositories.mixins import *
from schemas.project import CreateProjectSchema, UpdateProjectSchema
@ -17,4 +18,10 @@ class ProjectRepository(
return stmt.options(selectinload(Project.boards))
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
if data.built_in_modules is not None:
built_in_modules = data.built_in_modules
data.built_in_modules = await BuiltInModuleRepository(self.session).get_by_ids(
[module.id for module in built_in_modules]
)
return await self._apply_update_data_to_model(project, data, True)

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.board import *
from services import BoardService
board_router = APIRouter(
tags=["board"],
)
router = APIRouter(tags=["board"])
@board_router.get(
@router.get(
"/{projectId}",
response_model=GetBoardsResponse,
operation_id="get_boards",
@ -21,7 +19,7 @@ async def get_boards(
return await BoardService(session).get_all(project_id)
@board_router.post(
@router.post(
"/",
response_model=CreateBoardResponse,
operation_id="create_board",
@ -33,7 +31,7 @@ async def create_board(
return await BoardService(session).create(request)
@board_router.patch(
@router.patch(
"/{pk}",
response_model=UpdateBoardResponse,
operation_id="update_board",
@ -46,7 +44,7 @@ async def update_board(
return await BoardService(session).update(pk, request)
@board_router.delete(
@router.delete(
"/{pk}",
response_model=DeleteBoardResponse,
operation_id="delete_board",

View File

@ -8,12 +8,10 @@ from backend.dependecies import (
from schemas.deal import *
from services import DealService
deal_router = APIRouter(
tags=["deal"],
)
router = APIRouter(tags=["deal"])
@deal_router.get(
@router.get(
"/",
response_model=GetDealsResponse,
operation_id="get_deals",
@ -39,7 +37,7 @@ async def get_deals(
)
@deal_router.post(
@router.post(
"/",
response_model=CreateDealResponse,
operation_id="create_deal",
@ -51,7 +49,7 @@ async def create_deal(
return await DealService(session).create(request)
@deal_router.patch(
@router.patch(
"/{pk}",
response_model=UpdateDealResponse,
operation_id="update_deal",
@ -64,7 +62,7 @@ async def update_deal(
return await DealService(session).update(pk, request)
@deal_router.delete(
@router.delete(
"/{pk}",
response_model=DeleteDealResponse,
operation_id="delete_deal",

18
routers/module.py Normal file
View File

@ -0,0 +1,18 @@
from fastapi import APIRouter
from backend.dependecies import SessionDependency
from schemas.module import GetAllBuiltInModulesResponse
from services.built_in_module import BuiltInModuleService
router = APIRouter(tags=["modules"])
@router.get(
"/built-in/",
response_model=GetAllBuiltInModulesResponse,
operation_id="get_built_in_modules",
)
async def get_built_in_modules(
session: SessionDependency,
):
return await BuiltInModuleService(session).get_all()

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.project import *
from services import ProjectService
project_router = APIRouter(
tags=["project"],
)
router = APIRouter(tags=["project"])
@project_router.get(
@router.get(
"/",
response_model=GetProjectsResponse,
operation_id="get_projects",
@ -20,7 +18,7 @@ async def get_projects(
return await ProjectService(session).get_all()
@project_router.post(
@router.post(
"/",
response_model=CreateProjectResponse,
operation_id="create_project",
@ -32,7 +30,7 @@ async def create_project(
return await ProjectService(session).create(request)
@project_router.patch(
@router.patch(
"/{pk}",
response_model=UpdateProjectResponse,
operation_id="update_project",
@ -45,7 +43,7 @@ async def update_project(
return await ProjectService(session).update(pk, request)
@project_router.delete(
@router.delete(
"/{pk}",
response_model=DeleteProjectResponse,
operation_id="delete_project",

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.status import *
from services import StatusService
status_router = APIRouter(
tags=["status"],
)
router = APIRouter(tags=["status"])
@status_router.get(
@router.get(
"/{boardId}",
response_model=GetStatusesResponse,
operation_id="get_statuses",
@ -21,7 +19,7 @@ async def get_statuses(
return await StatusService(session).get_all(board_id)
@status_router.post(
@router.post(
"/",
response_model=CreateStatusResponse,
operation_id="create_status",
@ -33,7 +31,7 @@ async def create_status(
return await StatusService(session).create(request)
@status_router.patch(
@router.patch(
"/{pk}",
response_model=UpdateStatusResponse,
operation_id="update_status",
@ -46,7 +44,7 @@ async def update_status(
return await StatusService(session).update(pk, request)
@status_router.delete(
@router.delete(
"/{pk}",
response_model=DeleteStatusResponse,
operation_id="delete_status",

23
schemas/module.py Normal file
View File

@ -0,0 +1,23 @@
from schemas.base import BaseSchema
# region Entity
class BuiltInModuleSchema(BaseSchema):
id: int
key: str
label: str
icon_name: str
description: str
# endregion
# region Response
class GetAllBuiltInModulesResponse(BaseSchema):
items: list[BuiltInModuleSchema]
# endregion

View File

@ -1,6 +1,7 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
from schemas.module import BuiltInModuleSchema
# region Entity
@ -9,6 +10,7 @@ from schemas.base import BaseSchema, BaseResponse
class ProjectSchema(BaseSchema):
id: int
name: str
built_in_modules: list[BuiltInModuleSchema]
class CreateProjectSchema(BaseSchema):
@ -17,6 +19,7 @@ class CreateProjectSchema(BaseSchema):
class UpdateProjectSchema(BaseSchema):
name: Optional[str] = None
built_in_modules: list[BuiltInModuleSchema] = None
# endregion

View File

@ -0,0 +1,11 @@
from models import BuiltInModule
from repositories import BuiltInModuleRepository
from schemas.module import BuiltInModuleSchema
from services.mixins import *
class BuiltInModuleService(ServiceGetAllMixin[BuiltInModule, BuiltInModuleSchema]):
schema_class = BuiltInModuleSchema
def __init__(self, session: AsyncSession):
self.repository = BuiltInModuleRepository(session)

View File

@ -0,0 +1,51 @@
import importlib
import pkgutil
from pathlib import Path
from types import ModuleType
from fastapi import FastAPI, APIRouter
def auto_include_routers(app: FastAPI, package: ModuleType, full_path: bool = False):
"""
Automatically discover and include FastAPI routers from a given package.
:param FastAPI app: FastAPI application.
:param ModuleType package: Imported package.
:param bool full_path: If True, prefix is built from full path. If False, prefix is only the last filename.
"""
package_path = Path(package.__file__).parent
base_pkg_name = package.__name__.replace("_", "-")
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
module = importlib.import_module(name)
# Try to get `router` from the module
router = getattr(module, "router", None)
if not isinstance(router, APIRouter):
continue
# Build API prefix
file_path = Path(module.__file__)
relative_path = file_path.relative_to(package_path)
parts: list[str] = list(relative_path.parts)
# Remove "routers" folder(s) from prefix parts
parts = [p for p in parts if p != "routers"]
# Drop extension from filename
parts[-1] = parts[-1].replace(".py", "")
if full_path:
# Use full path
prefix_parts = [p.replace("_", "-") for p in parts if p != "__init__"]
prefix_parts.insert(0, base_pkg_name)
else:
# Only use last file name
prefix_parts = [parts[-1].replace("_", "-")]
prefix = "/" + "/".join(prefix_parts)
app.include_router(router, prefix=prefix)

2
utils/exceptions.py Normal file
View File

@ -0,0 +1,2 @@
class ObjectNotFoundException(Exception):
pass