Initial commit
This commit is contained in:
218
routers/decisions.py
Normal file
218
routers/decisions.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""Decisions: knowledge base of decisions with rationale, status, and supersession."""
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from typing import Optional
|
||||
|
||||
from core.database import get_db
|
||||
from core.base_repository import BaseRepository
|
||||
from core.sidebar import get_sidebar_data
|
||||
|
||||
router = APIRouter(prefix="/decisions", tags=["decisions"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_decisions(
|
||||
request: Request,
|
||||
status: Optional[str] = None,
|
||||
impact: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
where_clauses = ["d.is_deleted = false"]
|
||||
params = {}
|
||||
if status:
|
||||
where_clauses.append("d.status = :status")
|
||||
params["status"] = status
|
||||
if impact:
|
||||
where_clauses.append("d.impact = :impact")
|
||||
params["impact"] = impact
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
|
||||
result = await db.execute(text(f"""
|
||||
SELECT d.*, m.title as meeting_title
|
||||
FROM decisions d
|
||||
LEFT JOIN meetings m ON d.meeting_id = m.id
|
||||
WHERE {where_sql}
|
||||
ORDER BY d.created_at DESC
|
||||
"""), params)
|
||||
items = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("decisions.html", {
|
||||
"request": request, "sidebar": sidebar, "items": items,
|
||||
"current_status": status or "",
|
||||
"current_impact": impact or "",
|
||||
"page_title": "Decisions", "active_nav": "decisions",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/create")
|
||||
async def create_form(
|
||||
request: Request,
|
||||
meeting_id: Optional[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
# Meetings for linking
|
||||
result = await db.execute(text("""
|
||||
SELECT id, title, meeting_date FROM meetings
|
||||
WHERE is_deleted = false ORDER BY meeting_date DESC LIMIT 50
|
||||
"""))
|
||||
meetings = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("decision_form.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"meetings": meetings,
|
||||
"page_title": "New Decision", "active_nav": "decisions",
|
||||
"item": None,
|
||||
"prefill_meeting_id": meeting_id or "",
|
||||
"prefill_task_id": task_id or "",
|
||||
})
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_decision(
|
||||
request: Request,
|
||||
title: str = Form(...),
|
||||
rationale: Optional[str] = Form(None),
|
||||
status: str = Form("proposed"),
|
||||
impact: str = Form("medium"),
|
||||
decided_at: Optional[str] = Form(None),
|
||||
meeting_id: Optional[str] = Form(None),
|
||||
task_id: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("decisions", db)
|
||||
data = {
|
||||
"title": title, "status": status, "impact": impact,
|
||||
"rationale": rationale,
|
||||
}
|
||||
if decided_at and decided_at.strip():
|
||||
data["decided_at"] = decided_at
|
||||
if meeting_id and meeting_id.strip():
|
||||
data["meeting_id"] = meeting_id
|
||||
if task_id and task_id.strip():
|
||||
data["task_id"] = task_id
|
||||
if tags and tags.strip():
|
||||
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
|
||||
decision = await repo.create(data)
|
||||
if task_id and task_id.strip():
|
||||
return RedirectResponse(url=f"/tasks/{task_id}?tab=decisions", status_code=303)
|
||||
return RedirectResponse(url=f"/decisions/{decision['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/{decision_id}")
|
||||
async def decision_detail(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("decisions", db)
|
||||
sidebar = await get_sidebar_data(db)
|
||||
item = await repo.get(decision_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/decisions", status_code=303)
|
||||
|
||||
# Meeting info
|
||||
meeting = None
|
||||
if item.get("meeting_id"):
|
||||
result = await db.execute(text(
|
||||
"SELECT id, title, meeting_date FROM meetings WHERE id = :id"
|
||||
), {"id": str(item["meeting_id"])})
|
||||
row = result.first()
|
||||
meeting = dict(row._mapping) if row else None
|
||||
|
||||
# Superseded by
|
||||
superseded_by = None
|
||||
if item.get("superseded_by_id"):
|
||||
result = await db.execute(text(
|
||||
"SELECT id, title FROM decisions WHERE id = :id"
|
||||
), {"id": str(item["superseded_by_id"])})
|
||||
row = result.first()
|
||||
superseded_by = dict(row._mapping) if row else None
|
||||
|
||||
# Decisions that this one supersedes
|
||||
result = await db.execute(text("""
|
||||
SELECT id, title FROM decisions
|
||||
WHERE superseded_by_id = :did AND is_deleted = false
|
||||
"""), {"did": decision_id})
|
||||
supersedes = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("decision_detail.html", {
|
||||
"request": request, "sidebar": sidebar, "item": item,
|
||||
"meeting": meeting, "superseded_by": superseded_by,
|
||||
"supersedes": supersedes,
|
||||
"page_title": item["title"], "active_nav": "decisions",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/{decision_id}/edit")
|
||||
async def edit_form(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("decisions", db)
|
||||
sidebar = await get_sidebar_data(db)
|
||||
item = await repo.get(decision_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/decisions", status_code=303)
|
||||
|
||||
result = await db.execute(text("""
|
||||
SELECT id, title, meeting_date FROM meetings
|
||||
WHERE is_deleted = false ORDER BY meeting_date DESC LIMIT 50
|
||||
"""))
|
||||
meetings = [dict(r._mapping) for r in result]
|
||||
|
||||
# Other decisions for supersession
|
||||
result = await db.execute(text("""
|
||||
SELECT id, title FROM decisions
|
||||
WHERE is_deleted = false AND id != :did ORDER BY created_at DESC LIMIT 50
|
||||
"""), {"did": decision_id})
|
||||
other_decisions = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("decision_form.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"meetings": meetings, "other_decisions": other_decisions,
|
||||
"page_title": "Edit Decision", "active_nav": "decisions",
|
||||
"item": item,
|
||||
"prefill_meeting_id": "",
|
||||
})
|
||||
|
||||
|
||||
@router.post("/{decision_id}/edit")
|
||||
async def update_decision(
|
||||
decision_id: str,
|
||||
title: str = Form(...),
|
||||
rationale: Optional[str] = Form(None),
|
||||
status: str = Form("proposed"),
|
||||
impact: str = Form("medium"),
|
||||
decided_at: Optional[str] = Form(None),
|
||||
meeting_id: Optional[str] = Form(None),
|
||||
superseded_by_id: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("decisions", db)
|
||||
data = {
|
||||
"title": title, "status": status, "impact": impact,
|
||||
"rationale": rationale if rationale and rationale.strip() else None,
|
||||
"decided_at": decided_at if decided_at and decided_at.strip() else None,
|
||||
"meeting_id": meeting_id if meeting_id and meeting_id.strip() else None,
|
||||
"superseded_by_id": superseded_by_id if superseded_by_id and superseded_by_id.strip() else None,
|
||||
}
|
||||
if tags and tags.strip():
|
||||
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
else:
|
||||
data["tags"] = None
|
||||
|
||||
await repo.update(decision_id, data)
|
||||
return RedirectResponse(url=f"/decisions/{decision_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{decision_id}/delete")
|
||||
async def delete_decision(decision_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("decisions", db)
|
||||
await repo.soft_delete(decision_id)
|
||||
return RedirectResponse(url="/decisions", status_code=303)
|
||||
Reference in New Issue
Block a user