From 5297711c77c63f877afdadabcd8b0f5c51cd0bb1 Mon Sep 17 00:00:00 2001 From: Zhi Date: Thu, 12 Mar 2026 22:26:24 +0000 Subject: [PATCH] refactor: split tasks/supports/meetings into separate tables --- app/api/routers/misc.py | 245 +++++++++++++++++++++------------------- app/models/meeting.py | 39 +++++++ app/models/support.py | 38 +++++++ app/models/task.py | 45 ++++++++ 4 files changed, 249 insertions(+), 118 deletions(-) create mode 100644 app/models/meeting.py create mode 100644 app/models/support.py create mode 100644 app/models/task.py diff --git a/app/api/routers/misc.py b/app/api/routers/misc.py index aed7579..042d3cc 100644 --- a/app/api/routers/misc.py +++ b/app/api/routers/misc.py @@ -17,6 +17,9 @@ from app.models import models from app.models.apikey import APIKey from app.models.activity import ActivityLog from app.models.milestone import Milestone as MilestoneModel +from app.models.task import Task, TaskStatus, TaskPriority +from app.models.support import Support, SupportStatus, SupportPriority +from app.models.meeting import Meeting, MeetingStatus, MeetingPriority from app.models.notification import Notification as NotificationModel from app.models.worklog import WorkLog from app.schemas import schemas @@ -487,6 +490,7 @@ def dashboard_stats(project_id: int = None, db: Session = Depends(get_db)): for p in ["low", "medium", "high", "critical"]} return {"total": total, "by_status": by_status, "by_type": by_type, "by_priority": by_priority} + # ============ Tasks ============ @router.get("/tasks/{project_code}/{milestone_id}", tags=["Tasks"]) @@ -495,34 +499,32 @@ def list_tasks(project_code: str, milestone_id: int, db: Session = Depends(get_d if not project: raise HTTPException(status_code=404, detail="Project not found") - issues = db.query(models.Issue).filter( - models.Issue.project_id == project.id, - models.Issue.milestone_id == milestone_id, - models.Issue.issue_type == "task" + tasks = db.query(Task).filter( + Task.project_id == project.id, + Task.milestone_id == milestone_id ).all() return [{ - "id": i.id, - "title": i.title, - "description": i.description, - "status": i.status.value if hasattr(i.status, 'value') else i.status, - "priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, - "task_code": i.task_code, - "task_status": i.task_status, - "estimated_effort": i.estimated_effort, - "estimated_working_time": str(i.estimated_working_time) if i.estimated_working_time else None, - "started_on": i.started_on, - "finished_on": i.finished_on, - "depend_on": i.depend_on, - "related_tasks": i.related_tasks, - "assignee_id": i.assignee_id, - "created_at": i.created_at, - } for i in issues] + "id": t.id, + "title": t.title, + "description": t.description, + "status": t.status.value if hasattr(t.status, "value") else t.status, + "priority": t.priority.value if hasattr(t.priority, "value") else t.priority, + "task_code": t.task_code, + "estimated_effort": t.estimated_effort, + "estimated_working_time": str(t.estimated_working_time) if t.estimated_working_time else None, + "started_on": t.started_on, + "finished_on": t.finished_on, + "depend_on": t.depend_on, + "related_tasks": t.related_tasks, + "assignee_id": t.assignee_id, + "created_at": t.created_at, + } for t in tasks] @router.post("/tasks/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["Tasks"]) def create_task(project_code: str, milestone_id: int, task_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): - from datetime import datetime, time + from datetime import datetime project = db.query(models.Project).filter(models.Project.project_code == project_code).first() if not project: @@ -532,11 +534,11 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi if not ms: 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 == "progressing": raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") - max_issue = db.query(models.Issue).filter(models.Issue.project_id == project.id).order_by(models.Issue.id.desc()).first() - next_id = (max_issue.id + 1) if max_issue else 1 + max_task = db.query(Task).filter(Task.project_id == project.id).order_by(Task.id.desc()).first() + next_id = (max_task.id + 1) if max_task else 1 task_code = f"i_{project_code}_{next_id:06x}" est_time = None @@ -546,33 +548,31 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi except: pass - issue = models.Issue( + task = Task( title=task_data.get("title"), description=task_data.get("description"), - issue_type="task", - status=models.IssueStatus.OPEN, - priority=models.IssuePriority.MEDIUM, + status=TaskStatus.OPEN, + priority=TaskPriority.MEDIUM, project_id=project.id, milestone_id=milestone_id, reporter_id=current_user.id, task_code=task_code, estimated_effort=task_data.get("estimated_effort"), estimated_working_time=est_time, - task_status="open", created_by_id=current_user.id, ) - db.add(issue) + db.add(task) db.commit() - db.refresh(issue) + db.refresh(task) return { - "id": issue.id, - "title": issue.title, - "description": issue.description, - "task_code": issue.task_code, - "status": issue.status.value if hasattr(issue.status, 'value') else issue.status, - "priority": issue.priority.value if hasattr(issue.priority, 'value') else issue.priority, - "created_at": issue.created_at, + "id": task.id, + "title": task.title, + "description": task.description, + "task_code": task.task_code, + "status": task.status.value, + "priority": task.priority.value, + "created_at": task.created_at, } @@ -582,31 +582,29 @@ def get_task(project_code: str, milestone_id: int, task_id: int, db: Session = D if not project: raise HTTPException(status_code=404, detail="Project not found") - issue = db.query(models.Issue).filter( - models.Issue.id == task_id, - models.Issue.project_id == project.id, - models.Issue.milestone_id == milestone_id, - models.Issue.issue_type == "task" + task = db.query(Task).filter( + Task.id == task_id, + Task.project_id == project.id, + Task.milestone_id == milestone_id ).first() - if not issue: + if not task: raise HTTPException(status_code=404, detail="Task not found") return { - "id": issue.id, - "title": issue.title, - "description": issue.description, - "status": issue.status.value if hasattr(issue.status, 'value') else issue.status, - "priority": issue.priority.value if hasattr(issue.priority, 'value') else issue.priority, - "task_code": issue.task_code, - "task_status": issue.task_status, - "estimated_effort": issue.estimated_effort, - "estimated_working_time": str(issue.estimated_working_time) if issue.estimated_working_time else None, - "started_on": issue.started_on, - "finished_on": issue.finished_on, - "depend_on": issue.depend_on, - "related_tasks": issue.related_tasks, - "assignee_id": issue.assignee_id, - "created_at": issue.created_at, + "id": task.id, + "title": task.title, + "description": task.description, + "status": task.status.value, + "priority": task.priority.value, + "task_code": task.task_code, + "estimated_effort": task.estimated_effort, + "estimated_working_time": str(task.estimated_working_time) if task.estimated_working_time else None, + "started_on": task.started_on, + "finished_on": task.finished_on, + "depend_on": task.depend_on, + "related_tasks": task.related_tasks, + "assignee_id": task.assignee_id, + "created_at": task.created_at, } @@ -618,36 +616,37 @@ def update_task(project_code: str, milestone_id: int, task_id: int, task_data: d if not project: raise HTTPException(status_code=404, detail="Project not found") - issue = db.query(models.Issue).filter( - models.Issue.id == task_id, - models.Issue.project_id == project.id, - models.Issue.milestone_id == milestone_id, - models.Issue.issue_type == "task" + task = db.query(Task).filter( + Task.id == task_id, + Task.project_id == project.id, + Task.milestone_id == milestone_id ).first() - if not issue: + if not task: raise HTTPException(status_code=404, detail="Task not found") if "title" in task_data: - issue.title = task_data["title"] + task.title = task_data["title"] if "description" in task_data: - issue.description = task_data["description"] - if "task_status" in task_data: - issue.task_status = task_data["task_status"] - if task_data["task_status"] == "progressing" and not issue.started_on: - issue.started_on = datetime.now() - if task_data["task_status"] == "closed" and not issue.finished_on: - issue.finished_on = datetime.now() - if "estimated_effort" in task_data: - issue.estimated_effort = task_data["estimated_effort"] - if "assignee_id" in task_data: - issue.assignee_id = task_data["assignee_id"] + task.description = task_data["description"] if "status" in task_data: - issue.status = models.IssueStatus[task_data["status"].upper()] if task_data["status"].upper() in [s.name for s in models.IssueStatus] else models.IssueStatus.OPEN + new_status = task_data["status"] + if new_status == "progressing" and not task.started_on: + task.started_on = datetime.now() + if new_status == "closed" and not task.finished_on: + task.finished_on = datetime.now() + task.status = TaskStatus[new_status.upper()] if new_status.upper() in [s.name for s in TaskStatus] else TaskStatus.OPEN + if "priority" in task_data: + task.priority = TaskPriority[task_data["priority"].upper()] if task_data["priority"].upper() in [s.name for s in TaskPriority] else TaskPriority.MEDIUM + if "estimated_effort" in task_data: + task.estimated_effort = task_data["estimated_effort"] + if "assignee_id" in task_data: + task.assignee_id = task_data["assignee_id"] db.commit() - db.refresh(issue) + db.refresh(task) - return issue + return task + # ============ Supports ============ @@ -657,20 +656,20 @@ def list_supports(project_code: str, milestone_id: int, db: Session = Depends(ge if not project: raise HTTPException(status_code=404, detail="Project not found") - issues = db.query(models.Issue).filter( - models.Issue.project_id == project.id, - models.Issue.milestone_id == milestone_id, - models.Issue.issue_type == "support" + supports = db.query(Support).filter( + Support.project_id == project.id, + Support.milestone_id == milestone_id ).all() return [{ - "id": i.id, - "title": i.title, - "description": i.description, - "status": i.status.value if hasattr(i.status, 'value') else i.status, - "priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, - "created_at": i.created_at, - } for i in issues] + "id": s.id, + "title": s.title, + "description": s.description, + "status": s.status.value, + "priority": s.priority.value, + "assignee_id": s.assignee_id, + "created_at": s.created_at, + } for s in supports] @router.post("/supports/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["Supports"]) @@ -683,23 +682,22 @@ def create_support(project_code: str, milestone_id: int, support_data: dict, db: if not ms: 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 == "progressing": raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") - issue = models.Issue( + support = Support( title=support_data.get("title"), description=support_data.get("description"), - issue_type="support", - status=models.IssueStatus.OPEN, - priority=models.IssuePriority.MEDIUM, + status=SupportStatus.OPEN, + priority=SupportPriority.MEDIUM, project_id=project.id, milestone_id=milestone_id, reporter_id=current_user.id, ) - db.add(issue) + db.add(support) db.commit() - db.refresh(issue) - return issue + db.refresh(support) + return support # ============ Meetings ============ @@ -710,24 +708,27 @@ def list_meetings(project_code: str, milestone_id: int, db: Session = Depends(ge if not project: raise HTTPException(status_code=404, detail="Project not found") - issues = db.query(models.Issue).filter( - models.Issue.project_id == project.id, - models.Issue.milestone_id == milestone_id, - models.Issue.issue_type == "meeting" + meetings = db.query(Meeting).filter( + Meeting.project_id == project.id, + Meeting.milestone_id == milestone_id ).all() return [{ - "id": i.id, - "title": i.title, - "description": i.description, - "status": i.status.value if hasattr(i.status, 'value') else i.status, - "priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, - "created_at": i.created_at, - } for i in issues] + "id": m.id, + "title": m.title, + "description": m.description, + "status": m.status.value, + "priority": m.priority.value, + "scheduled_at": m.scheduled_at, + "duration_minutes": m.duration_minutes, + "created_at": m.created_at, + } for m in meetings] @router.post("/meetings/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["Meetings"]) def create_meeting(project_code: str, milestone_id: int, meeting_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)): + from datetime import datetime + project = db.query(models.Project).filter(models.Project.project_code == project_code).first() if not project: raise HTTPException(status_code=404, detail="Project not found") @@ -736,20 +737,28 @@ def create_meeting(project_code: str, milestone_id: int, meeting_data: dict, db: if not ms: 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 == "progressing": raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress") - issue = models.Issue( + scheduled_at = None + if meeting_data.get("scheduled_at"): + try: + scheduled_at = datetime.fromisoformat(meeting_data["scheduled_at"].replace("Z", "+00:00")) + except: + pass + + meeting = Meeting( title=meeting_data.get("title"), description=meeting_data.get("description"), - issue_type="meeting", - status=models.IssueStatus.OPEN, - priority=models.IssuePriority.MEDIUM, + status=MeetingStatus.SCHEDULED, + priority=MeetingPriority.MEDIUM, project_id=project.id, milestone_id=milestone_id, reporter_id=current_user.id, + scheduled_at=scheduled_at, + duration_minutes=meeting_data.get("duration_minutes"), ) - db.add(issue) + db.add(meeting) db.commit() - db.refresh(issue) - return issue + db.refresh(meeting) + return meeting diff --git a/app/models/meeting.py b/app/models/meeting.py new file mode 100644 index 0000000..078d94a --- /dev/null +++ b/app/models/meeting.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.core.config import Base +import enum + +class MeetingStatus(str, enum.Enum): + SCHEDULED = "scheduled" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + +class MeetingPriority(str, enum.Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + +class Meeting(Base): + __tablename__ = "meetings" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + status = Column(Enum(MeetingStatus), default=MeetingStatus.SCHEDULED) + priority = Column(Enum(MeetingPriority), default=MeetingPriority.MEDIUM) + + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + milestone_id = Column(Integer, ForeignKey("milestones.id"), nullable=False) + reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False) + + scheduled_at = Column(DateTime(timezone=True), nullable=True) + duration_minutes = Column(Integer, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + + reporter = relationship("User", foreign_keys=[reporter_id]) diff --git a/app/models/support.py b/app/models/support.py new file mode 100644 index 0000000..7b02421 --- /dev/null +++ b/app/models/support.py @@ -0,0 +1,38 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.core.config import Base +import enum + +class SupportStatus(str, enum.Enum): + OPEN = "open" + IN_PROGRESS = "in_progress" + RESOLVED = "resolved" + CLOSED = "closed" + +class SupportPriority(str, enum.Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + +class Support(Base): + __tablename__ = "supports" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + status = Column(Enum(SupportStatus), default=SupportStatus.OPEN) + priority = Column(Enum(SupportPriority), default=SupportPriority.MEDIUM) + + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + milestone_id = Column(Integer, ForeignKey("milestones.id"), nullable=False) + reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False) + assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + + reporter = relationship("User", foreign_keys=[reporter_id]) + assignee = relationship("User", foreign_keys=[assignee_id]) diff --git a/app/models/task.py b/app/models/task.py new file mode 100644 index 0000000..acf5753 --- /dev/null +++ b/app/models/task.py @@ -0,0 +1,45 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Time +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.core.config import Base +import enum + +class TaskStatus(str, enum.Enum): + OPEN = "open" + PENDING = "pending" + PROGRESSING = "progressing" + CLOSED = "closed" + +class TaskPriority(str, enum.Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + +class Task(Base): + __tablename__ = "tasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + status = Column(Enum(TaskStatus), default=TaskStatus.OPEN) + priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM) + + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + milestone_id = Column(Integer, ForeignKey("milestones.id"), nullable=False) + reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False) + assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True) + created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + task_code = Column(String(64), nullable=True, unique=True, index=True) + depend_on = Column(Text, nullable=True) + estimated_effort = Column(Integer, nullable=True) + estimated_working_time = Column(Time, nullable=True) + started_on = Column(DateTime(timezone=True), nullable=True) + finished_on = Column(DateTime(timezone=True), nullable=True) + related_tasks = Column(Text, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + +