feat: milestones, due dates, overdue filter, CSV export
This commit is contained in:
128
app/main.py
128
app/main.py
@@ -132,6 +132,19 @@ def list_issues(
|
||||
return issues
|
||||
|
||||
|
||||
@app.get("/issues/overdue", response_model=List[schemas.IssueResponse])
|
||||
def list_overdue_issues(project_id: int = None, db: Session = Depends(get_db)):
|
||||
"""List issues past their due date that aren't closed/resolved."""
|
||||
query = db.query(models.Issue).filter(
|
||||
models.Issue.due_date != None,
|
||||
models.Issue.due_date < datetime.utcnow(),
|
||||
models.Issue.status.notin_(["resolved", "closed"])
|
||||
)
|
||||
if project_id:
|
||||
query = query.filter(models.Issue.project_id == project_id)
|
||||
return query.order_by(models.Issue.due_date.asc()).all()
|
||||
|
||||
|
||||
@app.get("/issues/{issue_id}", response_model=schemas.IssueResponse)
|
||||
def get_issue(issue_id: int, db: Session = Depends(get_db)):
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
|
||||
@@ -254,6 +267,7 @@ def startup():
|
||||
from app.models import webhook
|
||||
from app.models import apikey
|
||||
from app.models import activity
|
||||
from app.models import milestone
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@@ -772,3 +786,117 @@ def list_all_tags(project_id: int = None, db: Session = Depends(get_db)):
|
||||
if t:
|
||||
all_tags.add(t)
|
||||
return {"tags": sorted(all_tags)}
|
||||
|
||||
|
||||
# ============ Milestones API ============
|
||||
|
||||
from app.models.milestone import Milestone as MilestoneModel, MilestoneStatus
|
||||
from app.schemas.schemas import MilestoneCreate, MilestoneUpdate, MilestoneResponse
|
||||
|
||||
|
||||
@app.post("/milestones", response_model=MilestoneResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_milestone(ms: MilestoneCreate, db: Session = Depends(get_db)):
|
||||
db_ms = MilestoneModel(**ms.model_dump())
|
||||
db.add(db_ms)
|
||||
db.commit()
|
||||
db.refresh(db_ms)
|
||||
return db_ms
|
||||
|
||||
|
||||
@app.get("/milestones", response_model=List[MilestoneResponse])
|
||||
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()
|
||||
|
||||
|
||||
@app.get("/milestones/{milestone_id}", response_model=MilestoneResponse)
|
||||
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
|
||||
|
||||
|
||||
@app.patch("/milestones/{milestone_id}", response_model=MilestoneResponse)
|
||||
def update_milestone(milestone_id: int, ms_update: 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
|
||||
|
||||
|
||||
@app.delete("/milestones/{milestone_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
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
|
||||
|
||||
|
||||
@app.get("/milestones/{milestone_id}/issues", response_model=List[schemas.IssueResponse])
|
||||
def list_milestone_issues(milestone_id: int, db: Session = Depends(get_db)):
|
||||
"""List all issues in a milestone."""
|
||||
return db.query(models.Issue).filter(models.Issue.milestone_id == milestone_id).all()
|
||||
|
||||
|
||||
@app.get("/milestones/{milestone_id}/progress")
|
||||
def milestone_progress(milestone_id: int, db: Session = Depends(get_db)):
|
||||
"""Get milestone completion progress."""
|
||||
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()
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# ============ Export API ============
|
||||
|
||||
import csv
|
||||
import io
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
@app.get("/export/issues")
|
||||
def export_issues_csv(project_id: int = None, db: Session = Depends(get_db)):
|
||||
"""Export issues as CSV."""
|
||||
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", "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.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"}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user