feat(P4.3): wire task depend_on check into pending→open transition via reusable helper
This commit is contained in:
@@ -16,6 +16,7 @@ from app.models.notification import Notification as NotificationModel
|
|||||||
from app.api.deps import get_current_user_or_apikey
|
from app.api.deps import get_current_user_or_apikey
|
||||||
from app.api.rbac import check_project_role, check_permission, ensure_can_edit_task
|
from app.api.rbac import check_project_role, check_permission, ensure_can_edit_task
|
||||||
from app.services.activity import log_activity
|
from app.services.activity import log_activity
|
||||||
|
from app.services.dependency_check import check_task_deps
|
||||||
|
|
||||||
router = APIRouter(tags=["Tasks"])
|
router = APIRouter(tags=["Tasks"])
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ def transition_task(
|
|||||||
# P5.1: enforce state-machine
|
# P5.1: enforce state-machine
|
||||||
_check_transition(old_status, new_status)
|
_check_transition(old_status, new_status)
|
||||||
|
|
||||||
# P5.2: pending -> open requires milestone to be undergoing (dependencies checked later)
|
# P5.2: pending -> open requires milestone to be undergoing + task deps satisfied
|
||||||
if old_status == "pending" and new_status == "open":
|
if old_status == "pending" and new_status == "open":
|
||||||
milestone = db.query(Milestone).filter(Milestone.id == task.milestone_id).first()
|
milestone = db.query(Milestone).filter(Milestone.id == task.milestone_id).first()
|
||||||
if milestone:
|
if milestone:
|
||||||
@@ -303,6 +304,13 @@ def transition_task(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Cannot open task: milestone is '{ms_status}', must be 'undergoing'",
|
detail=f"Cannot open task: milestone is '{ms_status}', must be 'undergoing'",
|
||||||
)
|
)
|
||||||
|
# P4.3: check task-level depend_on
|
||||||
|
dep_result = check_task_deps(db, task.depend_on)
|
||||||
|
if not dep_result.ok:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Cannot open task: {dep_result.reason}",
|
||||||
|
)
|
||||||
|
|
||||||
# P5.3: open -> undergoing requires assignee AND operator must be the assignee
|
# P5.3: open -> undergoing requires assignee AND operator must be the assignee
|
||||||
if old_status == "open" and new_status == "undergoing":
|
if old_status == "open" and new_status == "undergoing":
|
||||||
|
|||||||
@@ -111,3 +111,38 @@ def check_milestone_deps(
|
|||||||
result.blockers.append(f"Dependent tasks not {required_status}: {incomplete_tasks}")
|
result.blockers.append(f"Dependent tasks not {required_status}: {incomplete_tasks}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_task_deps(
|
||||||
|
db: Session,
|
||||||
|
depend_on: str | None,
|
||||||
|
*,
|
||||||
|
required_status: str = "completed",
|
||||||
|
) -> DepCheckResult:
|
||||||
|
"""Check whether a task's depend_on tasks are all satisfied.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
db:
|
||||||
|
Active DB session.
|
||||||
|
depend_on:
|
||||||
|
JSON-encoded list of task IDs (from the task's ``depend_on`` field).
|
||||||
|
required_status:
|
||||||
|
The status dependees must have reached (default ``"completed"``).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DepCheckResult with ``ok=True`` if all deps satisfied, else ``ok=False``
|
||||||
|
with human-readable ``blockers``.
|
||||||
|
"""
|
||||||
|
result = DepCheckResult()
|
||||||
|
task_ids = _parse_json_ids(depend_on)
|
||||||
|
if task_ids:
|
||||||
|
dep_tasks: Sequence[Task] = (
|
||||||
|
db.query(Task).filter(Task.id.in_(task_ids)).all()
|
||||||
|
)
|
||||||
|
incomplete = [t.id for t in dep_tasks if _task_status(t) != required_status]
|
||||||
|
if incomplete:
|
||||||
|
result.ok = False
|
||||||
|
result.blockers.append(f"Dependent tasks not {required_status}: {incomplete}")
|
||||||
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user