feat(v4.2.0): Database Templates & Dashboards
- Enhance page_templates: add description, is_recurring, recurrence_rule columns
- New table collection_dashboards for widget-based dashboard layouts
- API: PUT/DELETE /workspace/collections/{id}/templates/page/{tid} — template CRUD
- API: CRUD /workspace/collections/{id}/dashboards (create/list/update/delete)
- Dashboard layout_json supports columns + widgets with view_id/x/y/w/h
- 6 new tests (159 total)
This commit is contained in:
@@ -487,6 +487,34 @@ def init_db():
|
||||
pass
|
||||
conn.commit()
|
||||
|
||||
# v4.2.0: Collection Templates (enhanced) + Dashboards
|
||||
# Add description, is_recurring, recurrence_rule to page_templates
|
||||
try:
|
||||
conn.execute("ALTER TABLE page_templates ADD COLUMN description TEXT DEFAULT ''")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE page_templates ADD COLUMN is_recurring BOOLEAN NOT NULL DEFAULT 0")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE page_templates ADD COLUMN recurrence_rule TEXT DEFAULT ''")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# Dashboard: combinaison de vues/widgets sur une page
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS collection_dashboards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL DEFAULT 'Dashboard',
|
||||
layout_json TEXT NOT NULL DEFAULT '{"columns":1,"widgets":[]}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_conn():
|
||||
|
||||
@@ -296,6 +296,115 @@ async def apply_page_template(request: Request, collection_id: int, tid: int):
|
||||
return {"id": cur.lastrowid, "status": "created"}
|
||||
|
||||
|
||||
@router.put("/collections/{collection_id}/templates/page/{tid}")
|
||||
async def update_page_template(request: Request, collection_id: int, tid: int):
|
||||
"""Update a page template — name, properties, content, recurrence."""
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
with get_conn() as conn:
|
||||
tmpl = conn.execute(
|
||||
"SELECT * FROM page_templates WHERE id=? AND collection_id=?", (tid, collection_id)
|
||||
).fetchone()
|
||||
if not tmpl:
|
||||
raise HTTPException(404, "Template not found")
|
||||
|
||||
name = body.get("name", tmpl["name"])
|
||||
tmpl_dict = dict(tmpl)
|
||||
description = body.get("description", tmpl_dict.get("description", ""))
|
||||
property_values_json = json.dumps(body.get("properties", json.loads(tmpl["property_values_json"])))
|
||||
content_json = json.dumps(body.get("content", json.loads(tmpl_dict.get("content_json", "[]"))))
|
||||
is_recurring = int(body.get("is_recurring", tmpl_dict.get("is_recurring", 0)))
|
||||
recurrence_rule = body.get("recurrence_rule", tmpl_dict.get("recurrence_rule", ""))
|
||||
|
||||
conn.execute(
|
||||
"""UPDATE page_templates SET name=?, description=?, property_values_json=?,
|
||||
content_json=?, is_recurring=?, recurrence_rule=? WHERE id=?""",
|
||||
(name, description, property_values_json, content_json, is_recurring, recurrence_rule, tid),
|
||||
)
|
||||
conn.commit()
|
||||
return {"id": tid, "status": "updated"}
|
||||
|
||||
|
||||
@router.delete("/collections/{collection_id}/templates/page/{tid}")
|
||||
async def delete_page_template(request: Request, collection_id: int, tid: int):
|
||||
"""Delete a page template."""
|
||||
with get_conn() as conn:
|
||||
tmpl = conn.execute(
|
||||
"SELECT * FROM page_templates WHERE id=? AND collection_id=?", (tid, collection_id)
|
||||
).fetchone()
|
||||
if not tmpl:
|
||||
raise HTTPException(404, "Template not found")
|
||||
conn.execute("DELETE FROM page_templates WHERE id=?", (tid,))
|
||||
conn.commit()
|
||||
return {"id": tid, "status": "deleted"}
|
||||
|
||||
|
||||
# ── v4.2.0: Dashboards ──
|
||||
|
||||
@router.get("/collections/{collection_id}/dashboards")
|
||||
async def list_dashboards(request: Request, collection_id: int):
|
||||
"""List all dashboards for a collection."""
|
||||
with get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM collection_dashboards WHERE collection_id=? ORDER BY name", (collection_id,)
|
||||
).fetchall()
|
||||
return {"dashboards": [dict(r) for r in rows]}
|
||||
|
||||
|
||||
@router.post("/collections/{collection_id}/dashboards")
|
||||
async def create_dashboard(request: Request, collection_id: int):
|
||||
"""Create a new dashboard for a collection."""
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
name = body.get("name", "Dashboard").strip()
|
||||
layout = json.dumps(body.get("layout", {"columns": 1, "widgets": []}))
|
||||
|
||||
with get_conn() as conn:
|
||||
coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone()
|
||||
if not coll:
|
||||
raise HTTPException(404, "Collection not found")
|
||||
cur = conn.execute(
|
||||
"INSERT INTO collection_dashboards (collection_id, name, layout_json) VALUES (?,?,?)",
|
||||
(collection_id, name, layout),
|
||||
)
|
||||
conn.commit()
|
||||
return {"id": cur.lastrowid, "name": name, "status": "created"}
|
||||
|
||||
|
||||
@router.put("/collections/{collection_id}/dashboards/{did}")
|
||||
async def update_dashboard(request: Request, collection_id: int, did: int):
|
||||
"""Update a dashboard — name or layout (widgets grid)."""
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
with get_conn() as conn:
|
||||
dash = conn.execute(
|
||||
"SELECT * FROM collection_dashboards WHERE id=? AND collection_id=?", (did, collection_id)
|
||||
).fetchone()
|
||||
if not dash:
|
||||
raise HTTPException(404, "Dashboard not found")
|
||||
|
||||
name = body.get("name", dash["name"])
|
||||
layout = json.dumps(body.get("layout", json.loads(dash["layout_json"])))
|
||||
|
||||
conn.execute(
|
||||
"UPDATE collection_dashboards SET name=?, layout_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
(name, layout, did),
|
||||
)
|
||||
conn.commit()
|
||||
return {"id": did, "status": "updated"}
|
||||
|
||||
|
||||
@router.delete("/collections/{collection_id}/dashboards/{did}")
|
||||
async def delete_dashboard(request: Request, collection_id: int, did: int):
|
||||
"""Delete a dashboard."""
|
||||
with get_conn() as conn:
|
||||
dash = conn.execute(
|
||||
"SELECT * FROM collection_dashboards WHERE id=? AND collection_id=?", (did, collection_id)
|
||||
).fetchone()
|
||||
if not dash:
|
||||
raise HTTPException(404, "Dashboard not found")
|
||||
conn.execute("DELETE FROM collection_dashboards WHERE id=?", (did,))
|
||||
conn.commit()
|
||||
return {"id": did, "status": "deleted"}
|
||||
|
||||
|
||||
# ── CSV Import/Export ──
|
||||
|
||||
@router.post("/collections/{collection_id}/import/csv")
|
||||
|
||||
Reference in New Issue
Block a user