refactor: split tasks/supports/meetings into separate tables

This commit is contained in:
Zhi
2026-03-12 22:26:24 +00:00
parent 724be87a04
commit 5297711c77
4 changed files with 249 additions and 118 deletions

View File

@@ -17,6 +17,9 @@ from app.models import models
from app.models.apikey import APIKey from app.models.apikey import APIKey
from app.models.activity import ActivityLog from app.models.activity import ActivityLog
from app.models.milestone import Milestone as MilestoneModel 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.notification import Notification as NotificationModel
from app.models.worklog import WorkLog from app.models.worklog import WorkLog
from app.schemas import schemas 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"]} for p in ["low", "medium", "high", "critical"]}
return {"total": total, "by_status": by_status, "by_type": by_type, "by_priority": by_priority} return {"total": total, "by_status": by_status, "by_type": by_type, "by_priority": by_priority}
# ============ Tasks ============ # ============ Tasks ============
@router.get("/tasks/{project_code}/{milestone_id}", tags=["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: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
issues = db.query(models.Issue).filter( tasks = db.query(Task).filter(
models.Issue.project_id == project.id, Task.project_id == project.id,
models.Issue.milestone_id == milestone_id, Task.milestone_id == milestone_id
models.Issue.issue_type == "task"
).all() ).all()
return [{ return [{
"id": i.id, "id": t.id,
"title": i.title, "title": t.title,
"description": i.description, "description": t.description,
"status": i.status.value if hasattr(i.status, 'value') else i.status, "status": t.status.value if hasattr(t.status, "value") else t.status,
"priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, "priority": t.priority.value if hasattr(t.priority, "value") else t.priority,
"task_code": i.task_code, "task_code": t.task_code,
"task_status": i.task_status, "estimated_effort": t.estimated_effort,
"estimated_effort": i.estimated_effort, "estimated_working_time": str(t.estimated_working_time) if t.estimated_working_time else None,
"estimated_working_time": str(i.estimated_working_time) if i.estimated_working_time else None, "started_on": t.started_on,
"started_on": i.started_on, "finished_on": t.finished_on,
"finished_on": i.finished_on, "depend_on": t.depend_on,
"depend_on": i.depend_on, "related_tasks": t.related_tasks,
"related_tasks": i.related_tasks, "assignee_id": t.assignee_id,
"assignee_id": i.assignee_id, "created_at": t.created_at,
"created_at": i.created_at, } for t in tasks]
} for i in issues]
@router.post("/tasks/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["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)): 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() project = db.query(models.Project).filter(models.Project.project_code == project_code).first()
if not project: if not project:
@@ -532,11 +534,11 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi
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 == "progressing":
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 in_progress")
max_issue = db.query(models.Issue).filter(models.Issue.project_id == project.id).order_by(models.Issue.id.desc()).first() max_task = db.query(Task).filter(Task.project_id == project.id).order_by(Task.id.desc()).first()
next_id = (max_issue.id + 1) if max_issue else 1 next_id = (max_task.id + 1) if max_task else 1
task_code = f"i_{project_code}_{next_id:06x}" task_code = f"i_{project_code}_{next_id:06x}"
est_time = None est_time = None
@@ -546,33 +548,31 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi
except: except:
pass pass
issue = models.Issue( task = Task(
title=task_data.get("title"), title=task_data.get("title"),
description=task_data.get("description"), description=task_data.get("description"),
issue_type="task", status=TaskStatus.OPEN,
status=models.IssueStatus.OPEN, priority=TaskPriority.MEDIUM,
priority=models.IssuePriority.MEDIUM,
project_id=project.id, project_id=project.id,
milestone_id=milestone_id, milestone_id=milestone_id,
reporter_id=current_user.id, reporter_id=current_user.id,
task_code=task_code, task_code=task_code,
estimated_effort=task_data.get("estimated_effort"), estimated_effort=task_data.get("estimated_effort"),
estimated_working_time=est_time, estimated_working_time=est_time,
task_status="open",
created_by_id=current_user.id, created_by_id=current_user.id,
) )
db.add(issue) db.add(task)
db.commit() db.commit()
db.refresh(issue) db.refresh(task)
return { return {
"id": issue.id, "id": task.id,
"title": issue.title, "title": task.title,
"description": issue.description, "description": task.description,
"task_code": issue.task_code, "task_code": task.task_code,
"status": issue.status.value if hasattr(issue.status, 'value') else issue.status, "status": task.status.value,
"priority": issue.priority.value if hasattr(issue.priority, 'value') else issue.priority, "priority": task.priority.value,
"created_at": issue.created_at, "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: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
issue = db.query(models.Issue).filter( task = db.query(Task).filter(
models.Issue.id == task_id, Task.id == task_id,
models.Issue.project_id == project.id, Task.project_id == project.id,
models.Issue.milestone_id == milestone_id, Task.milestone_id == milestone_id
models.Issue.issue_type == "task"
).first() ).first()
if not issue: if not task:
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
return { return {
"id": issue.id, "id": task.id,
"title": issue.title, "title": task.title,
"description": issue.description, "description": task.description,
"status": issue.status.value if hasattr(issue.status, 'value') else issue.status, "status": task.status.value,
"priority": issue.priority.value if hasattr(issue.priority, 'value') else issue.priority, "priority": task.priority.value,
"task_code": issue.task_code, "task_code": task.task_code,
"task_status": issue.task_status, "estimated_effort": task.estimated_effort,
"estimated_effort": issue.estimated_effort, "estimated_working_time": str(task.estimated_working_time) if task.estimated_working_time else None,
"estimated_working_time": str(issue.estimated_working_time) if issue.estimated_working_time else None, "started_on": task.started_on,
"started_on": issue.started_on, "finished_on": task.finished_on,
"finished_on": issue.finished_on, "depend_on": task.depend_on,
"depend_on": issue.depend_on, "related_tasks": task.related_tasks,
"related_tasks": issue.related_tasks, "assignee_id": task.assignee_id,
"assignee_id": issue.assignee_id, "created_at": task.created_at,
"created_at": issue.created_at,
} }
@@ -618,36 +616,37 @@ def update_task(project_code: str, milestone_id: int, task_id: int, task_data: d
if not project: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
issue = db.query(models.Issue).filter( task = db.query(Task).filter(
models.Issue.id == task_id, Task.id == task_id,
models.Issue.project_id == project.id, Task.project_id == project.id,
models.Issue.milestone_id == milestone_id, Task.milestone_id == milestone_id
models.Issue.issue_type == "task"
).first() ).first()
if not issue: if not task:
raise HTTPException(status_code=404, detail="Task not found") raise HTTPException(status_code=404, detail="Task not found")
if "title" in task_data: if "title" in task_data:
issue.title = task_data["title"] task.title = task_data["title"]
if "description" in task_data: if "description" in task_data:
issue.description = task_data["description"] task.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"]
if "status" in task_data: 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.commit()
db.refresh(issue) db.refresh(task)
return issue return task
# ============ Supports ============ # ============ Supports ============
@@ -657,20 +656,20 @@ def list_supports(project_code: str, milestone_id: int, db: Session = Depends(ge
if not project: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
issues = db.query(models.Issue).filter( supports = db.query(Support).filter(
models.Issue.project_id == project.id, Support.project_id == project.id,
models.Issue.milestone_id == milestone_id, Support.milestone_id == milestone_id
models.Issue.issue_type == "support"
).all() ).all()
return [{ return [{
"id": i.id, "id": s.id,
"title": i.title, "title": s.title,
"description": i.description, "description": s.description,
"status": i.status.value if hasattr(i.status, 'value') else i.status, "status": s.status.value,
"priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, "priority": s.priority.value,
"created_at": i.created_at, "assignee_id": s.assignee_id,
} for i in issues] "created_at": s.created_at,
} for s in supports]
@router.post("/supports/{project_code}/{milestone_id}", status_code=status.HTTP_201_CREATED, tags=["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: 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 == "progressing":
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 in_progress")
issue = models.Issue( support = Support(
title=support_data.get("title"), title=support_data.get("title"),
description=support_data.get("description"), description=support_data.get("description"),
issue_type="support", status=SupportStatus.OPEN,
status=models.IssueStatus.OPEN, priority=SupportPriority.MEDIUM,
priority=models.IssuePriority.MEDIUM,
project_id=project.id, project_id=project.id,
milestone_id=milestone_id, milestone_id=milestone_id,
reporter_id=current_user.id, reporter_id=current_user.id,
) )
db.add(issue) db.add(support)
db.commit() db.commit()
db.refresh(issue) db.refresh(support)
return issue return support
# ============ Meetings ============ # ============ Meetings ============
@@ -710,24 +708,27 @@ def list_meetings(project_code: str, milestone_id: int, db: Session = Depends(ge
if not project: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
issues = db.query(models.Issue).filter( meetings = db.query(Meeting).filter(
models.Issue.project_id == project.id, Meeting.project_id == project.id,
models.Issue.milestone_id == milestone_id, Meeting.milestone_id == milestone_id
models.Issue.issue_type == "meeting"
).all() ).all()
return [{ return [{
"id": i.id, "id": m.id,
"title": i.title, "title": m.title,
"description": i.description, "description": m.description,
"status": i.status.value if hasattr(i.status, 'value') else i.status, "status": m.status.value,
"priority": i.priority.value if hasattr(i.priority, 'value') else i.priority, "priority": m.priority.value,
"created_at": i.created_at, "scheduled_at": m.scheduled_at,
} for i in issues] "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"]) @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)): 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() project = db.query(models.Project).filter(models.Project.project_code == project_code).first()
if not project: if not project:
raise HTTPException(status_code=404, detail="Project not found") 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: 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 == "progressing":
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 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"), title=meeting_data.get("title"),
description=meeting_data.get("description"), description=meeting_data.get("description"),
issue_type="meeting", status=MeetingStatus.SCHEDULED,
status=models.IssueStatus.OPEN, priority=MeetingPriority.MEDIUM,
priority=models.IssuePriority.MEDIUM,
project_id=project.id, project_id=project.id,
milestone_id=milestone_id, milestone_id=milestone_id,
reporter_id=current_user.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.commit()
db.refresh(issue) db.refresh(meeting)
return issue return meeting

39
app/models/meeting.py Normal file
View File

@@ -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])

38
app/models/support.py Normal file
View File

@@ -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])

45
app/models/task.py Normal file
View File

@@ -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())