HarborForge.Backend: dev-2026-03-29 -> main #13
@@ -63,6 +63,26 @@ def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials:
|
||||
.all()
|
||||
)
|
||||
result["essentials"] = [_serialize_essential(e) for e in essentials]
|
||||
|
||||
# BE-PR-008: include tasks generated from this Proposal via Accept
|
||||
gen_tasks = (
|
||||
db.query(Task)
|
||||
.filter(Task.source_proposal_id == proposal.id)
|
||||
.order_by(Task.id.asc())
|
||||
.all()
|
||||
)
|
||||
result["generated_tasks"] = [
|
||||
{
|
||||
"task_id": t.id,
|
||||
"task_code": t.task_code,
|
||||
"task_type": t.task_type or "story",
|
||||
"task_subtype": t.task_subtype,
|
||||
"title": t.title,
|
||||
"status": t.status.value if hasattr(t.status, "value") else t.status,
|
||||
"source_essential_id": t.source_essential_id,
|
||||
}
|
||||
for t in gen_tasks
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
@@ -327,6 +347,9 @@ def accept_proposal(
|
||||
reporter_id=proposal.created_by_id or current_user.id,
|
||||
created_by_id=proposal.created_by_id or current_user.id,
|
||||
task_code=task_code,
|
||||
# BE-PR-008: track which Proposal/Essential generated this task
|
||||
source_proposal_id=proposal.id,
|
||||
source_essential_id=essential.id,
|
||||
)
|
||||
db.add(task)
|
||||
db.flush() # materialise task.id
|
||||
|
||||
@@ -78,6 +78,14 @@ class Proposal(Base):
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
# BE-PR-008: reverse lookup — story tasks generated from this Proposal
|
||||
generated_tasks = relationship(
|
||||
"Task",
|
||||
foreign_keys="Task.source_proposal_id",
|
||||
lazy="select",
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
# ---- convenience alias ------------------------------------------------
|
||||
@hybrid_property
|
||||
def proposal_code(self) -> str | None:
|
||||
|
||||
@@ -37,6 +37,17 @@ class Task(Base):
|
||||
assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Proposal Accept tracking (BE-PR-008)
|
||||
# When a task is generated from Proposal Accept, these record the source.
|
||||
source_proposal_id = Column(
|
||||
Integer, ForeignKey("proposes.id"), nullable=True,
|
||||
comment="Proposal that generated this task via accept (NULL if manually created)",
|
||||
)
|
||||
source_essential_id = Column(
|
||||
Integer, ForeignKey("essentials.id"), nullable=True,
|
||||
comment="Essential that generated this task via accept (NULL if manually created)",
|
||||
)
|
||||
|
||||
# Tags (comma-separated)
|
||||
tags = Column(String(500), nullable=True)
|
||||
|
||||
|
||||
@@ -93,6 +93,9 @@ class TaskResponse(TaskBase):
|
||||
resolution_summary: Optional[str] = None
|
||||
positions: Optional[str] = None
|
||||
pending_matters: Optional[str] = None
|
||||
# BE-PR-008: Proposal Accept tracking
|
||||
source_proposal_id: Optional[int] = None
|
||||
source_essential_id: Optional[int] = None
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@@ -344,10 +347,22 @@ class EssentialResponse(EssentialBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GeneratedTaskBrief(BaseModel):
|
||||
"""Brief info about a story task generated from Proposal Accept."""
|
||||
task_id: int
|
||||
task_code: Optional[str] = None
|
||||
task_type: str
|
||||
task_subtype: Optional[str] = None
|
||||
title: str
|
||||
status: Optional[str] = None
|
||||
source_essential_id: Optional[int] = None
|
||||
|
||||
|
||||
class ProposalDetailResponse(ProposalResponse):
|
||||
"""Extended Proposal response that embeds its Essential list."""
|
||||
"""Extended Proposal response that embeds its Essential list and generated tasks."""
|
||||
|
||||
essentials: List[EssentialResponse] = []
|
||||
generated_tasks: List[GeneratedTaskBrief] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
Reference in New Issue
Block a user