From a4ac2a6471f6c748507db300ad5eb842e8888038 Mon Sep 17 00:00:00 2001 From: markov Date: Sat, 28 Feb 2026 00:03:08 +0100 Subject: [PATCH] Subtasks: load subtasks + parent in task_out - selectinload(Task.subtasks), joinedload(Task.parent) in _get_task + list_tasks - SubtaskBrief schema: id, key, title, status, assignee - TaskOut: subtasks list, parent_key, parent_title - .unique() for joinedload dedup --- src/tracker/api/converters.py | 23 +++++++++++++++++++++++ src/tracker/api/schemas.py | 11 +++++++++++ src/tracker/api/tasks.py | 8 ++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/tracker/api/converters.py b/src/tracker/api/converters.py index 69efd8c..5f61fe7 100644 --- a/src/tracker/api/converters.py +++ b/src/tracker/api/converters.py @@ -10,6 +10,7 @@ from .schemas import ( MessageOut, ProjectFileOut, StepOut, + SubtaskBrief, TaskOut, ) @@ -95,6 +96,25 @@ def message_out(m: Message) -> MessageOut: def task_out(t: Task, project_slug: str = "") -> TaskOut: prefix = project_slug[:2].upper() if project_slug else "XX" + + # Subtasks + subtasks = [] + for sub in (t.subtasks or []): + subtasks.append(SubtaskBrief( + id=str(sub.id), + key=f"{prefix}-{sub.number}", + title=sub.title, + status=sub.status, + assignee=member_brief(sub.assignee) if sub.assignee else None, + )) + + # Parent info + parent_key = None + parent_title = None + if t.parent: + parent_key = f"{prefix}-{t.parent.number}" + parent_title = t.parent.title + return TaskOut( id=str(t.id), project_id=str(t.project_id), @@ -116,6 +136,9 @@ def task_out(t: Task, project_slug: str = "") -> TaskOut: position=t.position, time_spent=t.time_spent, steps=[step_out(s) for s in (t.steps or [])], + subtasks=subtasks, + parent_key=parent_key, + parent_title=parent_title, created_at=t.created_at.isoformat() if t.created_at else "", updated_at=t.updated_at.isoformat() if t.updated_at else "", ) diff --git a/src/tracker/api/schemas.py b/src/tracker/api/schemas.py index f628fbb..03040dd 100644 --- a/src/tracker/api/schemas.py +++ b/src/tracker/api/schemas.py @@ -47,10 +47,21 @@ class TaskOut(BaseModel): position: int time_spent: int steps: list[StepOut] = [] + subtasks: list["SubtaskBrief"] = [] + parent_key: str | None = None + parent_title: str | None = None created_at: str updated_at: str +class SubtaskBrief(BaseModel): + id: str + key: str + title: str + status: str + assignee: MemberBrief | None = None + + class MessageOut(BaseModel): id: str chat_id: str | None = None diff --git a/src/tracker/api/tasks.py b/src/tracker/api/tasks.py index 14298dd..a7a3853 100644 --- a/src/tracker/api/tasks.py +++ b/src/tracker/api/tasks.py @@ -189,12 +189,14 @@ async def _get_task(task_id: str, db: AsyncSession) -> Task: select(Task).where(Task.id == uuid.UUID(task_id)) .options( selectinload(Task.steps), + selectinload(Task.subtasks).joinedload(Task.assignee), joinedload(Task.project), joinedload(Task.assignee), joinedload(Task.reviewer), + joinedload(Task.parent), ) ) - task = result.scalar_one_or_none() + task = result.unique().scalar_one_or_none() if not task: raise HTTPException(404, "Task not found") return task @@ -221,9 +223,11 @@ async def list_tasks( ): q = select(Task).options( selectinload(Task.steps), + selectinload(Task.subtasks).joinedload(Task.assignee), joinedload(Task.project), joinedload(Task.assignee), joinedload(Task.reviewer), + joinedload(Task.parent), ) if project_id: q = q.where(Task.project_id == uuid.UUID(project_id)) @@ -235,7 +239,7 @@ async def list_tasks( q = q.where(Task.labels.contains([label])) q = q.order_by(Task.position, Task.created_at) result = await db.execute(q) - return [task_out(t, t.project.slug if t.project else "") for t in result.scalars()] + return [task_out(t, t.project.slug if t.project else "") for t in result.unique().scalars()] @router.get("/tasks/{task_id}", response_model=TaskOut)