feat(P3.6): milestone edit restrictions — block PATCH in terminal states, restrict scope fields in freeze/undergoing, protect delete
This commit is contained in:
@@ -82,7 +82,36 @@ def update_milestone(project_id: int, milestone_id: int, milestone: schemas.Mile
|
|||||||
if not db_milestone:
|
if not db_milestone:
|
||||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||||
ensure_can_edit_milestone(db, current_user.id, db_milestone)
|
ensure_can_edit_milestone(db, current_user.id, db_milestone)
|
||||||
|
|
||||||
|
# --- P3.6 Milestone edit restrictions based on status ---
|
||||||
|
ms_status = db_milestone.status.value if hasattr(db_milestone.status, 'value') else db_milestone.status
|
||||||
|
|
||||||
|
# Terminal states: no edits allowed
|
||||||
|
if ms_status in ("completed", "closed"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Cannot edit a milestone that is '{ms_status}'. No modifications are allowed in terminal state."
|
||||||
|
)
|
||||||
|
|
||||||
data = milestone.model_dump(exclude_unset=True)
|
data = milestone.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
# Never allow status changes via PATCH — use action endpoints instead
|
||||||
|
if "status" in data:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Milestone status cannot be changed via PATCH. Use the action endpoints (freeze/start/close) instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Freeze / undergoing: restrict scope-changing fields
|
||||||
|
SCOPE_FIELDS = {"title", "description", "due_date", "planned_release_date", "depend_on_milestones", "depend_on_tasks"}
|
||||||
|
if ms_status in ("freeze", "undergoing"):
|
||||||
|
blocked = SCOPE_FIELDS & set(data.keys())
|
||||||
|
if blocked:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Cannot modify scope fields {sorted(blocked)} when milestone is '{ms_status}'. Scope changes are only allowed in 'open' status."
|
||||||
|
)
|
||||||
|
|
||||||
if "depend_on_milestones" in data:
|
if "depend_on_milestones" in data:
|
||||||
data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"]) if data["depend_on_milestones"] else None
|
data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"]) if data["depend_on_milestones"] else None
|
||||||
if "depend_on_tasks" in data:
|
if "depend_on_tasks" in data:
|
||||||
@@ -100,6 +129,9 @@ def delete_milestone(project_id: int, milestone_id: int, db: Session = Depends(g
|
|||||||
db_milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
db_milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||||
if not db_milestone:
|
if not db_milestone:
|
||||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||||
|
ms_status = db_milestone.status.value if hasattr(db_milestone.status, 'value') else db_milestone.status
|
||||||
|
if ms_status in ("undergoing", "completed"):
|
||||||
|
raise HTTPException(status_code=400, detail=f"Cannot delete a milestone that is '{ms_status}'")
|
||||||
db.delete(db_milestone)
|
db.delete(db_milestone)
|
||||||
db.commit()
|
db.commit()
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user