"""FlowDeck — Collections : pages. Découpe A28 de l'ancien app/routers/collections.py (2 622 lignes, 53 routes) — un module par concern, contrat inchangé. """ from __future__ import annotations import json import logging from fastapi import APIRouter, Body, HTTPException, Request from app.db import get_conn from app.services.automations import fire_event, run_event_sync from app.services.property_types import ( apply_auto_properties, ) from ._common import ( _collection_properties, _current_user, _require_edit, _require_view, _session_user, _validate_meta_keys, _validate_page_properties, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["collections"], prefix="/db") # ── Page CRUD (standalone, BEFORE collection wildcards) ── @router.get("/pages/{page_id}/api") def get_page_api(request: Request, page_id: int): """API: get a single page.""" with get_conn() as conn: page = conn.execute( "SELECT * FROM collection_pages WHERE id=?", (page_id,) ).fetchone() if not page: raise HTTPException(status_code=404, detail="Page not found") # v6.0.0: granular collection/page permissions. _require_view(page["collection_id"], _session_user(request)) return dict(page) @router.get("/pages/{page_id}/open/api") def open_row_page_api(request: Request, page_id: int): """v6.5.0 — content page of a database row (lazy-created). Any DB view (table/board/gallery/list/calendar) opens a row through this endpoint: it returns the shadow ``pages`` id whose full page editor carries the row's block content (synced blocks included). """ with get_conn() as conn: row = conn.execute( "SELECT collection_id FROM collection_pages WHERE id=?", (page_id,), ).fetchone() if not row: raise HTTPException(status_code=404, detail="Page not found") coll_id = row["collection_id"] # v6.0.0: granular collection permissions (same gate as the row itself). _require_view(coll_id, _session_user(request)) from app.services.row_pages import ensure_row_page try: content_page_id = ensure_row_page(page_id) except KeyError: raise HTTPException(status_code=404, detail="Page not found") from None return {"page_id": content_page_id, "row_id": page_id} @router.put("/pages/{page_id}/api") def update_page_api(request: Request, page_id: int, body: dict = Body(default={})): """API: update a page's properties.""" with get_conn() as conn: existing = conn.execute( "SELECT * FROM collection_pages WHERE id=?", (page_id,) ).fetchone() if not existing: raise HTTPException(status_code=404, detail="Page not found") # v6.0.0: granular collection/page permissions. _require_edit(existing["collection_id"], _session_user(request)) title = body.get("title", existing["title"]) icon = body.get("icon", existing["icon"]) cover_url = body.get("cover_url", existing["cover_url"] if "cover_url" in existing.keys() else "") position = body.get("position", existing["position"]) parent_id = body.get("parent_id", existing["parent_id"]) try: stored = json.loads(existing["property_values_json"]) except (json.JSONDecodeError, TypeError): stored = {} if "properties" in body: # Partial PATCH semantics: merge submitted values over stored ones. props = dict(stored) props.update(body["properties"]) else: props = stored _validate_page_properties(conn, existing["collection_id"], props, exclude_page_id=page_id) _validate_meta_keys(conn, existing["collection_id"], props) apply_auto_properties( _collection_properties(conn, existing["collection_id"]), props, _current_user(request), is_create=False, ) property_values = json.dumps(props) conn.execute( """UPDATE collection_pages SET title=?, icon=?, cover_url=?, position=?, parent_id=?, property_values_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?""", (title, icon, cover_url, position, parent_id, property_values, page_id), ) # v6.5.0: keep the row's content page title in sync (row → page). from app.services.row_pages import sync_row_title_to_page sync_row_title_to_page(conn, page_id) conn.commit() run_event_sync(fire_event("page.updated", { "page_id": page_id, "collection_id": existing["collection_id"], "title": title, "icon": icon, "properties": props, })) run_event_sync(fire_event("collection.page.updated", { "page_id": page_id, "collection_id": existing["collection_id"], "title": title, })) # Notify newly assigned people (person properties) — v5.8.0. from app.services.notifications import notify_assignment user = _current_user(request) notify_assignment(existing["collection_id"], page_id, title, stored, props, user.get("id")) return {"id": page_id, "status": "updated"} @router.delete("/pages/{page_id}/api") def delete_page_api(request: Request, page_id: int): """API: delete a page from its collection.""" with get_conn() as conn: existing = conn.execute( "SELECT * FROM collection_pages WHERE id=?", (page_id,) ).fetchone() if not existing: raise HTTPException(status_code=404, detail="Page not found") # v6.0.0: granular collection/page permissions. _require_edit(existing["collection_id"], _session_user(request)) conn.execute("DELETE FROM collection_pages WHERE id=?", (page_id,)) conn.commit() run_event_sync(fire_event("page.deleted", { "page_id": page_id, "collection_id": existing["collection_id"], "title": existing["title"], })) run_event_sync(fire_event("collection.page.deleted", { "page_id": page_id, "collection_id": existing["collection_id"], })) return {"id": page_id, "status": "deleted"} # ── Gitea Board Compatibility (BEFORE {collection_id} wildcards) ──