Découpe par concern de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) en package `app/routers/dashboard/` : - 8 modules de routes : local_workspace 559 L (15 r.), pages_html 485 (6), account_settings 439 (16), workspace 321 (9), pages_api 240 (6), workspaces 131 (6), public 78 (1), account_api 77 (4) - _common.py (774 L) : les 15 helpers top-level INTERCALÉS dans l'ancien fichier + état (logger, _VERSION, WORKSPACE_COOKIE) - __init__.py : ordre d'enregistrement identique à l'origine, re-export complet (7 importateurs inchangés : main, board ×3, my_tasks, web_clipper, wiki, sites `_dash._render_blocks_public`, tests) + __all__ Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - segment décorateur sans sa fonction → assert `def in seg` + récupération git (corps perdus en silence à la 1ʳᵉ exécution) - collision `settings` (section vs from app.config import settings → hasattr du fromlist) → renommée account_settings - WORKSPACE_COOKIE utilisé sans import dans workspaces.py (F821) - script __all__ mangeant la fin du fichier → __init__ réécrit Reste A28 : collections.py 2 622 L, board.py 2 101 L (lot 3). suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
"""FlowDeck — Dashboard : workspaces.
|
|
|
|
Découpe A28 de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) — un module par concern, contrat inchangé.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Body, Request
|
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
|
|
from ._common import WORKSPACE_COOKIE, _get_active_workspace, _get_user_id, _sidebar_data
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["dashboard"])
|
|
|
|
|
|
|
|
|
|
@router.get("/workspaces", response_class=HTMLResponse)
|
|
def workspaces_page(request: Request):
|
|
"""Workspaces list page."""
|
|
from app.templating import ENV
|
|
env = ENV
|
|
sidebar = _sidebar_data(request, [], include_workspace=False)
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
if not user:
|
|
return RedirectResponse("/auth/login?provider=local&expired=1", status_code=302)
|
|
ctx = {**sidebar, "user": user}
|
|
# Pass active workspace for breadcrumb nav menu (null on workspaces home)
|
|
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
|
ctx["nav_workspace_id"] = ws["id"] if ws else 0
|
|
template = env.get_template("workspaces.html")
|
|
return template.render(**ctx)
|
|
|
|
|
|
|
|
|
|
@router.get("/api/workspaces")
|
|
def list_workspaces(request: Request):
|
|
"""List all workspaces for the current user."""
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
uid = user["id"] if user and user.get("id") else 1
|
|
with get_conn() as conn:
|
|
rows = conn.execute(
|
|
"SELECT w.*, (SELECT COUNT(*) FROM pages WHERE workspace_id=w.id AND collection_row_id IS NULL) as page_count "
|
|
"FROM workspaces w WHERE w.owner_id=? ORDER BY w.created_at DESC",
|
|
(uid,),
|
|
).fetchall()
|
|
workspaces = []
|
|
for r in rows:
|
|
d = dict(r)
|
|
workspaces.append(d)
|
|
active = _get_active_workspace(request, user_id=_get_user_id(request))
|
|
return {"workspaces": workspaces, "active_id": active["id"] if active else None}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/workspaces")
|
|
def create_workspace(request: Request, body: dict = Body(default={})):
|
|
"""Create a new workspace."""
|
|
name = body.get("name", "New Workspace").strip()
|
|
if not name:
|
|
return {"error": "Name required"}
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
uid = user["id"] if user and user.get("id") else 1
|
|
with get_conn() as conn:
|
|
# Ensure user exists (FK constraint)
|
|
uid_ok = conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone()
|
|
if not uid_ok:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO users (id, login, full_name, is_admin) VALUES (?, ?, ?, 1)",
|
|
(uid, user.get("login", "admin") if user else "admin",
|
|
user.get("full_name", "Admin") if user else "Admin"),
|
|
)
|
|
cursor = conn.execute(
|
|
"INSERT INTO workspaces (name, owner_id) VALUES (?, ?)",
|
|
(name, uid),
|
|
)
|
|
ws_id = cursor.lastrowid
|
|
# Add owner as member
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'owner')",
|
|
(ws_id, uid),
|
|
)
|
|
conn.commit()
|
|
return {"id": ws_id, "name": name}
|
|
|
|
|
|
|
|
|
|
@router.put("/api/workspaces/{ws_id:int}")
|
|
def rename_workspace(request: Request, ws_id: int, body: dict = Body(default={})):
|
|
"""Rename a workspace."""
|
|
name = body.get("name", "").strip()
|
|
if not name:
|
|
return {"error": "Name required"}
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE workspaces SET name=? WHERE id=?", (name, ws_id))
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/workspaces/{ws_id:int}")
|
|
def delete_workspace(request: Request, ws_id: int):
|
|
"""Delete a workspace and all its pages."""
|
|
with get_conn() as conn:
|
|
conn.execute("DELETE FROM pages WHERE workspace_id=?", (ws_id,))
|
|
conn.execute("DELETE FROM workspace_members WHERE workspace_id=?", (ws_id,))
|
|
conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,))
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/workspaces/{ws_id:int}/select")
|
|
def select_workspace(request: Request, ws_id: int):
|
|
"""Set the active workspace via cookie."""
|
|
response = JSONResponse({"status": "ok", "workspace_id": ws_id})
|
|
response.set_cookie(WORKSPACE_COOKIE, str(ws_id), max_age=86400 * 30, httponly=True, path="/")
|
|
return response
|
|
|
|
|
|
# ═══════════ Settings Page ═══════════
|