refactor: A28 TERMINÉ — board.py (2 101 L) → package 14 fichiers (v7.32.0)
Lot 4/4 de l'A28 (god files) : l'ancien app/routers/board.py (2 101 lignes, 53 routes) devient le package `app/routers/board/` : - 12 modules de routes : pages 271 L (7 r.), page_api 229 (5), board_views 223 (8), page_ops 176 (3), sharing 175 (10), synced 144 (8), page_media 122 (4), import_ 85 (2), wiki 76 (2), library 66 (1), embed 64 (2), sync 51 (1) - _common.py (878 L) : 23 helpers dont 4 async + les 4 constantes (STATUS_COLORS, STATUS_LABELS, AI_KEYWORD_COLORS, _REPO_REF_RE) - __init__.py : __all__ complet — importateurs inchangés (api.py ×4 top-level, webhooks top-level, dashboard ×5 lazy, tests ×4) Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - constantes d'état oubliées dans _common à la 1ʳᵉ passe (F821 + ImportError au chargement) → ré-insérées avec les valeurs exactes - docstring du header copié → F404 → slice [1:21] - helpers `async def` non détectés par `def ` seul A28 TERMINÉ en 4 lots : api_v2 (7.29.0), dashboard (7.30.0), collections (7.31.0), board (7.32.0) — 0 changement d'URL sur les 4. suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
"""FlowDeck — Board : sharing.
|
||||
|
||||
Découpe A28 de l'ancien app/routers/board.py (2 101 lignes, 53 routes) — un module par concern, contrat inchangé.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.auth.session import SessionManager
|
||||
from app.db import get_conn
|
||||
from app.services.automations import fire_event, run_event_sync
|
||||
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
|
||||
|
||||
from ._common import _sidebar_data
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["board"], prefix="/board")
|
||||
|
||||
|
||||
|
||||
# ═══════════ Favorites API ═══════════
|
||||
|
||||
@router.get("/api/favorites")
|
||||
def list_favorites(request: Request):
|
||||
"""List favorited page IDs 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 page_id FROM favorites WHERE user_id=? ORDER BY position", (uid,)
|
||||
).fetchall()
|
||||
return {"favorites": [r["page_id"] for r in rows]}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/api/favorites/{page_id:int}")
|
||||
def add_favorite(request: Request, page_id: int):
|
||||
"""Add a page to favorites."""
|
||||
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:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)
|
||||
).fetchone()
|
||||
if not existing:
|
||||
pos = conn.execute(
|
||||
"SELECT COALESCE(MAX(position), -1) + 1 FROM favorites WHERE user_id=?", (uid,)
|
||||
).fetchone()[0]
|
||||
conn.execute(
|
||||
"INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,?)",
|
||||
(uid, page_id, pos),
|
||||
)
|
||||
conn.commit()
|
||||
try:
|
||||
run_event_sync(fire_event("favorite.added", {"page_id": page_id, "user_id": uid}))
|
||||
except Exception:
|
||||
logger.exception("add_favorite")
|
||||
return {"status": "added", "page_id": page_id}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.delete("/api/favorites/{page_id:int}")
|
||||
def remove_favorite(request: Request, page_id: int):
|
||||
"""Remove a page from favorites."""
|
||||
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:
|
||||
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id))
|
||||
conn.commit()
|
||||
try:
|
||||
run_event_sync(fire_event("favorite.removed", {"page_id": page_id, "user_id": uid}))
|
||||
except Exception:
|
||||
logger.exception("remove_favorite")
|
||||
return {"status": "removed", "page_id": page_id}
|
||||
|
||||
# ═══════════ Share API ═══════════
|
||||
|
||||
|
||||
|
||||
# ═══════════ Share API ═══════════
|
||||
|
||||
@router.post("/api/share/{page_id:int}")
|
||||
def update_share(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""Save share settings for a page."""
|
||||
mode = body.get("mode", "private")
|
||||
published = body.get("published", False)
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"UPDATE pages SET share_mode=?, published=? WHERE id=?",
|
||||
(mode, 1 if published else 0, page_id),
|
||||
)
|
||||
conn.commit()
|
||||
return {"status": "ok", "share_mode": mode, "published": published}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id:int}/publish")
|
||||
def publish_page(request: Request, page_id: int):
|
||||
"""Publish a page to the web (generates publish_slug)."""
|
||||
if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
slug, title = publish(page_id)
|
||||
run_event_sync(fire_published(page_id, slug))
|
||||
return {"is_published": True, "publish_slug": slug, "title": title}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.delete("/api/pages/{page_id:int}/publish")
|
||||
def unpublish_page(request: Request, page_id: int):
|
||||
"""Unpublish a page from the web."""
|
||||
if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
unpublish(page_id)
|
||||
run_event_sync(fire_unpublished(page_id))
|
||||
return {"is_published": False}
|
||||
|
||||
|
||||
# ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════
|
||||
|
||||
|
||||
|
||||
|
||||
# ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════
|
||||
|
||||
@router.get("/api/trash")
|
||||
def list_trash(request: Request):
|
||||
with get_conn() as conn:
|
||||
rows = conn.execute("SELECT id, title, workspace, parent_id, deleted_at FROM pages WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC").fetchall()
|
||||
return [{"id": r["id"], "name": r["title"] or "Untitled", "icon": "📄", "path": r["workspace"] or "Private", "deleted_at": r["deleted_at"]} for r in rows]
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/api/trash/{page_id}/restore")
|
||||
def restore_page(request: Request, page_id: int):
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET deleted_at=NULL WHERE id=?", (page_id,))
|
||||
conn.commit()
|
||||
try:
|
||||
run_event_sync(fire_event("page.restored", {"page_id": page_id}))
|
||||
except Exception:
|
||||
logger.exception("restore_page")
|
||||
return {"status": "ok", "restored": page_id}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.delete("/api/trash/{page_id}")
|
||||
def permanent_delete(request: Request, page_id: int):
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET parent_id=NULL WHERE parent_id=?", (page_id,))
|
||||
conn.execute("DELETE FROM pages WHERE id=? AND deleted_at IS NOT NULL", (page_id,))
|
||||
conn.commit()
|
||||
return {"status": "ok", "deleted": page_id}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/trash", response_class=HTMLResponse)
|
||||
def trash_page(request: Request):
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
template = env.get_template("trash.html")
|
||||
return template.render(**_sidebar_data(request))
|
||||
|
||||
|
||||
# ═══════════════ v5.14.0: Synced Blocks ─═══════════════════
|
||||
# These routes MUST be registered before the catch-all /{owner}/{repo} below.
|
||||
Reference in New Issue
Block a user