Files
lifeos-dev/routers/notes.py
Michael c7a07ed280 feat: return-to-project redirects from create/edit forms
When creating or editing items from a project detail tab, users now
return to that project's tab instead of the entity's own page.
Edit links pass from_project param; forms include hidden field.
Reassigning to a different project redirects to the new project.
Decisions/meetings create from project context inserts junction rows.
File uploads from project context redirect back to project files tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:28:15 +00:00

227 lines
8.2 KiB
Python

"""Notes: knowledge documents with project associations."""
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="/notes", tags=["notes"])
templates = Jinja2Templates(directory="templates")
@router.get("/")
async def list_notes(
request: Request,
domain_id: Optional[str] = None,
project_id: Optional[str] = None,
db: AsyncSession = Depends(get_db),
):
sidebar = await get_sidebar_data(db)
where_clauses = ["n.is_deleted = false"]
params = {}
if domain_id:
where_clauses.append("n.domain_id = :domain_id")
params["domain_id"] = domain_id
if project_id:
where_clauses.append("n.project_id = :project_id")
params["project_id"] = project_id
where_sql = " AND ".join(where_clauses)
result = await db.execute(text(f"""
SELECT n.*, d.name as domain_name, d.color as domain_color,
p.name as project_name
FROM notes n
LEFT JOIN domains d ON n.domain_id = d.id
LEFT JOIN projects p ON n.project_id = p.id
WHERE {where_sql}
ORDER BY n.updated_at DESC
"""), params)
items = [dict(r._mapping) for r in result]
domains_repo = BaseRepository("domains", db)
domains = await domains_repo.list()
return templates.TemplateResponse("notes.html", {
"request": request, "sidebar": sidebar, "items": items,
"domains": domains,
"current_domain_id": domain_id or "",
"current_project_id": project_id or "",
"page_title": "Notes", "active_nav": "notes",
})
@router.get("/create")
async def create_form(
request: Request,
domain_id: Optional[str] = None,
project_id: Optional[str] = None,
task_id: Optional[str] = None,
meeting_id: Optional[str] = None,
focus_id: Optional[str] = None,
db: AsyncSession = Depends(get_db),
):
sidebar = await get_sidebar_data(db)
domains_repo = BaseRepository("domains", db)
domains = await domains_repo.list()
projects_repo = BaseRepository("projects", db)
projects = await projects_repo.list()
return templates.TemplateResponse("note_form.html", {
"request": request, "sidebar": sidebar,
"domains": domains, "projects": projects,
"page_title": "New Note", "active_nav": "notes",
"item": None,
"prefill_domain_id": domain_id or "",
"prefill_project_id": project_id or "",
"prefill_task_id": task_id or "",
"prefill_meeting_id": meeting_id or "",
"prefill_focus_id": focus_id or "",
})
@router.post("/create")
async def create_note(
request: Request,
title: str = Form(...),
domain_id: Optional[str] = Form(None),
project_id: Optional[str] = Form(None),
task_id: Optional[str] = Form(None),
meeting_id: Optional[str] = Form(None),
focus_id: Optional[str] = Form(None),
body: Optional[str] = Form(None),
content_format: str = Form("rich"),
tags: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("notes", db)
data = {
"title": title,
"body": body, "content_format": content_format,
}
if domain_id and domain_id.strip():
data["domain_id"] = domain_id
if project_id and project_id.strip():
data["project_id"] = project_id
if task_id and task_id.strip():
data["task_id"] = task_id
if meeting_id and meeting_id.strip():
data["meeting_id"] = meeting_id
if focus_id and focus_id.strip():
data["focus_id"] = focus_id
if tags and tags.strip():
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
note = await repo.create(data)
if task_id and task_id.strip():
return RedirectResponse(url=f"/tasks/{task_id}?tab=notes", status_code=303)
if meeting_id and meeting_id.strip():
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=notes", status_code=303)
if focus_id and focus_id.strip():
return RedirectResponse(url=f"/focus/{focus_id}?tab=notes", status_code=303)
if project_id and project_id.strip():
return RedirectResponse(url=f"/projects/{project_id}?tab=notes", status_code=303)
return RedirectResponse(url=f"/notes/{note['id']}", status_code=303)
@router.get("/{note_id}")
async def note_detail(note_id: str, request: Request, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("notes", db)
sidebar = await get_sidebar_data(db)
item = await repo.get(note_id)
if not item:
return RedirectResponse(url="/notes", status_code=303)
domain = None
if item.get("domain_id"):
result = await db.execute(text("SELECT name, color FROM domains WHERE id = :id"), {"id": str(item["domain_id"])})
row = result.first()
domain = dict(row._mapping) if row else None
project = None
if item.get("project_id"):
result = await db.execute(text("SELECT id, name FROM projects WHERE id = :id"), {"id": str(item["project_id"])})
row = result.first()
project = dict(row._mapping) if row else None
return templates.TemplateResponse("note_detail.html", {
"request": request, "sidebar": sidebar, "item": item,
"domain": domain, "project": project,
"page_title": item["title"], "active_nav": "notes",
})
@router.get("/{note_id}/edit")
async def edit_form(note_id: str, request: Request, from_project: Optional[str] = None, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("notes", db)
sidebar = await get_sidebar_data(db)
item = await repo.get(note_id)
if not item:
return RedirectResponse(url="/notes", status_code=303)
domains_repo = BaseRepository("domains", db)
domains = await domains_repo.list()
projects_repo = BaseRepository("projects", db)
projects = await projects_repo.list()
return templates.TemplateResponse("note_form.html", {
"request": request, "sidebar": sidebar,
"domains": domains, "projects": projects,
"page_title": f"Edit Note", "active_nav": "notes",
"item": item, "prefill_domain_id": "", "prefill_project_id": "",
"from_project": from_project or "",
})
@router.post("/{note_id}/edit")
async def update_note(
note_id: str,
title: str = Form(...),
domain_id: str = Form(...),
project_id: Optional[str] = Form(None),
body: Optional[str] = Form(None),
content_format: str = Form("rich"),
tags: Optional[str] = Form(None),
from_project: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("notes", db)
data = {
"title": title, "domain_id": domain_id, "body": body,
"content_format": content_format,
"project_id": project_id if project_id and project_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(note_id, data)
new_project = data.get("project_id")
if from_project and from_project.strip():
if new_project and new_project != from_project:
return RedirectResponse(url=f"/projects/{new_project}?tab=notes", status_code=303)
return RedirectResponse(url=f"/projects/{from_project}?tab=notes", status_code=303)
return RedirectResponse(url=f"/notes/{note_id}", status_code=303)
@router.post("/{note_id}/delete")
async def delete_note(note_id: str, request: Request, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("notes", db)
await repo.soft_delete(note_id)
referer = request.headers.get("referer", "/notes")
return RedirectResponse(url=referer, status_code=303)
@router.post("/reorder")
async def reorder_note(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("notes", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/notes"), status_code=303)