From 119a679e7f546dae0dd091d21759eb2b1f53adfb Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 29 Mar 2026 16:02:18 +0000 Subject: [PATCH] BE-PR-002: Proposal model naming & field adjustments - Add comprehensive docstring to Proposal model documenting all relationships - Add column comments for all fields (title, description, status, project_id, etc.) - Mark feat_task_id as DEPRECATED (will be replaced by Essential->task mapping in BE-PR-008) - Add proposal_code hybrid property as preferred alias for DB column propose_code - Update ProposalResponse schema to include proposal_code alongside propose_code - Update serializer to emit both proposal_code and propose_code for backward compat - No DB migration needed -- only Python-level changes --- app/api/routers/proposals.py | 6 ++-- app/models/proposal.py | 67 +++++++++++++++++++++++++++++++----- app/schemas/schemas.py | 5 +-- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/app/api/routers/proposals.py b/app/api/routers/proposals.py index 7499955..8ff7cd2 100644 --- a/app/api/routers/proposals.py +++ b/app/api/routers/proposals.py @@ -24,16 +24,18 @@ router = APIRouter(prefix="/projects/{project_id}/proposals", tags=["Proposals"] def _serialize_proposal(db: Session, proposal: Proposal) -> 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 { "id": proposal.id, "title": proposal.title, "description": proposal.description, - "propose_code": proposal.propose_code, + "proposal_code": code, # preferred name + "propose_code": code, # backward compat "status": proposal.status.value if hasattr(proposal.status, "value") else proposal.status, "project_id": proposal.project_id, "created_by_id": proposal.created_by_id, "created_by_username": creator.username if creator else None, - "feat_task_id": proposal.feat_task_id, + "feat_task_id": proposal.feat_task_id, # DEPRECATED — read-only compat "created_at": proposal.created_at, "updated_at": proposal.updated_at, } diff --git a/app/models/proposal.py b/app/models/proposal.py index 3e12a66..d3730f7 100644 --- a/app/models/proposal.py +++ b/app/models/proposal.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.sql import func from app.core.config import Base import enum @@ -11,23 +12,73 @@ class ProposalStatus(str, enum.Enum): class Proposal(Base): + """Proposal model — a suggested scope of work under a Project. + + After BE-PR-001 rename: Python class is ``Proposal``, DB table stays ``proposes`` + for backward compatibility. + + Relationships + ------------- + - ``project_id`` — FK to ``projects.id``; every Proposal belongs to exactly + one Project. + - ``created_by_id`` — FK to ``users.id``; the user who authored the Proposal. + Nullable for legacy rows created before tracking was added. + - ``feat_task_id`` — **DEPRECATED**. Previously stored the single generated + ``story/feature`` task id on accept. Will be replaced by + the Essential → story-task mapping (see BE-PR-008). + Kept in the DB column for read-only backward compat; new + code MUST NOT write to this field. + """ + __tablename__ = "proposes" # keep DB table name for compat id = Column(Integer, primary_key=True, index=True) - propose_code = Column(String(64), nullable=True, unique=True, index=True) # keep column name for DB compat - title = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - status = Column(Enum(ProposalStatus, values_callable=lambda x: [e.value for e in x]), default=ProposalStatus.OPEN) - project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) - created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True) + # DB column stays ``propose_code`` for migration safety; use the + # ``proposal_code`` hybrid property in new Python code. + propose_code = Column( + String(64), nullable=True, unique=True, index=True, + comment="Unique human-readable code, e.g. PROJ:P00001", + ) - # Populated server-side after accept; links to the generated feature story task - feat_task_id = Column(String(64), nullable=True) + title = Column(String(255), nullable=False, comment="Short title of the proposal") + description = Column(Text, nullable=True, comment="Detailed description / rationale") + + status = Column( + Enum(ProposalStatus, values_callable=lambda x: [e.value for e in x]), + default=ProposalStatus.OPEN, + comment="Lifecycle status: open → accepted | rejected", + ) + + project_id = Column( + Integer, ForeignKey("projects.id"), nullable=False, + comment="Owning project", + ) + created_by_id = Column( + Integer, ForeignKey("users.id"), nullable=True, + comment="Author of the proposal (nullable for legacy rows)", + ) + + # DEPRECATED — see class docstring. Read-only; will be removed once + # Essential-based accept (BE-PR-007 / BE-PR-008) is fully rolled out. + feat_task_id = Column( + String(64), nullable=True, + comment="DEPRECATED: id of the single story/feature task generated on old-style accept", + ) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + # ---- convenience alias ------------------------------------------------ + @hybrid_property + def proposal_code(self) -> str | None: + """Preferred accessor — maps to the DB column ``propose_code``.""" + return self.propose_code + + @proposal_code.setter # type: ignore[no-redef] + def proposal_code(self, value: str | None) -> None: + self.propose_code = value + # Backward-compatible aliases ProposeStatus = ProposalStatus diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 6fc70f0..6d84533 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -288,12 +288,13 @@ class ProposalUpdate(BaseModel): class ProposalResponse(ProposalBase): id: int - propose_code: Optional[str] = None # DB column name kept for compat + proposal_code: Optional[str] = None # preferred name + propose_code: Optional[str] = None # backward compat alias (same value) status: ProposalStatusEnum project_id: int created_by_id: Optional[int] = None created_by_username: Optional[str] = None - feat_task_id: Optional[str] = None + feat_task_id: Optional[str] = None # DEPRECATED — will be removed after BE-PR-008 created_at: datetime updated_at: Optional[datetime] = None