137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
"""Category attributes — scoped attribute definitions per category (P2 set).
|
|
|
|
Identified by composite key {category_id}/{attribute_definition_id}.
|
|
The nested `attribute` object is ALWAYS included (not expand-based).
|
|
"""
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db.engine import get_db
|
|
from app.models.catalog import AttributeDefinition, CategoryAttribute, MaterialCategory
|
|
from app.schemas.catalog import CategoryAttributeCreate, CategoryAttributeUpdate
|
|
from app.utils.errors import ConflictError, NotFoundError
|
|
from app.utils.pagination import paginate
|
|
|
|
router = APIRouter(
|
|
prefix="/material-categories/{category_id}/attributes",
|
|
tags=["category-attributes"],
|
|
)
|
|
|
|
|
|
def ca_to_dict(ca: CategoryAttribute, attribute: AttributeDefinition) -> dict:
|
|
return {
|
|
"id": str(ca.id),
|
|
"category_id": str(ca.category_id),
|
|
"attribute_definition_id": str(ca.attribute_definition_id),
|
|
"is_required": ca.is_required,
|
|
"is_filterable": ca.is_filterable,
|
|
"conditional_rules": ca.conditional_rules,
|
|
"sort_order": ca.sort_order,
|
|
"attribute": {
|
|
"attribute_key": attribute.attribute_key,
|
|
"label": attribute.label,
|
|
"data_type": attribute.data_type,
|
|
"unit_id": attribute.unit_id,
|
|
"enum_values": attribute.enum_values,
|
|
},
|
|
}
|
|
|
|
|
|
async def _category_or_404(db: AsyncSession, category_id) -> MaterialCategory:
|
|
category = await db.get(MaterialCategory, category_id)
|
|
if category is None:
|
|
raise NotFoundError("دستهبندی یافت نشد")
|
|
return category
|
|
|
|
|
|
async def _with_attribute(db: AsyncSession, ca: CategoryAttribute) -> dict:
|
|
attribute = await db.get(AttributeDefinition, ca.attribute_definition_id)
|
|
return ca_to_dict(ca, attribute)
|
|
|
|
|
|
@router.get("")
|
|
async def list_category_attributes(
|
|
category_id,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
await _category_or_404(db, category_id)
|
|
stmt = (
|
|
select(CategoryAttribute)
|
|
.where(CategoryAttribute.category_id == category_id)
|
|
.order_by(CategoryAttribute.sort_order.asc())
|
|
)
|
|
items, meta = await paginate(stmt, page, page_size, db)
|
|
return {"data": [await _with_attribute(db, ca) for ca in items], "meta": meta}
|
|
|
|
|
|
@router.post("", status_code=201)
|
|
async def create_category_attribute(
|
|
category_id, payload: CategoryAttributeCreate, db: AsyncSession = Depends(get_db)
|
|
):
|
|
await _category_or_404(db, category_id)
|
|
if await db.get(AttributeDefinition, payload.attribute_definition_id) is None:
|
|
raise NotFoundError("تعریف ویژگی یافت نشد", field="attribute_definition_id")
|
|
duplicate = await db.scalar(
|
|
select(CategoryAttribute.id).where(
|
|
CategoryAttribute.category_id == category_id,
|
|
CategoryAttribute.attribute_definition_id == payload.attribute_definition_id,
|
|
)
|
|
)
|
|
if duplicate is not None:
|
|
raise ConflictError("این ویژگی قبلاً به این دستهبندی اضافه شده است", field="attribute_definition_id")
|
|
ca = CategoryAttribute(
|
|
category_id=category_id,
|
|
attribute_definition_id=payload.attribute_definition_id,
|
|
is_required=payload.is_required,
|
|
is_filterable=payload.is_filterable,
|
|
conditional_rules=payload.conditional_rules,
|
|
sort_order=payload.sort_order,
|
|
)
|
|
db.add(ca)
|
|
await db.flush()
|
|
return {"data": await _with_attribute(db, ca)}
|
|
|
|
|
|
@router.patch("/{attribute_definition_id}")
|
|
async def update_category_attribute(
|
|
category_id, attribute_definition_id, payload: CategoryAttributeUpdate, db: AsyncSession = Depends(get_db)
|
|
):
|
|
ca = await db.scalar(
|
|
select(CategoryAttribute).where(
|
|
CategoryAttribute.category_id == category_id,
|
|
CategoryAttribute.attribute_definition_id == attribute_definition_id,
|
|
)
|
|
)
|
|
if ca is None:
|
|
raise NotFoundError("ویژگی این دستهبندی یافت نشد")
|
|
if payload.is_required is not None:
|
|
ca.is_required = payload.is_required
|
|
if payload.is_filterable is not None:
|
|
ca.is_filterable = payload.is_filterable
|
|
if payload.conditional_rules is not None:
|
|
ca.conditional_rules = payload.conditional_rules
|
|
if payload.sort_order is not None:
|
|
ca.sort_order = payload.sort_order
|
|
await db.flush()
|
|
return {"data": await _with_attribute(db, ca)}
|
|
|
|
|
|
@router.delete("/{attribute_definition_id}")
|
|
async def delete_category_attribute(
|
|
category_id, attribute_definition_id, db: AsyncSession = Depends(get_db)
|
|
):
|
|
ca = await db.scalar(
|
|
select(CategoryAttribute).where(
|
|
CategoryAttribute.category_id == category_id,
|
|
CategoryAttribute.attribute_definition_id == attribute_definition_id,
|
|
)
|
|
)
|
|
if ca is None:
|
|
raise NotFoundError("ویژگی این دستهبندی یافت نشد")
|
|
await db.delete(ca)
|
|
await db.flush()
|
|
return {"data": {"deleted": True}}
|