refactor: replace issues backend with milestone tasks
This commit is contained in:
@@ -103,22 +103,19 @@ def list_activity(entity_type: str = None, entity_id: int = None, user_id: int =
|
||||
return query.order_by(ActivityLog.created_at.desc()).limit(limit).all()
|
||||
|
||||
|
||||
# ============ Milestones ============
|
||||
# ============ Milestones (top-level, non project-scoped) ============
|
||||
|
||||
@router.post("/milestones", response_model=schemas.MilestoneResponse, status_code=status.HTTP_201_CREATED, tags=["Milestones"])
|
||||
def create_milestone(ms: schemas.MilestoneCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
import json
|
||||
# Generate milestone_code: projCode:{i:05x}
|
||||
project = db.query(models.Project).filter(models.Project.id == ms.project_id).first()
|
||||
project_code = project.project_code if project and project.project_code else f"P{ms.project_id}"
|
||||
|
||||
# Get max milestone number for this project
|
||||
max_ms = db.query(MilestoneModel).filter(MilestoneModel.project_id == ms.project_id).order_by(MilestoneModel.id.desc()).first()
|
||||
next_num = (max_ms.id + 1) if max_ms else 1
|
||||
milestone_code = f"{project_code}:{next_num:05x}"
|
||||
|
||||
data = ms.model_dump()
|
||||
# Serialize list fields to JSON strings
|
||||
if data.get("depend_on_milestones"):
|
||||
data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"])
|
||||
else:
|
||||
@@ -176,188 +173,34 @@ def delete_milestone(milestone_id: int, db: Session = Depends(get_db)):
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/milestones/{milestone_id}/issues", response_model=List[schemas.IssueResponse], tags=["Milestones"])
|
||||
def list_milestone_issues(milestone_id: int, db: Session = Depends(get_db)):
|
||||
return db.query(models.Issue).filter(models.Issue.milestone_id == milestone_id).all()
|
||||
|
||||
|
||||
@router.get("/milestones/{milestone_id}/progress", tags=["Milestones"])
|
||||
def milestone_progress(milestone_id: int, db: Session = Depends(get_db)):
|
||||
from datetime import datetime
|
||||
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
|
||||
if not ms:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
# Count tasks only
|
||||
issues = db.query(models.Issue).filter(
|
||||
models.Issue.milestone_id == milestone_id,
|
||||
models.Issue.issue_type == "task"
|
||||
).all()
|
||||
total = len(issues)
|
||||
done = sum(1 for i in issues if i.status in ("resolved", "closed"))
|
||||
|
||||
tasks = db.query(Task).filter(Task.milestone_id == milestone_id).all()
|
||||
total = len(tasks)
|
||||
done = sum(1 for t in tasks if t.status == TaskStatus.CLOSED)
|
||||
|
||||
time_progress = None
|
||||
if ms.planned_release_date and ms.created_at:
|
||||
now = datetime.now()
|
||||
total_duration = (ms.planned_release_date - ms.created_at).total_seconds()
|
||||
elapsed = (now - ms.created_at).total_seconds()
|
||||
time_progress = min(100, max(0, (elapsed / total_duration * 100)))
|
||||
|
||||
return {"milestone_id": milestone_id, "title": ms.title, "total": total,
|
||||
"completed": done, "progress_pct": round(done / total * 100, 1) if total else 0,
|
||||
"time_progress_pct": round(time_progress, 1) if time_progress else None,
|
||||
"planned_release_date": ms.planned_release_date}
|
||||
|
||||
|
||||
@router.get("/milestones/{milestone_id}/items", tags=["Milestones"])
|
||||
def milestone_items(milestone_id: int, db: Session = Depends(get_db)):
|
||||
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
|
||||
if not ms:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
issues = db.query(models.Issue).filter(models.Issue.milestone_id == milestone_id).all()
|
||||
|
||||
tasks = []
|
||||
supports = []
|
||||
meetings = []
|
||||
|
||||
for issue in issues:
|
||||
issue_data = {
|
||||
"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,
|
||||
"created_at": issue.created_at,
|
||||
}
|
||||
if issue.issue_type == "task":
|
||||
tasks.append(issue_data)
|
||||
elif issue.issue_type == "support":
|
||||
supports.append(issue_data)
|
||||
elif issue.issue_type == "meeting":
|
||||
meetings.append(issue_data)
|
||||
|
||||
return {"tasks": tasks, "supports": supports, "meetings": meetings}
|
||||
|
||||
|
||||
@router.post("/milestones/{milestone_id}/tasks", status_code=status.HTTP_201_CREATED, tags=["Milestones"])
|
||||
def create_milestone_task(milestone_id: int, issue_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
import json
|
||||
from datetime import datetime, time
|
||||
|
||||
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
|
||||
if not ms:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
# Check if milestone is 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")
|
||||
|
||||
# Get project_id from milestone
|
||||
project_id = ms.project_id
|
||||
|
||||
# Generate task_code: i_{project_code}_{id:06x}
|
||||
project = db.query(models.Project).filter(models.Project.id == project_id).first()
|
||||
project_code = project.project_code if project else f"P{project_id}"
|
||||
|
||||
# Get max id for this project to generate unique code
|
||||
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
|
||||
task_code = f"{milestone_code}:T{next_num:05x}"
|
||||
|
||||
# Parse estimated_working_time if provided
|
||||
est_time = None
|
||||
if issue_data.get("estimated_working_time"):
|
||||
try:
|
||||
est_time = datetime.strptime(issue_data["estimated_working_time"], "%H:%M").time()
|
||||
except:
|
||||
pass
|
||||
|
||||
issue = models.Issue(
|
||||
title=issue_data.get("title"),
|
||||
description=issue_data.get("description"),
|
||||
issue_type="task",
|
||||
status=models.IssueStatus.OPEN,
|
||||
priority=models.IssuePriority.MEDIUM,
|
||||
project_id=project_id,
|
||||
milestone_id=milestone_id,
|
||||
reporter_id=current_user.id,
|
||||
# Task-specific fields
|
||||
task_code=task_code,
|
||||
estimated_effort=issue_data.get("estimated_effort"),
|
||||
estimated_working_time=est_time,
|
||||
task_status="open",
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
db.add(issue)
|
||||
db.commit()
|
||||
db.refresh(issue)
|
||||
|
||||
# Return with task_code
|
||||
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,
|
||||
"milestone_id": milestone_id,
|
||||
"title": ms.title,
|
||||
"total": total,
|
||||
"total_tasks": total,
|
||||
"completed": done,
|
||||
"progress_pct": round(done / total * 100, 1) if total else 0,
|
||||
"time_progress_pct": round(time_progress, 1) if time_progress else None,
|
||||
"planned_release_date": ms.planned_release_date,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/milestones/{milestone_id}/supports", status_code=status.HTTP_201_CREATED, tags=["Milestones"])
|
||||
def create_milestone_support(milestone_id: int, issue_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
|
||||
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":
|
||||
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress")
|
||||
|
||||
project_id = ms.project_id
|
||||
|
||||
issue = models.Issue(
|
||||
title=issue_data.get("title"),
|
||||
description=issue_data.get("description"),
|
||||
issue_type="support",
|
||||
status=models.IssueStatus.OPEN,
|
||||
priority=models.IssuePriority.MEDIUM,
|
||||
project_id=project_id,
|
||||
milestone_id=milestone_id,
|
||||
reporter_id=current_user.id,
|
||||
)
|
||||
db.add(issue)
|
||||
db.commit()
|
||||
db.refresh(issue)
|
||||
return issue
|
||||
|
||||
|
||||
@router.post("/milestones/{milestone_id}/meetings", status_code=status.HTTP_201_CREATED, tags=["Milestones"])
|
||||
def create_milestone_meeting(milestone_id: int, issue_data: dict, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
ms = db.query(MilestoneModel).filter(MilestoneModel.id == milestone_id).first()
|
||||
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":
|
||||
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress")
|
||||
|
||||
project_id = ms.project_id
|
||||
|
||||
issue = models.Issue(
|
||||
title=issue_data.get("title"),
|
||||
description=issue_data.get("description"),
|
||||
issue_type="meeting",
|
||||
status=models.IssueStatus.OPEN,
|
||||
priority=models.IssuePriority.MEDIUM,
|
||||
project_id=project_id,
|
||||
milestone_id=milestone_id,
|
||||
reporter_id=current_user.id,
|
||||
)
|
||||
db.add(issue)
|
||||
db.commit()
|
||||
db.refresh(issue)
|
||||
return issue
|
||||
|
||||
|
||||
# ============ Notifications ============
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
@@ -368,10 +211,24 @@ class NotificationResponse(BaseModel):
|
||||
message: str | None = None
|
||||
entity_type: str | None = None
|
||||
entity_id: int | None = None
|
||||
task_id: int | None = None
|
||||
is_read: bool
|
||||
created_at: datetime
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def _serialize_notification(notification: NotificationModel):
|
||||
return {
|
||||
"id": notification.id,
|
||||
"user_id": notification.user_id,
|
||||
"type": notification.type,
|
||||
"title": notification.title,
|
||||
"message": notification.message or notification.title,
|
||||
"entity_type": notification.entity_type,
|
||||
"entity_id": notification.entity_id,
|
||||
"task_id": notification.entity_id if notification.entity_type == "task" else None,
|
||||
"is_read": notification.is_read,
|
||||
"created_at": notification.created_at,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/notifications", response_model=List[NotificationResponse], tags=["Notifications"])
|
||||
@@ -379,7 +236,8 @@ def list_notifications(unread_only: bool = False, limit: int = 50, db: Session =
|
||||
query = db.query(NotificationModel).filter(NotificationModel.user_id == current_user.id)
|
||||
if unread_only:
|
||||
query = query.filter(NotificationModel.is_read == False)
|
||||
return query.order_by(NotificationModel.created_at.desc()).limit(limit).all()
|
||||
notifications = query.order_by(NotificationModel.created_at.desc()).limit(limit).all()
|
||||
return [_serialize_notification(n) for n in notifications]
|
||||
|
||||
|
||||
@router.get("/notifications/count", tags=["Notifications"])
|
||||
@@ -414,7 +272,7 @@ def mark_all_read(db: Session = Depends(get_db), current_user: models.User = Dep
|
||||
# ============ Work Logs ============
|
||||
|
||||
class WorkLogCreate(BaseModel):
|
||||
issue_id: int
|
||||
task_id: int
|
||||
user_id: int
|
||||
hours: float
|
||||
description: str | None = None
|
||||
@@ -422,7 +280,7 @@ class WorkLogCreate(BaseModel):
|
||||
|
||||
class WorkLogResponse(BaseModel):
|
||||
id: int
|
||||
issue_id: int
|
||||
task_id: int
|
||||
user_id: int
|
||||
hours: float
|
||||
description: str | None = None
|
||||
@@ -434,9 +292,9 @@ class WorkLogResponse(BaseModel):
|
||||
|
||||
@router.post("/worklogs", response_model=WorkLogResponse, status_code=status.HTTP_201_CREATED, tags=["Time Tracking"])
|
||||
def create_worklog(wl: WorkLogCreate, db: Session = Depends(get_db)):
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == wl.issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
task = db.query(Task).filter(Task.id == wl.task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
user = db.query(models.User).filter(models.User.id == wl.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -449,19 +307,19 @@ def create_worklog(wl: WorkLogCreate, db: Session = Depends(get_db)):
|
||||
return db_wl
|
||||
|
||||
|
||||
@router.get("/issues/{issue_id}/worklogs", response_model=List[WorkLogResponse], tags=["Time Tracking"])
|
||||
def list_issue_worklogs(issue_id: int, db: Session = Depends(get_db)):
|
||||
return db.query(WorkLog).filter(WorkLog.issue_id == issue_id).order_by(WorkLog.logged_date.desc()).all()
|
||||
@router.get("/tasks/{task_id}/worklogs", response_model=List[WorkLogResponse], tags=["Time Tracking"])
|
||||
def list_task_worklogs(task_id: int, db: Session = Depends(get_db)):
|
||||
return db.query(WorkLog).filter(WorkLog.task_id == task_id).order_by(WorkLog.logged_date.desc()).all()
|
||||
|
||||
|
||||
@router.get("/issues/{issue_id}/worklogs/summary", tags=["Time Tracking"])
|
||||
def issue_worklog_summary(issue_id: int, db: Session = Depends(get_db)):
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
total = db.query(sqlfunc.sum(WorkLog.hours)).filter(WorkLog.issue_id == issue_id).scalar() or 0
|
||||
count = db.query(WorkLog).filter(WorkLog.issue_id == issue_id).count()
|
||||
return {"issue_id": issue_id, "total_hours": round(total, 2), "log_count": count}
|
||||
@router.get("/tasks/{task_id}/worklogs/summary", tags=["Time Tracking"])
|
||||
def task_worklog_summary(task_id: int, db: Session = Depends(get_db)):
|
||||
task = db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
total = db.query(sqlfunc.sum(WorkLog.hours)).filter(WorkLog.task_id == task_id).scalar() or 0
|
||||
count = db.query(WorkLog).filter(WorkLog.task_id == task_id).count()
|
||||
return {"task_id": task_id, "total_hours": round(total, 2), "log_count": count}
|
||||
|
||||
|
||||
@router.delete("/worklogs/{worklog_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Time Tracking"])
|
||||
@@ -476,47 +334,55 @@ def delete_worklog(worklog_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
# ============ Export ============
|
||||
|
||||
@router.get("/export/issues", tags=["Export"])
|
||||
def export_issues_csv(project_id: int = None, db: Session = Depends(get_db)):
|
||||
query = db.query(models.Issue)
|
||||
@router.get("/export/tasks", tags=["Export"])
|
||||
def export_tasks_csv(project_id: int = None, db: Session = Depends(get_db)):
|
||||
query = db.query(Task)
|
||||
if project_id:
|
||||
query = query.filter(models.Issue.project_id == project_id)
|
||||
issues = query.all()
|
||||
query = query.filter(Task.project_id == project_id)
|
||||
tasks = query.all()
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["id", "title", "type", "subtype", "status", "priority", "project_id",
|
||||
"reporter_id", "assignee_id", "milestone_id", "due_date",
|
||||
"milestone_id", "reporter_id", "assignee_id", "task_code",
|
||||
"tags", "created_at", "updated_at"])
|
||||
for i in issues:
|
||||
writer.writerow([i.id, i.title, i.issue_type, i.issue_subtype or "", i.status, i.priority, i.project_id,
|
||||
i.reporter_id, i.assignee_id, i.milestone_id, i.due_date,
|
||||
i.tags, i.created_at, i.updated_at])
|
||||
for t in tasks:
|
||||
writer.writerow([t.id, t.title, t.task_type, t.task_subtype or "",
|
||||
t.status.value if hasattr(t.status, 'value') else t.status,
|
||||
t.priority.value if hasattr(t.priority, 'value') else t.priority,
|
||||
t.project_id, t.milestone_id, t.reporter_id, t.assignee_id, t.task_code,
|
||||
t.tags, t.created_at, t.updated_at])
|
||||
output.seek(0)
|
||||
return StreamingResponse(iter([output.getvalue()]), media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=issues.csv"})
|
||||
headers={"Content-Disposition": "attachment; filename=tasks.csv"})
|
||||
|
||||
|
||||
# ============ Dashboard ============
|
||||
|
||||
@router.get("/dashboard/stats", tags=["Dashboard"])
|
||||
def dashboard_stats(project_id: int = None, db: Session = Depends(get_db)):
|
||||
query = db.query(models.Issue)
|
||||
query = db.query(Task)
|
||||
if project_id:
|
||||
query = query.filter(models.Issue.project_id == project_id)
|
||||
query = query.filter(Task.project_id == project_id)
|
||||
total = query.count()
|
||||
by_status = {s: query.filter(models.Issue.status == s).count()
|
||||
for s in ["open", "in_progress", "resolved", "closed", "blocked"]}
|
||||
by_type = {t: query.filter(models.Issue.issue_type == t).count()
|
||||
for t in ["task", "story", "test", "resolution"]}
|
||||
by_priority = {p: query.filter(models.Issue.priority == p).count()
|
||||
for p in ["low", "medium", "high", "critical"]}
|
||||
return {"total": total, "by_status": by_status, "by_type": by_type, "by_priority": by_priority}
|
||||
by_status = {s.value: query.filter(Task.status == s).count() for s in TaskStatus}
|
||||
by_type = {t: query.filter(Task.task_type == t).count()
|
||||
for t in ["task", "story", "test", "resolution", "issue", "maintenance", "research", "review"]}
|
||||
by_priority = {p.value: query.filter(Task.priority == p).count() for p in TaskPriority}
|
||||
recent = query.order_by(Task.created_at.desc()).limit(10).all()
|
||||
return {
|
||||
"total": total,
|
||||
"total_tasks": total,
|
||||
"by_status": by_status,
|
||||
"by_type": by_type,
|
||||
"by_priority": by_priority,
|
||||
"recent_tasks": [schemas.TaskResponse.model_validate(t) for t in recent],
|
||||
}
|
||||
|
||||
|
||||
# ============ Tasks ============
|
||||
# ============ Milestone-scoped Tasks ============
|
||||
|
||||
@router.get("/tasks/{project_code}/{milestone_id}", tags=["Tasks"])
|
||||
def list_tasks(project_code: str, milestone_id: int, db: Session = Depends(get_db)):
|
||||
def list_milestone_tasks(project_code: str, milestone_id: int, db: Session = Depends(get_db)):
|
||||
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")
|
||||
@@ -533,6 +399,8 @@ def list_tasks(project_code: str, milestone_id: int, db: Session = Depends(get_d
|
||||
"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,
|
||||
"task_type": t.task_type,
|
||||
"task_subtype": t.task_subtype,
|
||||
"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,
|
||||
@@ -545,9 +413,7 @@ def list_tasks(project_code: str, milestone_id: int, db: Session = Depends(get_d
|
||||
|
||||
|
||||
@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
|
||||
|
||||
def create_milestone_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)):
|
||||
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")
|
||||
@@ -559,7 +425,6 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi
|
||||
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")
|
||||
|
||||
# Generate task_code: milestoneCode:T{i:05x}
|
||||
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()
|
||||
next_num = (max_task.id + 1) if max_task else 1
|
||||
@@ -577,6 +442,8 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi
|
||||
description=task_data.get("description"),
|
||||
status=TaskStatus.OPEN,
|
||||
priority=TaskPriority.MEDIUM,
|
||||
task_type=task_data.get("task_type", "task"),
|
||||
task_subtype=task_data.get("task_subtype"),
|
||||
project_id=project.id,
|
||||
milestone_id=milestone_id,
|
||||
reporter_id=current_user.id,
|
||||
@@ -600,78 +467,6 @@ def create_task(project_code: str, milestone_id: int, task_data: dict, db: Sessi
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tasks/{project_code}/{milestone_id}/{task_id}", tags=["Tasks"])
|
||||
def get_task(project_code: str, milestone_id: int, task_id: int, db: Session = Depends(get_db)):
|
||||
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")
|
||||
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.project_id == project.id,
|
||||
Task.milestone_id == milestone_id
|
||||
).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
return {
|
||||
"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,
|
||||
}
|
||||
|
||||
|
||||
@router.patch("/tasks/{project_code}/{milestone_id}/{task_id}", tags=["Tasks"])
|
||||
def update_task(project_code: str, milestone_id: int, task_id: int, task_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")
|
||||
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.project_id == project.id,
|
||||
Task.milestone_id == milestone_id
|
||||
).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
if "title" in task_data:
|
||||
task.title = task_data["title"]
|
||||
if "description" in task_data:
|
||||
task.description = task_data["description"]
|
||||
if "status" in task_data:
|
||||
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(task)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ============ Supports ============
|
||||
|
||||
@router.get("/supports/{project_code}/{milestone_id}", tags=["Supports"])
|
||||
@@ -709,7 +504,6 @@ def create_support(project_code: str, milestone_id: int, support_data: dict, db:
|
||||
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")
|
||||
|
||||
# Generate support_code: milestoneCode:S{i:05x}
|
||||
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()
|
||||
next_num = (max_support.id + 1) if max_support else 1
|
||||
@@ -758,8 +552,6 @@ def list_meetings(project_code: str, milestone_id: int, db: Session = Depends(ge
|
||||
|
||||
@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")
|
||||
@@ -771,7 +563,6 @@ def create_meeting(project_code: str, milestone_id: int, meeting_data: dict, db:
|
||||
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")
|
||||
|
||||
# Generate meeting_code: milestoneCode:M{i:05x}
|
||||
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()
|
||||
next_num = (max_meeting.id + 1) if max_meeting else 1
|
||||
|
||||
Reference in New Issue
Block a user