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.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

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