BE-PR-006: Add Essential CRUD API under Proposals
- New router: /projects/{project_id}/proposals/{proposal_id}/essentials
- GET (list), POST (create), GET/{id}, PATCH/{id}, DELETE/{id}
- All mutations restricted to open proposals only
- Permission: creator, project owner, or global admin
- Registered essentials router in main.py
- Updated GET /proposals/{id} to return ProposalDetailResponse with
embedded essentials list
- Activity logging on all CRUD operations
This commit is contained in:
@@ -13,6 +13,7 @@ from app.api.deps import get_current_user_or_apikey
|
||||
from app.api.rbac import check_project_role, check_permission, is_global_admin
|
||||
from app.models import models
|
||||
from app.models.proposal import Proposal, ProposalStatus
|
||||
from app.models.essential import Essential
|
||||
from app.models.milestone import Milestone, MilestoneStatus
|
||||
from app.models.task import Task, TaskStatus, TaskPriority
|
||||
from app.schemas import schemas
|
||||
@@ -21,11 +22,26 @@ from app.services.activity import log_activity
|
||||
router = APIRouter(prefix="/projects/{project_id}/proposals", tags=["Proposals"])
|
||||
|
||||
|
||||
def _serialize_proposal(db: Session, proposal: Proposal) -> dict:
|
||||
def _serialize_essential(e: Essential) -> dict:
|
||||
"""Serialize an Essential for embedding in Proposal detail."""
|
||||
return {
|
||||
"id": e.id,
|
||||
"essential_code": e.essential_code,
|
||||
"proposal_id": e.proposal_id,
|
||||
"type": e.type.value if hasattr(e.type, "value") else e.type,
|
||||
"title": e.title,
|
||||
"description": e.description,
|
||||
"created_by_id": e.created_by_id,
|
||||
"created_at": e.created_at,
|
||||
"updated_at": e.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials: bool = False) -> dict:
|
||||
"""Serialize proposal with created_by_username."""
|
||||
creator = db.query(models.User).filter(models.User.id == proposal.created_by_id).first() if proposal.created_by_id else None
|
||||
code = proposal.propose_code # DB column; also exposed as proposal_code
|
||||
return {
|
||||
result = {
|
||||
"id": proposal.id,
|
||||
"title": proposal.title,
|
||||
"description": proposal.description,
|
||||
@@ -39,6 +55,15 @@ def _serialize_proposal(db: Session, proposal: Proposal) -> dict:
|
||||
"created_at": proposal.created_at,
|
||||
"updated_at": proposal.updated_at,
|
||||
}
|
||||
if include_essentials:
|
||||
essentials = (
|
||||
db.query(Essential)
|
||||
.filter(Essential.proposal_id == proposal.id)
|
||||
.order_by(Essential.id.asc())
|
||||
.all()
|
||||
)
|
||||
result["essentials"] = [_serialize_essential(e) for e in essentials]
|
||||
return result
|
||||
|
||||
|
||||
def _find_project(db, identifier):
|
||||
@@ -150,13 +175,14 @@ def create_proposal(
|
||||
return _serialize_proposal(db, proposal)
|
||||
|
||||
|
||||
@router.get("/{proposal_id}", response_model=schemas.ProposalResponse)
|
||||
@router.get("/{proposal_id}", response_model=schemas.ProposalDetailResponse)
|
||||
def get_proposal(
|
||||
project_id: str,
|
||||
proposal_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey),
|
||||
):
|
||||
"""Get a single Proposal with its Essentials list embedded."""
|
||||
project = _find_project(db, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
@@ -164,7 +190,7 @@ def get_proposal(
|
||||
proposal = _find_proposal(db, proposal_id, project.id)
|
||||
if not proposal:
|
||||
raise HTTPException(status_code=404, detail="Proposal not found")
|
||||
return _serialize_proposal(db, proposal)
|
||||
return _serialize_proposal(db, proposal, include_essentials=True)
|
||||
|
||||
|
||||
@router.patch("/{proposal_id}", response_model=schemas.ProposalResponse)
|
||||
|
||||
Reference in New Issue
Block a user