|
|
|
|
@@ -236,7 +236,7 @@ class AcceptRequest(schemas.BaseModel):
|
|
|
|
|
milestone_id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{proposal_id}/accept", response_model=schemas.ProposalResponse)
|
|
|
|
|
@router.post("/{proposal_id}/accept", response_model=schemas.ProposalAcceptResponse)
|
|
|
|
|
def accept_proposal(
|
|
|
|
|
project_id: str,
|
|
|
|
|
proposal_id: str,
|
|
|
|
|
@@ -244,7 +244,16 @@ def accept_proposal(
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: models.User = Depends(get_current_user_or_apikey),
|
|
|
|
|
):
|
|
|
|
|
"""Accept a proposal: create a feature story task in the chosen milestone."""
|
|
|
|
|
"""Accept a proposal: generate story tasks from all Essentials into the chosen milestone.
|
|
|
|
|
|
|
|
|
|
Each Essential under the Proposal produces a corresponding ``story/*`` task:
|
|
|
|
|
- feature → story/feature
|
|
|
|
|
- improvement → story/improvement
|
|
|
|
|
- refactor → story/refactor
|
|
|
|
|
|
|
|
|
|
All tasks are created in a single transaction. The Proposal must have at
|
|
|
|
|
least one Essential to be accepted.
|
|
|
|
|
"""
|
|
|
|
|
project = _find_project(db, project_id)
|
|
|
|
|
if not project:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
|
@@ -270,18 +279,47 @@ def accept_proposal(
|
|
|
|
|
if ms_status != "open":
|
|
|
|
|
raise HTTPException(status_code=400, detail="Target milestone must be in 'open' status")
|
|
|
|
|
|
|
|
|
|
# Generate task code
|
|
|
|
|
# Fetch all Essentials for this Proposal
|
|
|
|
|
essentials = (
|
|
|
|
|
db.query(Essential)
|
|
|
|
|
.filter(Essential.proposal_id == proposal.id)
|
|
|
|
|
.order_by(Essential.id.asc())
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
if not essentials:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="Proposal has no Essentials. Add at least one Essential before accepting.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Map Essential type → task subtype
|
|
|
|
|
ESSENTIAL_TYPE_TO_SUBTYPE = {
|
|
|
|
|
"feature": "feature",
|
|
|
|
|
"improvement": "improvement",
|
|
|
|
|
"refactor": "refactor",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Determine next task number in this milestone
|
|
|
|
|
milestone_code = milestone.milestone_code or f"m{milestone.id}"
|
|
|
|
|
max_task = db.query(Task).filter(Task.milestone_id == milestone.id).order_by(Task.id.desc()).first()
|
|
|
|
|
next_num = (max_task.id + 1) if max_task else 1
|
|
|
|
|
max_task = (
|
|
|
|
|
db.query(sa_func.max(Task.id))
|
|
|
|
|
.filter(Task.milestone_id == milestone.id)
|
|
|
|
|
.scalar()
|
|
|
|
|
)
|
|
|
|
|
next_num = (max_task + 1) if max_task else 1
|
|
|
|
|
|
|
|
|
|
# Create one story task per Essential — all within the current transaction
|
|
|
|
|
generated_tasks = []
|
|
|
|
|
for essential in essentials:
|
|
|
|
|
etype = essential.type.value if hasattr(essential.type, "value") else essential.type
|
|
|
|
|
task_subtype = ESSENTIAL_TYPE_TO_SUBTYPE.get(etype, "feature")
|
|
|
|
|
task_code = f"{milestone_code}:T{next_num:05x}"
|
|
|
|
|
|
|
|
|
|
# Create feature story task
|
|
|
|
|
task = Task(
|
|
|
|
|
title=proposal.title,
|
|
|
|
|
description=proposal.description,
|
|
|
|
|
title=essential.title,
|
|
|
|
|
description=essential.description,
|
|
|
|
|
task_type="story",
|
|
|
|
|
task_subtype="feature",
|
|
|
|
|
task_subtype=task_subtype,
|
|
|
|
|
status=TaskStatus.PENDING,
|
|
|
|
|
priority=TaskPriority.MEDIUM,
|
|
|
|
|
project_id=project.id,
|
|
|
|
|
@@ -291,22 +329,36 @@ def accept_proposal(
|
|
|
|
|
task_code=task_code,
|
|
|
|
|
)
|
|
|
|
|
db.add(task)
|
|
|
|
|
db.flush() # get task.id
|
|
|
|
|
db.flush() # materialise task.id
|
|
|
|
|
|
|
|
|
|
# Update proposal
|
|
|
|
|
generated_tasks.append({
|
|
|
|
|
"task_id": task.id,
|
|
|
|
|
"task_code": task_code,
|
|
|
|
|
"task_type": "story",
|
|
|
|
|
"task_subtype": task_subtype,
|
|
|
|
|
"title": essential.title,
|
|
|
|
|
"essential_id": essential.id,
|
|
|
|
|
"essential_code": essential.essential_code,
|
|
|
|
|
})
|
|
|
|
|
next_num = task.id + 1 # use real id for next code to stay consistent
|
|
|
|
|
|
|
|
|
|
# Update proposal status (do NOT write feat_task_id — deprecated)
|
|
|
|
|
proposal.status = ProposalStatus.ACCEPTED
|
|
|
|
|
proposal.feat_task_id = str(task.id)
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(proposal)
|
|
|
|
|
|
|
|
|
|
log_activity(db, "accept", "proposal", proposal.id, user_id=current_user.id, details={
|
|
|
|
|
"milestone_id": milestone.id,
|
|
|
|
|
"generated_task_id": task.id,
|
|
|
|
|
"task_code": task_code,
|
|
|
|
|
"generated_tasks": [
|
|
|
|
|
{"task_id": t["task_id"], "task_code": t["task_code"], "essential_id": t["essential_id"]}
|
|
|
|
|
for t in generated_tasks
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return _serialize_proposal(db, proposal)
|
|
|
|
|
result = _serialize_proposal(db, proposal, include_essentials=True)
|
|
|
|
|
result["generated_tasks"] = generated_tasks
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RejectRequest(schemas.BaseModel):
|
|
|
|
|
|