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
123 lines
4.7 KiB
Python
123 lines
4.7 KiB
Python
"""FlowDeck — Board : page_media.
|
|
|
|
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 app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
from app.services.automations import fire_event, run_event_sync
|
|
|
|
from ._common import _store_uploaded_file, _ws_id_for
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["board"], prefix="/board")
|
|
|
|
|
|
|
|
|
|
# ═══════════ v5.4.0: Page & collection duplication ═══════════
|
|
|
|
|
|
@router.post("/api/pages/{page_id}/duplicate")
|
|
def duplicate_page(request: Request, page_id: int):
|
|
"""Duplicate a page (block/markdown content included) as a sibling."""
|
|
SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
with get_conn() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM pages WHERE id=? AND deleted_at IS NULL", (page_id,)
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Page not found")
|
|
page = dict(row)
|
|
|
|
def copy_tree(src_id: int, parent_id) -> int:
|
|
with get_conn() as conn:
|
|
conn.execute("SELECT * FROM pages WHERE id=?", (src_id,)).fetchone()
|
|
cur = conn.execute(
|
|
"INSERT INTO pages (workspace, workspace_id, title, content, content_format, "
|
|
"parent_section, parent_id, sort_order, share_mode, published, is_published, "
|
|
"publish_slug, is_shared, cover_url, page_icon, created_at, updated_at) "
|
|
"SELECT workspace, workspace_id, title || ' copy', content, content_format, "
|
|
"parent_section, ?, sort_order, share_mode, 0, is_published, '', is_shared, "
|
|
"cover_url, page_icon, created_at, updated_at FROM pages WHERE id=?",
|
|
(parent_id, src_id),
|
|
)
|
|
new_id = cur.lastrowid
|
|
conn.commit()
|
|
for child in conn.execute(
|
|
"SELECT id FROM pages WHERE parent_id=? ", (src_id,)
|
|
).fetchall():
|
|
copy_tree(child["id"], new_id)
|
|
return new_id
|
|
|
|
new_id = copy_tree(page_id, page.get("parent_id"))
|
|
title = (page.get("title") or "Untitled") + " copy"
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE pages SET title=? WHERE id=?", (title, new_id))
|
|
conn.commit()
|
|
run_event_sync(fire_event("page.created", {"page_id": new_id, "title": title,
|
|
"workspace": page.get("workspace")}))
|
|
return {"status": "ok", "id": new_id, "title": title}
|
|
|
|
|
|
# ═══════════ v5.5.0: Cover & icon ═══════════
|
|
|
|
|
|
|
|
|
|
@router.post("/api/pages/{page_id}/cover")
|
|
async def set_page_cover(request: Request, page_id: int):
|
|
"""v5.5.0: upload an image cover for a page.
|
|
|
|
JSON body {cover_url} accepts an external URL; multipart ``file`` uploads
|
|
an image stored in the workspace's uploads directory.
|
|
"""
|
|
ctype = (request.headers.get("content-type") or "").lower()
|
|
if ctype.startswith("application/json"):
|
|
body = await request.json()
|
|
cover_url = (body.get("cover_url") or "").strip()
|
|
if not cover_url:
|
|
raise HTTPException(400, "cover_url required")
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE pages SET cover_url=? WHERE id=?", (cover_url, page_id))
|
|
conn.commit()
|
|
return {"status": "ok", "page_id": page_id, "cover_url": cover_url}
|
|
ws_id = _ws_id_for(request, page_id)
|
|
meta = await _store_uploaded_file(request, ws_id)
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE pages SET cover_url=? WHERE id=?", (meta["file_url"], page_id))
|
|
conn.commit()
|
|
return {"status": "ok", "page_id": page_id, "cover_url": meta["file_url"]}
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/pages/{page_id}/cover")
|
|
def remove_page_cover(request: Request, page_id: int):
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE pages SET cover_url='' WHERE id=?", (page_id,))
|
|
conn.commit()
|
|
return {"status": "ok", "page_id": page_id}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/pages/{page_id}/icon")
|
|
def set_page_icon(request: Request, page_id: int, body: dict = Body(default={})):
|
|
"""v5.5.0: set a page emoji/icon label (or a custom-emoji image URL)."""
|
|
icon = (body.get("icon") or "").strip()
|
|
if len(icon) > 512:
|
|
raise HTTPException(400, "icon too long")
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE pages SET page_icon=? WHERE id=?", (icon, page_id))
|
|
conn.commit()
|
|
return {"status": "ok", "page_id": page_id, "icon": icon}
|
|
|
|
|
|
# ═══════════ v5.4.0: Import (Markdown / .md / Notion .zip) ═══════════
|