P4.1: Extract reusable dependency check helper, deduplicate milestone_actions.py
- New app/services/dependency_check.py with check_milestone_deps() - Replaces 3x duplicated JSON-parse + query + filter logic - Supports both milestone and task dependency checking - Returns structured DepCheckResult with ok/blockers/reason - Refactored preflight and start endpoints to use shared helper
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
Provides freeze / start / close actions on milestones.
|
||||
completed is triggered automatically when the sole release maintenance task finishes.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
@@ -18,6 +17,7 @@ from app.models import models
|
||||
from app.models.milestone import Milestone, MilestoneStatus
|
||||
from app.models.task import Task, TaskStatus
|
||||
from app.services.activity import log_activity
|
||||
from app.services.dependency_check import check_milestone_deps
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/projects/{project_id}/milestones/{milestone_id}/actions",
|
||||
@@ -101,38 +101,13 @@ def preflight_milestone_actions(
|
||||
|
||||
# --- start pre-check (only meaningful when status == freeze) ---
|
||||
if ms_status == "freeze":
|
||||
blockers: list[str] = []
|
||||
|
||||
# milestone dependencies
|
||||
dep_ms_ids = []
|
||||
if ms.depend_on_milestones:
|
||||
try:
|
||||
dep_ms_ids = json.loads(ms.depend_on_milestones)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
dep_ms_ids = []
|
||||
if dep_ms_ids:
|
||||
dep_milestones = db.query(Milestone).filter(Milestone.id.in_(dep_ms_ids)).all()
|
||||
incomplete = [m.id for m in dep_milestones if _ms_status_value(m) != "completed"]
|
||||
if incomplete:
|
||||
blockers.append(f"Dependent milestones not completed: {incomplete}")
|
||||
|
||||
# task dependencies
|
||||
dep_task_ids = []
|
||||
if ms.depend_on_tasks:
|
||||
try:
|
||||
dep_task_ids = json.loads(ms.depend_on_tasks)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
dep_task_ids = []
|
||||
if dep_task_ids:
|
||||
dep_tasks = db.query(Task).filter(Task.id.in_(dep_task_ids)).all()
|
||||
incomplete_tasks = [t.id for t in dep_tasks if (t.status.value if hasattr(t.status, "value") else t.status) != "completed"]
|
||||
if incomplete_tasks:
|
||||
blockers.append(f"Dependent tasks not completed: {incomplete_tasks}")
|
||||
|
||||
if blockers:
|
||||
result["start"] = {"allowed": False, "reason": "; ".join(blockers)}
|
||||
else:
|
||||
dep_result = check_milestone_deps(
|
||||
db, ms.depend_on_milestones, ms.depend_on_tasks,
|
||||
)
|
||||
if dep_result.ok:
|
||||
result["start"] = {"allowed": True, "reason": None}
|
||||
else:
|
||||
result["start"] = {"allowed": False, "reason": dep_result.reason}
|
||||
|
||||
return result
|
||||
|
||||
@@ -232,51 +207,15 @@ def start_milestone(
|
||||
detail=f"Cannot start: milestone is '{_ms_status_value(ms)}', expected 'freeze'",
|
||||
)
|
||||
|
||||
# Dependency check — milestone dependencies
|
||||
dep_ms_ids = []
|
||||
if ms.depend_on_milestones:
|
||||
try:
|
||||
dep_ms_ids = json.loads(ms.depend_on_milestones)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
dep_ms_ids = []
|
||||
|
||||
if dep_ms_ids:
|
||||
dep_milestones = (
|
||||
db.query(Milestone)
|
||||
.filter(Milestone.id.in_(dep_ms_ids))
|
||||
.all()
|
||||
# Dependency check (P4.1 — shared helper)
|
||||
dep_result = check_milestone_deps(
|
||||
db, ms.depend_on_milestones, ms.depend_on_tasks,
|
||||
)
|
||||
if not dep_result.ok:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot start: {dep_result.reason}",
|
||||
)
|
||||
incomplete = [
|
||||
m.id
|
||||
for m in dep_milestones
|
||||
if _ms_status_value(m) != "completed"
|
||||
]
|
||||
if incomplete:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot start: dependent milestones not completed: {incomplete}",
|
||||
)
|
||||
|
||||
# Dependency check — task dependencies
|
||||
dep_task_ids = []
|
||||
if ms.depend_on_tasks:
|
||||
try:
|
||||
dep_task_ids = json.loads(ms.depend_on_tasks)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
dep_task_ids = []
|
||||
|
||||
if dep_task_ids:
|
||||
dep_tasks = db.query(Task).filter(Task.id.in_(dep_task_ids)).all()
|
||||
incomplete_tasks = [
|
||||
t.id
|
||||
for t in dep_tasks
|
||||
if (t.status.value if hasattr(t.status, "value") else t.status) != "completed"
|
||||
]
|
||||
if incomplete_tasks:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot start: dependent tasks not completed: {incomplete_tasks}",
|
||||
)
|
||||
|
||||
ms.status = MilestoneStatus.UNDERGOING
|
||||
ms.started_at = datetime.now(timezone.utc)
|
||||
|
||||
Reference in New Issue
Block a user