feat: add code-first API support for projects, milestones, proposes, tasks

- Projects: get/update/delete/members endpoints now accept project_code
- Milestones: all project-scoped and top-level endpoints accept milestone_code
- Proposes: all endpoints accept project_code and propose_code
- Tasks: code-first support for all CRUD + transition + take + search
- Schemas: add code/type/due_date/project_code/milestone_code/taken_by fields
- All endpoints use id-or-code lookup helpers for backward compatibility
- Milestone serializer now includes milestone_code and code fields
- Task serializer enriches responses with project_code, milestone_code, taken_by

Addresses TODO §2.1: code-first API support across CLI-targeted resources
This commit is contained in:
zhi
2026-03-21 18:12:04 +00:00
parent 32e79a41d8
commit 43af5b29f6
6 changed files with 442 additions and 124 deletions

View File

@@ -145,17 +145,29 @@ def list_milestones(project_id: int = None, status_filter: str = None, db: Sessi
return query.order_by(MilestoneModel.due_date.is_(None), MilestoneModel.due_date.asc()).all()
def _find_milestone_by_id_or_code(db, identifier) -> MilestoneModel | None:
"""Look up milestone by numeric id or milestone_code."""
try:
mid = int(identifier)
ms = db.query(MilestoneModel).filter(MilestoneModel.id == mid).first()
if ms:
return ms
except (ValueError, TypeError):
pass
return db.query(MilestoneModel).filter(MilestoneModel.milestone_code == str(identifier)).first()
@router.get("/milestones/{milestone_id}", response_model=schemas.MilestoneResponse, tags=["Milestones"])
def get_milestone(milestone_id: int, db: Session = Depends(get_db)):
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
def get_milestone(milestone_id: str, db: Session = Depends(get_db)):
ms = _find_milestone_by_id_or_code(db, milestone_id)
if not ms:
raise HTTPException(status_code=404, detail="Milestone not found")
return ms
@router.patch("/milestones/{milestone_id}", response_model=schemas.MilestoneResponse, tags=["Milestones"])
def update_milestone(milestone_id: int, ms_update: schemas.MilestoneUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
def update_milestone(milestone_id: str, ms_update: schemas.MilestoneUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
ms = _find_milestone_by_id_or_code(db, milestone_id)
if not ms:
raise HTTPException(status_code=404, detail="Milestone not found")
ensure_can_edit_milestone(db, current_user.id, ms)
@@ -167,8 +179,8 @@ def update_milestone(milestone_id: int, ms_update: schemas.MilestoneUpdate, db:
@router.delete("/milestones/{milestone_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Milestones"])
def delete_milestone(milestone_id: int, db: Session = Depends(get_db)):
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
def delete_milestone(milestone_id: str, db: Session = Depends(get_db)):
ms = _find_milestone_by_id_or_code(db, milestone_id)
if not ms:
raise HTTPException(status_code=404, detail="Milestone not found")
db.delete(ms)
@@ -177,8 +189,8 @@ def delete_milestone(milestone_id: int, db: Session = Depends(get_db)):
@router.get("/milestones/{milestone_id}/progress", tags=["Milestones"])
def milestone_progress(milestone_id: int, db: Session = Depends(get_db)):
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
def milestone_progress(milestone_id: str, db: Session = Depends(get_db)):
ms = _find_milestone_by_id_or_code(db, milestone_id)
if not ms:
raise HTTPException(status_code=404, detail="Milestone not found")
tasks = db.query(Task).filter(Task.milestone_id == milestone_id).all()