Compare commits
12 Commits
755e4a80f9
...
fee2320cee
| Author | SHA1 | Date | |
|---|---|---|---|
| fee2320cee | |||
| 67adcc375e | |||
| 39da27301a | |||
| 41d92c5e68 | |||
| a1a99bb838 | |||
| 5297711c77 | |||
| 724be87a04 | |||
| 5a76f61692 | |||
| 6fe5e5ddb3 | |||
| dbcb92a9c3 | |||
| 1b1ca0e0d3 | |||
| d76328923c |
@@ -129,11 +129,6 @@ def get_issue(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")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
return issue
|
||||
|
||||
|
||||
@@ -144,11 +139,6 @@ def update_issue(issue_id: int, issue_update: schemas.IssueUpdate, db: Session =
|
||||
check_project_role(db, current_user.id, issue.project_id, min_role="dev")
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
for field, value in update_data.items():
|
||||
setattr(issue, field, value)
|
||||
db.commit()
|
||||
@@ -163,11 +153,6 @@ def delete_issue(issue_id: int, db: Session = Depends(get_db), current_user: mod
|
||||
check_project_role(db, current_user.id, issue.project_id, min_role="mgr")
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
log_activity(db, "issue.deleted", "issue", issue.id, current_user.id, {"title": issue.title})
|
||||
db.delete(issue)
|
||||
db.commit()
|
||||
@@ -184,11 +169,6 @@ def transition_issue(issue_id: int, new_status: str, bg: BackgroundTasks, db: Se
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
old_status = issue.status
|
||||
issue.status = new_status
|
||||
db.commit()
|
||||
@@ -207,11 +187,6 @@ def assign_issue(issue_id: int, assignee_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")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
user = db.query(models.User).filter(models.User.id == assignee_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -266,11 +241,6 @@ def add_tag(issue_id: int, tag: str, 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")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
current = set(issue.tags.split(",")) if issue.tags else set()
|
||||
current.add(tag.strip())
|
||||
current.discard("")
|
||||
@@ -284,11 +254,6 @@ def remove_tag(issue_id: int, tag: str, 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")
|
||||
update_data = issue_update.model_dump(exclude_unset=True)
|
||||
if "issue_type" in update_data or "issue_subtype" in update_data:
|
||||
new_type = update_data.get("issue_type", issue.issue_type)
|
||||
new_subtype = update_data.get("issue_subtype", issue.issue_subtype)
|
||||
_validate_issue_type_subtype(new_type, new_subtype, require_subtype="issue_type" in update_data)
|
||||
current = set(issue.tags.split(",")) if issue.tags else set()
|
||||
current.discard(tag.strip())
|
||||
current.discard("")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Milestones API router."""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from app.core.config import get_db
|
||||
from app.api.deps import get_current_user_or_apikey
|
||||
@@ -13,23 +15,55 @@ from app.schemas import schemas
|
||||
router = APIRouter(prefix="/projects/{project_id}/milestones", tags=["Milestones"])
|
||||
|
||||
|
||||
def _serialize_milestone(milestone):
|
||||
"""Serialize milestone with JSON fields."""
|
||||
result = {
|
||||
"id": milestone.id,
|
||||
"title": milestone.title,
|
||||
"description": milestone.description,
|
||||
"status": milestone.status.value if hasattr(milestone.status, 'value') else milestone.status,
|
||||
"due_date": milestone.due_date,
|
||||
"planned_release_date": milestone.planned_release_date,
|
||||
"depend_on_milestones": json.loads(milestone.depend_on_milestones) if milestone.depend_on_milestones else [],
|
||||
"depend_on_tasks": json.loads(milestone.depend_on_tasks) if milestone.depend_on_tasks else [],
|
||||
"project_id": milestone.project_id,
|
||||
"created_at": milestone.created_at,
|
||||
"updated_at": milestone.updated_at,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.MilestoneResponse])
|
||||
def list_milestones(project_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""List all milestones for a project."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="viewer")
|
||||
milestones = db.query(Milestone).filter(Milestone.project_id == project_id).all()
|
||||
return milestones
|
||||
return [_serialize_milestone(m) for m in milestones]
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.MilestoneResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_milestone(project_id: int, milestone: schemas.MilestoneCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Create a new milestone for a project."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="mgr")
|
||||
db_milestone = Milestone(project_id=project_id, **milestone.model_dump())
|
||||
|
||||
# Generate milestone_code: projCode:{i:05x}
|
||||
project = db.query(models.Project).filter(models.Project.id == project_id).first()
|
||||
project_code = project.project_code if project else f"P{project_id}"
|
||||
max_ms = db.query(Milestone).filter(Milestone.project_id == project_id).order_by(Milestone.id.desc()).first()
|
||||
next_num = (max_ms.id + 1) if max_ms else 1
|
||||
milestone_code = f"{project_code}:{next_num:05x}"
|
||||
|
||||
data = milestone.model_dump()
|
||||
# Handle JSON fields
|
||||
if data.get("depend_on_milestones"):
|
||||
data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"])
|
||||
if data.get("depend_on_tasks"):
|
||||
data["depend_on_tasks"] = json.dumps(data["depend_on_tasks"])
|
||||
db_milestone = Milestone(project_id=project_id, milestone_code=milestone_code, **data)
|
||||
db.add(db_milestone)
|
||||
db.commit()
|
||||
db.refresh(db_milestone)
|
||||
return db_milestone
|
||||
return _serialize_milestone(db_milestone)
|
||||
|
||||
|
||||
@router.get("/{milestone_id}", response_model=schemas.MilestoneResponse)
|
||||
@@ -39,7 +73,7 @@ def get_milestone(project_id: int, milestone_id: int, db: Session = Depends(get_
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
return milestone
|
||||
return _serialize_milestone(milestone)
|
||||
|
||||
|
||||
@router.patch("/{milestone_id}", response_model=schemas.MilestoneResponse)
|
||||
@@ -49,11 +83,17 @@ def update_milestone(project_id: int, milestone_id: int, milestone: schemas.Mile
|
||||
db_milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not db_milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
for key, value in milestone.model_dump(exclude_unset=True).items():
|
||||
data = milestone.model_dump(exclude_unset=True)
|
||||
# Handle JSON fields
|
||||
if "depend_on_milestones" in data:
|
||||
data["depend_on_milestones"] = json.dumps(data["depend_on_milestones"]) if data["depend_on_milestones"] else None
|
||||
if "depend_on_tasks" in data:
|
||||
data["depend_on_tasks"] = json.dumps(data["depend_on_tasks"]) if data["depend_on_tasks"] else None
|
||||
for key, value in data.items():
|
||||
setattr(db_milestone, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_milestone)
|
||||
return db_milestone
|
||||
return _serialize_milestone(db_milestone)
|
||||
|
||||
|
||||
@router.delete("/{milestone_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -66,3 +106,151 @@ def delete_milestone(project_id: int, milestone_id: int, db: Session = Depends(g
|
||||
db.delete(db_milestone)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
|
||||
# Issue type helpers
|
||||
ISSUE_TYPE_TASK = "task"
|
||||
ISSUE_TYPE_SUPPORT = "support"
|
||||
ISSUE_TYPE_MEETING = "meeting"
|
||||
|
||||
|
||||
@router.post("/{milestone_id}/tasks", status_code=status.HTTP_201_CREATED)
|
||||
def create_milestone_task(project_id: int, milestone_id: int, issue: schemas.IssueCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Create a task under a milestone."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="dev")
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
# Check if milestone is in progressing status - cannot add new story
|
||||
if milestone.status and hasattr(milestone.status, 'value') and milestone.status.value == "progressing":
|
||||
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress")
|
||||
|
||||
issue_data = issue.model_dump()
|
||||
issue_data["issue_type"] = ISSUE_TYPE_TASK
|
||||
issue_data["milestone_id"] = milestone_id
|
||||
issue_data["project_id"] = project_id
|
||||
issue_data["reporter_id"] = current_user.id
|
||||
db_issue = models.Issue(**issue_data)
|
||||
db.add(db_issue)
|
||||
db.commit()
|
||||
db.refresh(db_issue)
|
||||
return db_issue
|
||||
|
||||
|
||||
@router.post("/{milestone_id}/supports", status_code=status.HTTP_201_CREATED)
|
||||
def create_milestone_support(project_id: int, milestone_id: int, issue: schemas.IssueCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Create a support request under a milestone."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="dev")
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
if milestone.status and hasattr(milestone.status, 'value') and milestone.status.value == "progressing":
|
||||
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress")
|
||||
|
||||
issue_data = issue.model_dump()
|
||||
issue_data["issue_type"] = ISSUE_TYPE_SUPPORT
|
||||
issue_data["milestone_id"] = milestone_id
|
||||
issue_data["project_id"] = project_id
|
||||
issue_data["reporter_id"] = current_user.id
|
||||
db_issue = models.Issue(**issue_data)
|
||||
db.add(db_issue)
|
||||
db.commit()
|
||||
db.refresh(db_issue)
|
||||
return db_issue
|
||||
|
||||
|
||||
@router.post("/{milestone_id}/meetings", status_code=status.HTTP_201_CREATED)
|
||||
def create_milestone_meeting(project_id: int, milestone_id: int, issue: schemas.IssueCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Create a meeting under a milestone."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="dev")
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
if milestone.status and hasattr(milestone.status, 'value') and milestone.status.value == "progressing":
|
||||
raise HTTPException(status_code=400, detail="Cannot add items to a milestone that is in_progress")
|
||||
|
||||
issue_data = issue.model_dump()
|
||||
issue_data["issue_type"] = ISSUE_TYPE_MEETING
|
||||
issue_data["milestone_id"] = milestone_id
|
||||
issue_data["project_id"] = project_id
|
||||
issue_data["reporter_id"] = current_user.id
|
||||
db_issue = models.Issue(**issue_data)
|
||||
db.add(db_issue)
|
||||
db.commit()
|
||||
db.refresh(db_issue)
|
||||
return db_issue
|
||||
|
||||
|
||||
@router.get("/{milestone_id}/items")
|
||||
def get_milestone_items(project_id: int, milestone_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Get all items (tasks, supports, meetings) for a milestone."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="viewer")
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
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 == ISSUE_TYPE_TASK:
|
||||
tasks.append(issue_data)
|
||||
elif issue.issue_type == ISSUE_TYPE_SUPPORT:
|
||||
supports.append(issue_data)
|
||||
elif issue.issue_type == ISSUE_TYPE_MEETING:
|
||||
meetings.append(issue_data)
|
||||
|
||||
return {"tasks": tasks, "supports": supports, "meetings": meetings}
|
||||
|
||||
|
||||
@router.get("/{milestone_id}/progress")
|
||||
def get_milestone_progress(project_id: int, milestone_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
"""Get progress for a milestone - tasks only."""
|
||||
check_project_role(db, current_user.id, project_id, min_role="viewer")
|
||||
milestone = db.query(Milestone).filter(Milestone.id == milestone_id, Milestone.project_id == project_id).first()
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=404, detail="Milestone not found")
|
||||
|
||||
# Count tasks only (not meetings or supports)
|
||||
all_issues = db.query(models.Issue).filter(
|
||||
models.Issue.milestone_id == milestone_id,
|
||||
models.Issue.issue_type == ISSUE_TYPE_TASK
|
||||
).all()
|
||||
|
||||
total = len(all_issues)
|
||||
completed = sum(1 for i in all_issues if i.status and hasattr(i.status, 'value') and i.status.value == "closed")
|
||||
|
||||
progress_pct = (completed / total * 100) if total > 0 else 0
|
||||
|
||||
# Calculate time progress if planned_release_date is set
|
||||
time_progress = None
|
||||
if milestone.planned_release_date:
|
||||
now = datetime.now()
|
||||
if milestone.created_at and milestone.planned_release_date > milestone.created_at:
|
||||
total_duration = (milestone.planned_release_date - milestone.created_at).total_seconds()
|
||||
elapsed = (now - milestone.created_at).total_seconds()
|
||||
time_progress = min(100, max(0, (elapsed / total_duration * 100)))
|
||||
|
||||
return {
|
||||
"milestone_id": milestone_id,
|
||||
"title": milestone.title,
|
||||
"total": total,
|
||||
"completed": completed,
|
||||
"progress_pct": round(progress_pct, 1),
|
||||
"time_progress_pct": round(time_progress, 1) if time_progress else None,
|
||||
"planned_release_date": milestone.planned_release_date,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -103,8 +106,30 @@ def list_activity(entity_type: str = None, entity_id: int = None, user_id: int =
|
||||
# ============ 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)):
|
||||
db_ms = MilestoneModel(**ms.model_dump())
|
||||
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)
|
||||
@@ -158,14 +183,179 @@ def list_milestone_issues(milestone_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
@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")
|
||||
issues = db.query(models.Issue).filter(models.Issue.milestone_id == milestone_id).all()
|
||||
# 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"))
|
||||
return {"milestone_id": milestone_id, "title": ms.title, "total_issues": total,
|
||||
"completed": done, "progress_pct": round(done / total * 100, 1) if total else 0}
|
||||
|
||||
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 ============
|
||||
@@ -321,3 +511,292 @@ def dashboard_stats(project_id: int = None, db: Session = Depends(get_db)):
|
||||
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
|
||||
|
||||
37
app/models/meeting.py
Normal file
37
app/models/meeting.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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)
|
||||
meeting_code = Column(String(64), nullable=True, unique=True, index=True)
|
||||
|
||||
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())
|
||||
@@ -1,25 +1,30 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum
|
||||
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 MilestoneStatus(str, enum.Enum):
|
||||
OPEN = "open"
|
||||
PENDING = "pending"
|
||||
DEFERRED = "deferred"
|
||||
PROGRESSING = "progressing"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class Milestone(Base):
|
||||
__tablename__ = "milestones"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
status = Column(Enum(MilestoneStatus), default=MilestoneStatus.OPEN)
|
||||
milestone_code = Column(String(64), nullable=True, unique=True, index=True)
|
||||
due_date = Column(DateTime(timezone=True), nullable=True)
|
||||
planned_release_date = Column(DateTime(timezone=True), nullable=True)
|
||||
depend_on_milestones = Column(Text, nullable=True)
|
||||
depend_on_tasks = Column(Text, nullable=True)
|
||||
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
|
||||
project = relationship("Project")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean, JSON
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean, JSON, Time
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.config import Base
|
||||
@@ -68,6 +68,17 @@ class Issue(Base):
|
||||
due_date = Column(DateTime(timezone=True), nullable=True)
|
||||
milestone_id = Column(Integer, ForeignKey("milestones.id"), nullable=True)
|
||||
|
||||
# Task-specific fields
|
||||
task_code = Column(String(64), nullable=True, unique=True, index=True)
|
||||
depend_on = Column(Text, nullable=True) # JSON list of task codes
|
||||
estimated_effort = Column(Integer, nullable=True) # 1-10
|
||||
estimated_working_time = Column(Time(timezone=True), nullable=True)
|
||||
task_status = Column(String(32), default="open") # open, closed, pending, progressing
|
||||
started_on = Column(DateTime(timezone=True), nullable=True)
|
||||
finished_on = Column(DateTime(timezone=True), nullable=True)
|
||||
related_tasks = Column(Text, nullable=True) # JSON list of task codes
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
project = relationship("Project", back_populates="issues")
|
||||
reporter = relationship("User", foreign_keys=[reporter_id], back_populates="reported_issues")
|
||||
assignee = relationship("User", foreign_keys=[assignee_id], back_populates="assigned_issues")
|
||||
|
||||
35
app/models/support.py
Normal file
35
app/models/support.py
Normal file
@@ -0,0 +1,35 @@
|
||||
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)
|
||||
support_code = Column(String(64), nullable=True, unique=True, index=True)
|
||||
|
||||
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())
|
||||
43
app/models/task.py
Normal file
43
app/models/task.py
Normal file
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
task_code = Column(String(64), nullable=True, unique=True, index=True)
|
||||
|
||||
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)
|
||||
|
||||
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())
|
||||
@@ -203,10 +203,15 @@ class ProjectMemberResponse(BaseModel):
|
||||
class MilestoneBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
status: Optional[str] = "open"
|
||||
due_date: Optional[datetime] = None
|
||||
planned_release_date: Optional[datetime] = None
|
||||
depend_on_milestones: Optional[List[str]] = None
|
||||
depend_on_tasks: Optional[List[int]] = None
|
||||
|
||||
|
||||
class MilestoneCreate(MilestoneBase):
|
||||
project_id: int
|
||||
pass
|
||||
|
||||
|
||||
@@ -215,11 +220,13 @@ class MilestoneUpdate(BaseModel):
|
||||
description: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
due_date: Optional[datetime] = None
|
||||
planned_release_date: Optional[datetime] = None
|
||||
depend_on_milestones: Optional[List[str]] = None
|
||||
depend_on_tasks: Optional[List[int]] = None
|
||||
|
||||
|
||||
class MilestoneResponse(MilestoneBase):
|
||||
id: int
|
||||
status: str
|
||||
project_id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
Reference in New Issue
Block a user