diff --git a/app/main.py b/app/main.py index 664a950..09ef894 100644 --- a/app/main.py +++ b/app/main.py @@ -168,6 +168,45 @@ def _migrate_schema(): if _has_table(db, "issues"): db.execute(text("DROP TABLE issues")) + # --- Milestone status enum migration (old -> new) --- + if _has_table(db, "milestones"): + # Alter enum column to accept new values + db.execute(text( + "ALTER TABLE milestones MODIFY COLUMN status " + "ENUM('open','pending','deferred','progressing','freeze','undergoing','completed','closed') " + "DEFAULT 'open'" + )) + # Migrate old values + db.execute(text("UPDATE milestones SET status='open' WHERE status='pending'")) + db.execute(text("UPDATE milestones SET status='closed' WHERE status='deferred'")) + db.execute(text("UPDATE milestones SET status='undergoing' WHERE status='progressing'")) + # Shrink enum to new-only values + db.execute(text( + "ALTER TABLE milestones MODIFY COLUMN status " + "ENUM('open','freeze','undergoing','completed','closed') " + "DEFAULT 'open'" + )) + # Add started_at if missing + if not _has_column(db, "milestones", "started_at"): + db.execute(text("ALTER TABLE milestones ADD COLUMN started_at DATETIME NULL")) + + # --- Task status enum migration (old -> new) --- + if _has_table(db, "tasks"): + # Widen enum first + db.execute(text( + "ALTER TABLE tasks MODIFY COLUMN status " + "ENUM('open','pending','progressing','undergoing','completed','closed') " + "DEFAULT 'open'" + )) + # Migrate old values + db.execute(text("UPDATE tasks SET status='undergoing' WHERE status='progressing'")) + # Shrink enum to new-only values + db.execute(text( + "ALTER TABLE tasks MODIFY COLUMN status " + "ENUM('open','pending','undergoing','completed','closed') " + "DEFAULT 'open'" + )) + db.commit() except Exception as e: db.rollback() @@ -179,7 +218,7 @@ def _migrate_schema(): @app.on_event("startup") def startup(): from app.core.config import Base, engine, SessionLocal - from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting + from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting, propose Base.metadata.create_all(bind=engine) _migrate_schema() diff --git a/app/models/propose.py b/app/models/propose.py new file mode 100644 index 0000000..59e43cc --- /dev/null +++ b/app/models/propose.py @@ -0,0 +1,29 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum +from sqlalchemy.sql import func +from app.core.config import Base +import enum + + +class ProposeStatus(str, enum.Enum): + OPEN = "open" + ACCEPTED = "accepted" + REJECTED = "rejected" + + +class Propose(Base): + __tablename__ = "proposes" + + id = Column(Integer, primary_key=True, index=True) + propose_code = Column(String(64), nullable=True, unique=True, index=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + status = Column(Enum(ProposeStatus), default=ProposeStatus.OPEN) + + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + # Populated server-side after accept; links to the generated feature story task + feat_task_id = Column(String(64), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 071574b..102b4c3 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -240,6 +240,42 @@ class MilestoneResponse(MilestoneBase): from_attributes = True +# Propose schemas + +class ProposeStatusEnum(str, Enum): + OPEN = "open" + ACCEPTED = "accepted" + REJECTED = "rejected" + + +class ProposeBase(BaseModel): + title: str + description: Optional[str] = None + + +class ProposeCreate(ProposeBase): + project_id: Optional[int] = None + + +class ProposeUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + + +class ProposeResponse(ProposeBase): + id: int + propose_code: Optional[str] = None + status: ProposeStatusEnum + project_id: int + created_by_id: Optional[int] = None + feat_task_id: Optional[str] = None + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + # Paginated response from typing import Generic, TypeVar T = TypeVar("T")