feat(P8.3): milestone preflight endpoint for freeze/start button pre-condition checks
This commit is contained in:
@@ -53,6 +53,90 @@ class CloseBody(BaseModel):
|
||||
reason: Optional[str] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /preflight — lightweight pre-condition check for UI button states
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/preflight", status_code=200)
|
||||
def preflight_milestone_actions(
|
||||
project_id: int,
|
||||
milestone_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey),
|
||||
):
|
||||
"""Return pre-condition check results for freeze / start actions.
|
||||
|
||||
The frontend uses this to decide whether to *disable* buttons and what
|
||||
hint text to show. This endpoint never mutates data.
|
||||
"""
|
||||
check_project_role(db, current_user.id, project_id, min_role="viewer")
|
||||
ms = _get_milestone_or_404(db, project_id, milestone_id)
|
||||
ms_status = _ms_status_value(ms)
|
||||
|
||||
result: dict = {"status": ms_status, "freeze": None, "start": None}
|
||||
|
||||
# --- freeze pre-check (only meaningful when status == open) ---
|
||||
if ms_status == "open":
|
||||
release_tasks = (
|
||||
db.query(Task)
|
||||
.filter(
|
||||
Task.milestone_id == milestone_id,
|
||||
Task.task_type == "maintenance",
|
||||
Task.task_subtype == "release",
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if len(release_tasks) == 0:
|
||||
result["freeze"] = {
|
||||
"allowed": False,
|
||||
"reason": "No maintenance/release task found. Create one before freezing.",
|
||||
}
|
||||
elif len(release_tasks) > 1:
|
||||
result["freeze"] = {
|
||||
"allowed": False,
|
||||
"reason": f"Found {len(release_tasks)} maintenance/release tasks — expected exactly 1.",
|
||||
}
|
||||
else:
|
||||
result["freeze"] = {"allowed": True, "reason": None}
|
||||
|
||||
# --- 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:
|
||||
result["start"] = {"allowed": True, "reason": None}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /freeze
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user