feat: notifications system, webhook retry, issue assign endpoint, CLI milestones/notifications/overdue commands
This commit is contained in:
114
app/main.py
114
app/main.py
@@ -268,6 +268,7 @@ def startup():
|
||||
from app.models import apikey
|
||||
from app.models import activity
|
||||
from app.models import milestone
|
||||
from app.models import notification
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@@ -870,6 +871,7 @@ def milestone_progress(milestone_id: int, db: Session = Depends(get_db)):
|
||||
# ============ Export API ============
|
||||
|
||||
import csv
|
||||
import json
|
||||
import io
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
@@ -900,3 +902,115 @@ def export_issues_csv(project_id: int = None, db: Session = Depends(get_db)):
|
||||
headers={"Content-Disposition": "attachment; filename=issues.csv"}
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ============ Notifications API ============
|
||||
|
||||
from app.models.notification import Notification as NotificationModel
|
||||
|
||||
|
||||
class NotificationResponse(PydanticBaseModel):
|
||||
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
|
||||
|
||||
|
||||
def notify_user(db: Session, user_id: int, ntype: str, title: str, message: str = None, entity_type: str = None, entity_id: int = None):
|
||||
"""Helper to create a notification."""
|
||||
n = NotificationModel(user_id=user_id, type=ntype, title=title, message=message, entity_type=entity_type, entity_id=entity_id)
|
||||
db.add(n)
|
||||
db.commit()
|
||||
return n
|
||||
|
||||
|
||||
@app.get("/notifications", response_model=List[NotificationResponse])
|
||||
def list_notifications(user_id: int, unread_only: bool = False, limit: int = 50, db: Session = Depends(get_db)):
|
||||
"""List notifications for a user."""
|
||||
query = db.query(NotificationModel).filter(NotificationModel.user_id == user_id)
|
||||
if unread_only:
|
||||
query = query.filter(NotificationModel.is_read == False)
|
||||
return query.order_by(NotificationModel.created_at.desc()).limit(limit).all()
|
||||
|
||||
|
||||
@app.get("/notifications/count")
|
||||
def notification_count(user_id: int, db: Session = Depends(get_db)):
|
||||
"""Get unread notification count."""
|
||||
count = db.query(NotificationModel).filter(
|
||||
NotificationModel.user_id == user_id,
|
||||
NotificationModel.is_read == False
|
||||
).count()
|
||||
return {"user_id": user_id, "unread": count}
|
||||
|
||||
|
||||
@app.post("/notifications/{notification_id}/read")
|
||||
def mark_read(notification_id: int, db: Session = Depends(get_db)):
|
||||
n = db.query(NotificationModel).filter(NotificationModel.id == notification_id).first()
|
||||
if not n:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
n.is_read = True
|
||||
db.commit()
|
||||
return {"status": "read"}
|
||||
|
||||
|
||||
@app.post("/notifications/read-all")
|
||||
def mark_all_read(user_id: int, db: Session = Depends(get_db)):
|
||||
"""Mark all notifications as read for a user."""
|
||||
db.query(NotificationModel).filter(
|
||||
NotificationModel.user_id == user_id,
|
||||
NotificationModel.is_read == False
|
||||
).update({"is_read": True})
|
||||
db.commit()
|
||||
return {"status": "all_read"}
|
||||
|
||||
|
||||
# ============ Webhook Retry ============
|
||||
|
||||
@app.post("/webhooks/{webhook_id}/retry/{log_id}")
|
||||
def retry_webhook(webhook_id: int, log_id: int, bg: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
"""Retry a failed webhook delivery."""
|
||||
log_entry = db.query(WebhookLog).filter(
|
||||
WebhookLog.id == log_id,
|
||||
WebhookLog.webhook_id == webhook_id
|
||||
).first()
|
||||
if not log_entry:
|
||||
raise HTTPException(status_code=404, detail="Webhook log not found")
|
||||
wh = db.query(Webhook).filter(Webhook.id == webhook_id).first()
|
||||
if not wh:
|
||||
raise HTTPException(status_code=404, detail="Webhook not found")
|
||||
|
||||
bg.add_task(fire_webhooks_sync, log_entry.event, json.loads(log_entry.payload), wh.project_id, db)
|
||||
return {"status": "retry_queued", "log_id": log_id}
|
||||
|
||||
|
||||
# ============ Issue Assignment with Notification ============
|
||||
|
||||
@app.post("/issues/{issue_id}/assign")
|
||||
def assign_issue(issue_id: int, assignee_id: int, db: Session = Depends(get_db)):
|
||||
"""Assign issue to user and send notification."""
|
||||
issue = db.query(models.Issue).filter(models.Issue.id == issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
user = db.query(models.User).filter(models.User.id == assignee_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
old_assignee = issue.assignee_id
|
||||
issue.assignee_id = assignee_id
|
||||
db.commit()
|
||||
db.refresh(issue)
|
||||
|
||||
# Notify assignee
|
||||
notify_user(db, assignee_id, "issue.assigned",
|
||||
f"Issue #{issue.id} assigned to you",
|
||||
f"'{issue.title}' has been assigned to you.",
|
||||
"issue", issue.id)
|
||||
|
||||
return {"issue_id": issue.id, "assignee_id": assignee_id, "title": issue.title}
|
||||
|
||||
Reference in New Issue
Block a user