diff --git a/src/tracker/api/tasks.py b/src/tracker/api/tasks.py index 0afe9b3..aa39b64 100644 --- a/src/tracker/api/tasks.py +++ b/src/tracker/api/tasks.py @@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload, joinedload from tracker.database import get_db -from tracker.models import Task, Step, Project +from tracker.models import Task, Step, Project, Member, Message router = APIRouter(tags=["tasks"]) @@ -257,10 +257,12 @@ async def delete_task(task_id: str, db: AsyncSession = Depends(get_db)): @router.post("/tasks/{task_id}/take", response_model=TaskOut) async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Depends(get_db)): - """Atomically take a task — only if not already assigned.""" + """Atomically take a task — only if unassigned and in backlog/todo.""" task = await _get_task(task_id, db) if task.assignee_slug: raise HTTPException(409, f"Task already assigned to {task.assignee_slug}") + if task.status not in ("backlog", "todo"): + raise HTTPException(409, f"Task status is {task.status}, expected backlog or todo") task.assignee_slug = slug task.status = "in_progress" # Add to watchers @@ -288,11 +290,21 @@ async def reject_task(task_id: str, req: RejectRequest, db: AsyncSession = Depen task = await _get_task(task_id, db) old_assignee = task.assignee_slug task.assignee_slug = None - task.status = "todo" + task.status = "backlog" + # Add rejection comment + if old_assignee: + comment = Message( + task_id=task.id, + author_type="system", + author_slug=old_assignee, + content=f"Задача отклонена: {req.reason}", + mentions=[], + ) + db.add(comment) await db.commit() from tracker.ws.manager import manager await manager.broadcast_task_event(str(task.project_id), "task.updated", { - "id": str(task.id), "status": "todo", "assignee_slug": None, + "id": str(task.id), "status": "backlog", "assignee_slug": None, }) return {"ok": True, "reason": req.reason, "old_assignee": old_assignee} @@ -300,6 +312,10 @@ async def reject_task(task_id: str, req: RejectRequest, db: AsyncSession = Depen @router.post("/tasks/{task_id}/assign", response_model=TaskOut) async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depends(get_db)): """Assign task to a member.""" + # Verify assignee exists + result = await db.execute(select(Member).where(Member.slug == req.assignee_slug)) + if not result.scalar_one_or_none(): + raise HTTPException(404, f"Member {req.assignee_slug} not found") task = await _get_task(task_id, db) task.assignee_slug = req.assignee_slug if req.assignee_slug not in (task.watchers or []):