diff --git a/app/api/routers/misc.py b/app/api/routers/misc.py index 83b9467..fefa4a5 100644 --- a/app/api/routers/misc.py +++ b/app/api/routers/misc.py @@ -13,7 +13,7 @@ from pydantic import BaseModel from app.core.config import get_db from app.api.deps import get_current_user_or_apikey -from app.api.rbac import ensure_can_edit_milestone +from app.api.rbac import check_project_role, ensure_can_edit_milestone from app.models import models from app.models.apikey import APIKey from app.models.activity import ActivityLog @@ -484,26 +484,58 @@ def create_milestone_task(project_code: str, milestone_id: int, task_data: dict, # ============ Supports ============ + +def _find_support_by_id_or_code(db: Session, identifier: str) -> Support | None: + try: + support_id = int(identifier) + support = db.query(Support).filter(Support.id == support_id).first() + if support: + return support + except (TypeError, ValueError): + pass + return db.query(Support).filter(Support.support_code == str(identifier)).first() + + + +def _serialize_support(db: Session, support: Support) -> dict: + project = db.query(models.Project).filter(models.Project.id == support.project_id).first() + milestone = db.query(MilestoneModel).filter(MilestoneModel.id == support.milestone_id).first() + assignee = None + if support.assignee_id: + assignee = db.query(models.User).filter(models.User.id == support.assignee_id).first() + + return { + "id": support.id, + "code": support.support_code, + "support_code": support.support_code, + "title": support.title, + "description": support.description, + "status": support.status.value if hasattr(support.status, "value") else support.status, + "priority": support.priority.value if hasattr(support.priority, "value") else support.priority, + "project_id": support.project_id, + "project_code": project.project_code if project else None, + "milestone_id": support.milestone_id, + "milestone_code": milestone.milestone_code if milestone else None, + "reporter_id": support.reporter_id, + "assignee_id": support.assignee_id, + "taken_by": assignee.username if assignee else None, + "created_at": support.created_at, + "updated_at": support.updated_at, + } + + @router.get("/supports/{project_code}/{milestone_id}", tags=["Supports"]) def list_supports(project_code: str, milestone_id: int, db: Session = Depends(get_db)): project = db.query(models.Project).filter(models.Project.project_code == project_code).first() if not project: raise HTTPException(status_code=404, detail="Project not found") - + supports = db.query(Support).filter( Support.project_id == project.id, Support.milestone_id == milestone_id ).all() - - return [{ - "id": s.id, - "title": s.title, - "description": s.description, - "status": s.status.value, - "priority": s.priority.value, - "assignee_id": s.assignee_id, - "created_at": s.created_at, - } for s in supports] + + return [_serialize_support(db, s) for s in supports] @router.post("/supports/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["Supports"]) @@ -511,19 +543,19 @@ def create_support(project_code: str, milestone_id: int, support_data: dict, db: project = db.query(models.Project).filter(models.Project.project_code == project_code).first() if not project: raise HTTPException(status_code=404, detail="Project not found") - + ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first() if not ms: raise HTTPException(status_code=404, detail="Milestone not found") - + if ms.status and hasattr(ms.status, "value") and ms.status.value == "undergoing": raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is undergoing") - + milestone_code = ms.milestone_code or f"m{ms.id}" max_support = db.query(Support).filter(Support.milestone_id == milestone_id).order_by(Support.id.desc()).first() next_num = (max_support.id + 1) if max_support else 1 support_code = f"{milestone_code}:S{next_num:05x}" - + support = Support( title=support_data.get("title"), description=support_data.get("description"), @@ -537,7 +569,89 @@ def create_support(project_code: str, milestone_id: int, support_data: dict, db: db.add(support) db.commit() db.refresh(support) - return support + return _serialize_support(db, support) + + +@router.get("/supports/{support_id}", tags=["Supports"]) +def get_support(support_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + support = _find_support_by_id_or_code(db, support_id) + if not support: + raise HTTPException(status_code=404, detail="Support not found") + check_project_role(db, current_user.id, support.project_id, min_role="viewer") + return _serialize_support(db, support) + + +@router.patch("/supports/{support_id}", tags=["Supports"]) +def update_support(support_id: str, support_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + support = _find_support_by_id_or_code(db, support_id) + if not support: + raise HTTPException(status_code=404, detail="Support not found") + check_project_role(db, current_user.id, support.project_id, min_role="dev") + + allowed_fields = {"title", "description", "status", "priority"} + updated = False + for field, value in support_data.items(): + if field not in allowed_fields: + continue + if field == "status" and value is not None: + value = SupportStatus(value) + if field == "priority" and value is not None: + value = SupportPriority(value) + setattr(support, field, value) + updated = True + + if not updated: + raise HTTPException(status_code=400, detail="No supported fields to update") + + db.commit() + db.refresh(support) + return _serialize_support(db, support) + + +@router.delete("/supports/{support_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Supports"]) +def delete_support(support_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + support = _find_support_by_id_or_code(db, support_id) + if not support: + raise HTTPException(status_code=404, detail="Support not found") + check_project_role(db, current_user.id, support.project_id, min_role="dev") + db.delete(support) + db.commit() + return None + + +@router.post("/supports/{support_id}/take", tags=["Supports"]) +def take_support(support_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + support = _find_support_by_id_or_code(db, support_id) + if not support: + raise HTTPException(status_code=404, detail="Support not found") + check_project_role(db, current_user.id, support.project_id, min_role="dev") + + if support.assignee_id and support.assignee_id != current_user.id: + assignee = db.query(models.User).filter(models.User.id == support.assignee_id).first() + assignee_name = assignee.username if assignee else str(support.assignee_id) + raise HTTPException(status_code=409, detail=f"Support is already taken by {assignee_name}") + + support.assignee_id = current_user.id + db.commit() + db.refresh(support) + return _serialize_support(db, support) + + +@router.post("/supports/{support_id}/transition", tags=["Supports"]) +def transition_support(support_id: str, support_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + support = _find_support_by_id_or_code(db, support_id) + if not support: + raise HTTPException(status_code=404, detail="Support not found") + check_project_role(db, current_user.id, support.project_id, min_role="dev") + + status_value = support_data.get("status") + if not status_value: + raise HTTPException(status_code=400, detail="status is required") + + support.status = SupportStatus(status_value) + db.commit() + db.refresh(support) + return _serialize_support(db, support) # ============ Meetings ============