803 lines
32 KiB
Python
803 lines
32 KiB
Python
"""Miscellaneous routers: API keys, activity, milestones, notifications, worklogs, export, dashboard."""
|
|
import csv
|
|
import io
|
|
import secrets
|
|
import math
|
|
from typing import List
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func as sqlfunc
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.config import get_db
|
|
from app.api.deps import get_current_user_or_apikey
|
|
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
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ============ API Keys ============
|
|
|
|
class APIKeyCreate(BaseModel):
|
|
name: str
|
|
user_id: int
|
|
|
|
class APIKeyResponse(BaseModel):
|
|
id: int
|
|
key: str
|
|
name: str
|
|
user_id: int
|
|
is_active: bool
|
|
created_at: datetime
|
|
last_used_at: datetime | None = None
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.post("/api-keys", response_model=APIKeyResponse, status_code=status.HTTP_201_CREATED, tags=["API Keys"])
|
|
def create_api_key(data: APIKeyCreate, db: Session = Depends(get_db)):
|
|
user = db.query(models.User).filter(models.User.id == data.user_id).first()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
key = secrets.token_hex(32)
|
|
db_key = APIKey(key=key, name=data.name, user_id=data.user_id)
|
|
db.add(db_key)
|
|
db.commit()
|
|
db.refresh(db_key)
|
|
return db_key
|
|
|
|
|
|
@router.get("/api-keys", response_model=List[APIKeyResponse], tags=["API Keys"])
|
|
def list_api_keys(user_id: int = None, db: Session = Depends(get_db)):
|
|
query = db.query(APIKey)
|
|
if user_id:
|
|
query = query.filter(APIKey.user_id == user_id)
|
|
return query.all()
|
|
|
|
|
|
@router.delete("/api-keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["API Keys"])
|
|
def revoke_api_key(key_id: int, db: Session = Depends(get_db)):
|
|
key_obj = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not key_obj:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
key_obj.is_active = False
|
|
db.commit()
|
|
return None
|
|
|
|
|
|
# ============ Activity Log ============
|
|
|
|
class ActivityLogResponse(BaseModel):
|
|
id: int
|
|
action: str
|
|
entity_type: str
|
|
entity_id: int
|
|
user_id: int | None
|
|
details: str | None
|
|
created_at: datetime
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("/activity", response_model=List[ActivityLogResponse], tags=["Activity"])
|
|
def list_activity(entity_type: str = None, entity_id: int = None, user_id: int = None,
|
|
limit: int = 50, db: Session = Depends(get_db)):
|
|
query = db.query(ActivityLog)
|
|
if entity_type:
|
|
query = query.filter(ActivityLog.entity_type == entity_type)
|
|
if entity_id:
|
|
query = query.filter(ActivityLog.entity_id == entity_id)
|
|
if user_id:
|
|
query = query.filter(ActivityLog.user_id == user_id)
|
|
return query.order_by(ActivityLog.created_at.desc()).limit(limit).all()
|
|
|
|
|
|
# ============ Milestones ============
|
|
|
|
@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:
|
|
data["depend_on_milestones"] = None
|
|
if data.get("depend_on_tasks"):
|
|
data["depend_on_tasks"] = json.dumps(data["depend_on_tasks"])
|
|
else:
|
|
data["depend_on_tasks"] = None
|
|
|
|
db_ms = MilestoneModel(**data)
|
|
db_ms.milestone_code = milestone_code
|
|
db.add(db_ms)
|
|
db.commit()
|
|
db.refresh(db_ms)
|
|
return db_ms
|
|
|
|
|
|
@router.get("/milestones", response_model=List[schemas.MilestoneResponse], tags=["Milestones"])
|
|
def list_milestones(project_id: int = None, status_filter: str = None, db: Session = Depends(get_db)):
|
|
query = db.query(MilestoneModel)
|
|
if project_id:
|
|
query = query.filter(MilestoneModel.project_id == project_id)
|
|
if status_filter:
|
|
query = query.filter(MilestoneModel.status == status_filter)
|
|
return query.order_by(MilestoneModel.due_date.is_(None), MilestoneModel.due_date.asc()).all()
|
|
|
|
|
|
@router.get("/milestones/{milestone_id}", response_model=schemas.MilestoneResponse, tags=["Milestones"])
|
|
def get_milestone(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")
|
|
return ms
|
|
|
|
|
|
@router.patch("/milestones/{milestone_id}", response_model=schemas.MilestoneResponse, tags=["Milestones"])
|
|
def update_milestone(milestone_id: int, ms_update: schemas.MilestoneUpdate, 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")
|
|
for field, value in ms_update.model_dump(exclude_unset=True).items():
|
|
setattr(ms, field, value)
|
|
db.commit()
|
|
db.refresh(ms)
|
|
return ms
|
|
|
|
|
|
@router.delete("/milestones/{milestone_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Milestones"])
|
|
def delete_milestone(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")
|
|
db.delete(ms)
|
|
db.commit()
|
|
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"))
|
|
|
|
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,
|
|
}
|
|
|
|
|
|
@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):
|
|
id: int
|
|
user_id: int
|
|
type: str
|
|
title: str
|
|
message: str | None = None
|
|
entity_type: str | None = None
|
|
entity_id: int | None = None
|
|
is_read: bool
|
|
created_at: datetime
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("/notifications", response_model=List[NotificationResponse], tags=["Notifications"])
|
|
def list_notifications(unread_only: bool = False, limit: int = 50, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
|
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()
|
|
|
|
|
|
@router.get("/notifications/count", tags=["Notifications"])
|
|
def notification_count(db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
|
count = db.query(NotificationModel).filter(
|
|
NotificationModel.user_id == current_user.id, NotificationModel.is_read == False
|
|
).count()
|
|
return {"user_id": current_user.id, "count": count, "unread": count}
|
|
|
|
|
|
@router.post("/notifications/{notification_id}/read", tags=["Notifications"])
|
|
def mark_read(notification_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
|
n = db.query(NotificationModel).filter(NotificationModel.id == notification_id).first()
|
|
if not n:
|
|
raise HTTPException(status_code=404, detail="Notification not found")
|
|
if n.user_id != current_user.id and not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
n.is_read = True
|
|
db.commit()
|
|
return {"status": "read"}
|
|
|
|
|
|
@router.post("/notifications/read-all", tags=["Notifications"])
|
|
def mark_all_read(db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
|
db.query(NotificationModel).filter(
|
|
NotificationModel.user_id == current_user.id, NotificationModel.is_read == False
|
|
).update({"is_read": True})
|
|
db.commit()
|
|
return {"status": "all_read", "user_id": current_user.id}
|
|
|
|
|
|
# ============ Work Logs ============
|
|
|
|
class WorkLogCreate(BaseModel):
|
|
issue_id: int
|
|
user_id: int
|
|
hours: float
|
|
description: str | None = None
|
|
logged_date: datetime
|
|
|
|
class WorkLogResponse(BaseModel):
|
|
id: int
|
|
issue_id: int
|
|
user_id: int
|
|
hours: float
|
|
description: str | None = None
|
|
logged_date: datetime
|
|
created_at: datetime
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@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")
|
|
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")
|
|
if wl.hours <= 0:
|
|
raise HTTPException(status_code=400, detail="Hours must be positive")
|
|
db_wl = WorkLog(**wl.model_dump())
|
|
db.add(db_wl)
|
|
db.commit()
|
|
db.refresh(db_wl)
|
|
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("/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.delete("/worklogs/{worklog_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Time Tracking"])
|
|
def delete_worklog(worklog_id: int, db: Session = Depends(get_db)):
|
|
wl = db.query(WorkLog).filter(WorkLog.id == worklog_id).first()
|
|
if not wl:
|
|
raise HTTPException(status_code=404, detail="Work log not found")
|
|
db.delete(wl)
|
|
db.commit()
|
|
return None
|
|
|
|
|
|
# ============ 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)
|
|
if project_id:
|
|
query = query.filter(models.Issue.project_id == project_id)
|
|
issues = 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",
|
|
"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])
|
|
output.seek(0)
|
|
return StreamingResponse(iter([output.getvalue()]), media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=issues.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)
|
|
if project_id:
|
|
query = query.filter(models.Issue.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}
|
|
|
|
|
|
# ============ Tasks ============
|
|
|
|
@router.get("/tasks/{project_code}/{milestone_id}", tags=["Tasks"])
|
|
def list_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")
|
|
|
|
tasks = db.query(Task).filter(
|
|
Task.project_id == project.id,
|
|
Task.milestone_id == milestone_id
|
|
).all()
|
|
|
|
return [{
|
|
"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
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
# 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
|
|
task_code = f"{milestone_code}:T{next_num:05x}"
|
|
|
|
est_time = None
|
|
if task_data.get("estimated_working_time"):
|
|
try:
|
|
est_time = datetime.strptime(task_data["estimated_working_time"], "%H:%M").time()
|
|
except:
|
|
pass
|
|
|
|
task = Task(
|
|
title=task_data.get("title"),
|
|
description=task_data.get("description"),
|
|
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,
|
|
created_by_id=current_user.id,
|
|
)
|
|
db.add(task)
|
|
db.commit()
|
|
db.refresh(task)
|
|
|
|
return {
|
|
"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,
|
|
}
|
|
|
|
|
|
@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"])
|
|
def list_supports(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")
|
|
|
|
supports = db.query(Support).filter(
|
|
Support.project_id == project.id,
|
|
Support.milestone_id == milestone_id
|
|
).all()
|
|
|
|
return [{
|
|
"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"])
|
|
def create_support(project_code: str, milestone_id: int, support_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")
|
|
|
|
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")
|
|
|
|
# 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
|
|
support_code = f"{milestone_code}:S{next_num:05x}"
|
|
|
|
support = Support(
|
|
title=support_data.get("title"),
|
|
description=support_data.get("description"),
|
|
status=SupportStatus.OPEN,
|
|
priority=SupportPriority.MEDIUM,
|
|
project_id=project.id,
|
|
milestone_id=milestone_id,
|
|
reporter_id=current_user.id,
|
|
support_code=support_code,
|
|
)
|
|
db.add(support)
|
|
db.commit()
|
|
db.refresh(support)
|
|
return support
|
|
|
|
|
|
# ============ Meetings ============
|
|
|
|
@router.get("/meetings/{project_code}/{milestone_id}", tags=["Meetings"])
|
|
def list_meetings(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")
|
|
|
|
meetings = db.query(Meeting).filter(
|
|
Meeting.project_id == project.id,
|
|
Meeting.milestone_id == milestone_id
|
|
).all()
|
|
|
|
return [{
|
|
"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")
|
|
|
|
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")
|
|
|
|
# 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
|
|
meeting_code = f"{milestone_code}:M{next_num:05x}"
|
|
|
|
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"),
|
|
status=MeetingStatus.SCHEDULED,
|
|
priority=MeetingPriority.MEDIUM,
|
|
project_id=project.id,
|
|
milestone_id=milestone_id,
|
|
reporter_id=current_user.id,
|
|
meeting_code=meeting_code,
|
|
scheduled_at=scheduled_at,
|
|
duration_minutes=meeting_data.get("duration_minutes"),
|
|
)
|
|
db.add(meeting)
|
|
db.commit()
|
|
db.refresh(meeting)
|
|
return meeting
|