refactor: update milestone/task status enums to new state machine values

Milestone: open/freeze/undergoing/completed/closed (was open/pending/deferred/progressing/closed)
Task: open/pending/undergoing/completed/closed (was open/pending/progressing/closed)

- Add MilestoneStatusEnum to schemas with typed validation
- Add started_at field to Milestone model
- Update all router/CLI references from progressing->undergoing
- Add completed status handling in task transition logic
This commit is contained in:
zhi
2026-03-17 00:04:29 +00:00
parent 9e14df921e
commit 9e22c97ae8
8 changed files with 38 additions and 23 deletions

View File

@@ -111,8 +111,8 @@ def create_milestone_task(project_id: int, milestone_id: int, task_data: schemas
if not milestone: if not milestone:
raise HTTPException(status_code=404, detail="Milestone not found") raise HTTPException(status_code=404, detail="Milestone not found")
if milestone.status and hasattr(milestone.status, 'value') and milestone.status.value == "progressing": if milestone.status and hasattr(milestone.status, 'value') and milestone.status.value == "undergoing":
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is undergoing")
# Generate task_code # Generate task_code
milestone_code = milestone.milestone_code or f"m{milestone.id}" milestone_code = milestone.milestone_code or f"m{milestone.id}"

View File

@@ -425,8 +425,8 @@ def create_milestone_task(project_code: str, milestone_id: int, task_data: dict,
if not ms: if not ms:
raise HTTPException(status_code=404, detail="Milestone not found") raise HTTPException(status_code=404, detail="Milestone not found")
if ms.status and hasattr(ms.status, "value") and ms.status.value == "progressing": if ms.status and hasattr(ms.status, "value") and ms.status.value == "undergoing":
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is undergoing")
milestone_code = ms.milestone_code or f"m{ms.id}" milestone_code = ms.milestone_code or f"m{ms.id}"
max_task = db.query(Task).filter(Task.milestone_id == ms.id).order_by(Task.id.desc()).first() max_task = db.query(Task).filter(Task.milestone_id == ms.id).order_by(Task.id.desc()).first()
@@ -504,8 +504,8 @@ def create_support(project_code: str, milestone_id: int, support_data: dict, db:
if not ms: if not ms:
raise HTTPException(status_code=404, detail="Milestone not found") raise HTTPException(status_code=404, detail="Milestone not found")
if ms.status and hasattr(ms.status, "value") and ms.status.value == "progressing": if ms.status and hasattr(ms.status, "value") and ms.status.value == "undergoing":
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is undergoing")
milestone_code = ms.milestone_code or f"m{ms.id}" milestone_code = ms.milestone_code or f"m{ms.id}"
max_support = db.query(Support).filter(Support.milestone_id == milestone_id).order_by(Support.id.desc()).first() max_support = db.query(Support).filter(Support.milestone_id == milestone_id).order_by(Support.id.desc()).first()
@@ -563,8 +563,8 @@ def create_meeting(project_code: str, milestone_id: int, meeting_data: dict, db:
if not ms: if not ms:
raise HTTPException(status_code=404, detail="Milestone not found") raise HTTPException(status_code=404, detail="Milestone not found")
if ms.status and hasattr(ms.status, "value") and ms.status.value == "progressing": if ms.status and hasattr(ms.status, "value") and ms.status.value == "undergoing":
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is undergoing")
milestone_code = ms.milestone_code or f"m{ms.id}" milestone_code = ms.milestone_code or f"m{ms.id}"
max_meeting = db.query(Meeting).filter(Meeting.milestone_id == milestone_id).order_by(Meeting.id.desc()).first() max_meeting = db.query(Meeting).filter(Meeting.milestone_id == milestone_id).order_by(Meeting.id.desc()).first()

View File

@@ -167,9 +167,9 @@ def update_task(task_id: int, task_update: schemas.TaskUpdate, db: Session = Dep
update_data = task_update.model_dump(exclude_unset=True) update_data = task_update.model_dump(exclude_unset=True)
if "status" in update_data: if "status" in update_data:
new_status = update_data["status"] new_status = update_data["status"]
if new_status == "progressing" and not task.started_on: if new_status == "undergoing" and not task.started_on:
task.started_on = datetime.utcnow() task.started_on = datetime.utcnow()
if new_status == "closed" and not task.finished_on: if new_status in ("closed", "completed") and not task.finished_on:
task.finished_on = datetime.utcnow() task.finished_on = datetime.utcnow()
for field, value in update_data.items(): for field, value in update_data.items():
@@ -202,9 +202,9 @@ def transition_task(task_id: int, new_status: str, bg: BackgroundTasks, db: Sess
if not task: if not task:
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
old_status = task.status.value if hasattr(task.status, 'value') else task.status old_status = task.status.value if hasattr(task.status, 'value') else task.status
if new_status == "progressing" and not task.started_on: if new_status == "undergoing" and not task.started_on:
task.started_on = datetime.utcnow() task.started_on = datetime.utcnow()
if new_status == "closed" and not task.finished_on: if new_status in ("closed", "completed") and not task.finished_on:
task.finished_on = datetime.utcnow() task.finished_on = datetime.utcnow()
task.status = new_status task.status = new_status
db.commit() db.commit()

View File

@@ -6,9 +6,9 @@ import enum
class MilestoneStatus(str, enum.Enum): class MilestoneStatus(str, enum.Enum):
OPEN = "open" OPEN = "open"
PENDING = "pending" FREEZE = "freeze"
DEFERRED = "deferred" UNDERGOING = "undergoing"
PROGRESSING = "progressing" COMPLETED = "completed"
CLOSED = "closed" CLOSED = "closed"
class Milestone(Base): class Milestone(Base):
@@ -25,6 +25,7 @@ class Milestone(Base):
depend_on_tasks = Column(Text, nullable=True) depend_on_tasks = Column(Text, nullable=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True) created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
started_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())

View File

@@ -21,7 +21,8 @@ class TaskType(str, enum.Enum):
class TaskStatus(str, enum.Enum): class TaskStatus(str, enum.Enum):
OPEN = "open" OPEN = "open"
PENDING = "pending" PENDING = "pending"
PROGRESSING = "progressing" UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed" CLOSED = "closed"

View File

@@ -7,7 +7,8 @@ import enum
class TaskStatus(str, enum.Enum): class TaskStatus(str, enum.Enum):
OPEN = "open" OPEN = "open"
PENDING = "pending" PENDING = "pending"
PROGRESSING = "progressing" UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed" CLOSED = "closed"
class TaskPriority(str, enum.Enum): class TaskPriority(str, enum.Enum):

View File

@@ -18,7 +18,8 @@ class TaskTypeEnum(str, Enum):
class TaskStatusEnum(str, Enum): class TaskStatusEnum(str, Enum):
OPEN = "open" OPEN = "open"
PENDING = "pending" PENDING = "pending"
PROGRESSING = "progressing" UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed" CLOSED = "closed"
@@ -193,11 +194,19 @@ class ProjectMemberResponse(BaseModel):
from_attributes = True from_attributes = True
class MilestoneStatusEnum(str, Enum):
OPEN = "open"
FREEZE = "freeze"
UNDERGOING = "undergoing"
COMPLETED = "completed"
CLOSED = "closed"
# Milestone schemas # Milestone schemas
class MilestoneBase(BaseModel): class MilestoneBase(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
status: Optional[str] = "open" status: Optional[MilestoneStatusEnum] = MilestoneStatusEnum.OPEN
due_date: Optional[datetime] = None due_date: Optional[datetime] = None
planned_release_date: Optional[datetime] = None planned_release_date: Optional[datetime] = None
depend_on_milestones: Optional[List[str]] = None depend_on_milestones: Optional[List[str]] = None
@@ -212,7 +221,7 @@ class MilestoneCreate(MilestoneBase):
class MilestoneUpdate(BaseModel): class MilestoneUpdate(BaseModel):
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
status: Optional[str] = None status: Optional[MilestoneStatusEnum] = None
due_date: Optional[datetime] = None due_date: Optional[datetime] = None
planned_release_date: Optional[datetime] = None planned_release_date: Optional[datetime] = None
depend_on_milestones: Optional[List[str]] = None depend_on_milestones: Optional[List[str]] = None
@@ -223,6 +232,7 @@ class MilestoneResponse(MilestoneBase):
id: int id: int
project_id: int project_id: int
created_by_id: Optional[int] = None created_by_id: Optional[int] = None
started_at: Optional[datetime] = None
created_at: datetime created_at: datetime
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None

8
cli.py
View File

@@ -16,7 +16,9 @@ TOKEN = os.environ.get("HARBORFORGE_TOKEN", "")
STATUS_ICON = { STATUS_ICON = {
"open": "🟢", "open": "🟢",
"pending": "🟡", "pending": "🟡",
"progressing": "🔵", "freeze": "🧊",
"undergoing": "🔵",
"completed": "",
"closed": "", "closed": "",
} }
TYPE_ICON = { TYPE_ICON = {
@@ -241,7 +243,7 @@ def main():
p_tasks = sub.add_parser("tasks", aliases=["issues"], help="List tasks") p_tasks = sub.add_parser("tasks", aliases=["issues"], help="List tasks")
p_tasks.add_argument("--project", "-p", type=int) p_tasks.add_argument("--project", "-p", type=int)
p_tasks.add_argument("--type", "-t", choices=["task", "story", "test", "resolution", "issue", "maintenance", "research", "review"]) p_tasks.add_argument("--type", "-t", choices=["task", "story", "test", "resolution", "issue", "maintenance", "research", "review"])
p_tasks.add_argument("--status", "-s", choices=["open", "pending", "progressing", "closed"]) p_tasks.add_argument("--status", "-s", choices=["open", "pending", "undergoing", "completed", "closed"])
p_create = sub.add_parser("create-task", aliases=["create-issue"], help="Create a task") p_create = sub.add_parser("create-task", aliases=["create-issue"], help="Create a task")
p_create.add_argument("title") p_create.add_argument("title")
@@ -268,7 +270,7 @@ def main():
p_trans = sub.add_parser("transition", help="Transition task status") p_trans = sub.add_parser("transition", help="Transition task status")
p_trans.add_argument("task_id", type=int) p_trans.add_argument("task_id", type=int)
p_trans.add_argument("status", choices=["open", "pending", "progressing", "closed"]) p_trans.add_argument("status", choices=["open", "pending", "undergoing", "completed", "closed"])
p_stats = sub.add_parser("stats", help="Dashboard stats") p_stats = sub.add_parser("stats", help="Dashboard stats")
p_stats.add_argument("--project", "-p", type=int) p_stats.add_argument("--project", "-p", type=int)