feat: add Propose model/schema + DB enum migration scripts
- New Propose model (app/models/propose.py) with status enum (open/accepted/rejected) - New Propose schemas (ProposeCreate/Update/Response) in schemas.py - MySQL enum migration in main.py for milestone/task status columns - milestone: pending→open, deferred→closed, progressing→undergoing - task: progressing→undergoing - Import propose model in startup for create_all - Add started_at column migration for milestones
This commit is contained in:
41
app/main.py
41
app/main.py
@@ -168,6 +168,45 @@ def _migrate_schema():
|
|||||||
if _has_table(db, "issues"):
|
if _has_table(db, "issues"):
|
||||||
db.execute(text("DROP TABLE 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()
|
db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
@@ -179,7 +218,7 @@ def _migrate_schema():
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def startup():
|
def startup():
|
||||||
from app.core.config import Base, engine, SessionLocal
|
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)
|
Base.metadata.create_all(bind=engine)
|
||||||
_migrate_schema()
|
_migrate_schema()
|
||||||
|
|
||||||
|
|||||||
29
app/models/propose.py
Normal file
29
app/models/propose.py
Normal file
@@ -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())
|
||||||
@@ -240,6 +240,42 @@ class MilestoneResponse(MilestoneBase):
|
|||||||
from_attributes = True
|
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
|
# Paginated response
|
||||||
from typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|||||||
Reference in New Issue
Block a user