start
This commit is contained in:
97
backend/app/services/boxes.py
Normal file
97
backend/app/services/boxes.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.box import Box
|
||||
from app.schemas.box import BoxCreate, BoxRead, BoxSummary, BoxUpdate
|
||||
from app.schemas.object import ObjectSummary
|
||||
from app.services.qrcodes import box_public_url
|
||||
|
||||
|
||||
def _select_with_relations():
|
||||
return select(Box).options(selectinload(Box.children), selectinload(Box.objects))
|
||||
|
||||
|
||||
async def list_boxes(
|
||||
db: AsyncSession, search: str | None = None, root_only: bool = False
|
||||
) -> list[Box]:
|
||||
stmt = select(Box).order_by(Box.name)
|
||||
if root_only:
|
||||
stmt = stmt.where(Box.parent_box_id.is_(None))
|
||||
if search:
|
||||
stmt = stmt.where(Box.name.ilike(f"%{search}%"))
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_box(db: AsyncSession, box_id: uuid.UUID) -> Box | None:
|
||||
stmt = _select_with_relations().where(Box.id == box_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def build_box_read(db: AsyncSession, box: Box) -> BoxRead:
|
||||
ancestors: list[BoxSummary] = []
|
||||
current_parent_id = box.parent_box_id
|
||||
depth = 0
|
||||
while current_parent_id is not None and depth < 100:
|
||||
parent = await db.get(Box, current_parent_id)
|
||||
if parent is None:
|
||||
break
|
||||
ancestors.append(BoxSummary(id=parent.id, name=parent.name))
|
||||
current_parent_id = parent.parent_box_id
|
||||
depth += 1
|
||||
ancestors.reverse()
|
||||
|
||||
return BoxRead(
|
||||
id=box.id,
|
||||
name=box.name,
|
||||
description=box.description,
|
||||
parent_box_id=box.parent_box_id,
|
||||
created_at=box.created_at,
|
||||
updated_at=box.updated_at,
|
||||
qr_code_url=box_public_url(box.id),
|
||||
ancestors=ancestors,
|
||||
children=[BoxSummary.model_validate(c) for c in box.children],
|
||||
objects=[ObjectSummary.model_validate(o) for o in box.objects],
|
||||
)
|
||||
|
||||
|
||||
async def _is_descendant(db: AsyncSession, box_id: uuid.UUID, candidate_ancestor_id: uuid.UUID) -> bool:
|
||||
"""Whether candidate_ancestor_id is box_id itself or one of its descendants."""
|
||||
if box_id == candidate_ancestor_id:
|
||||
return True
|
||||
stmt = select(Box.id).where(Box.parent_box_id == box_id)
|
||||
result = await db.execute(stmt)
|
||||
for child_id in result.scalars().all():
|
||||
if await _is_descendant(db, child_id, candidate_ancestor_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def create_box(db: AsyncSession, data: BoxCreate) -> Box:
|
||||
box = Box(**data.model_dump())
|
||||
db.add(box)
|
||||
await db.commit()
|
||||
return await get_box(db, box.id)
|
||||
|
||||
|
||||
async def update_box(db: AsyncSession, box: Box, data: BoxUpdate) -> Box:
|
||||
updates = data.model_dump(exclude_unset=True)
|
||||
new_parent_id = updates.get("parent_box_id", box.parent_box_id)
|
||||
if new_parent_id is not None and await _is_descendant(db, box.id, new_parent_id):
|
||||
raise ValueError("A box cannot be moved into itself or one of its own descendants")
|
||||
for field, value in updates.items():
|
||||
setattr(box, field, value)
|
||||
await db.commit()
|
||||
return await get_box(db, box.id)
|
||||
|
||||
|
||||
async def delete_box(db: AsyncSession, box: Box) -> None:
|
||||
child_count = await db.scalar(select(Box.id).where(Box.parent_box_id == box.id).limit(1))
|
||||
if child_count is not None:
|
||||
raise ValueError("Cannot delete a box that still contains nested boxes")
|
||||
await db.delete(box)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user