"""FlowDeck — Public API v2 : views. Découpe A28 de l'ancien app/routers/api_v2.py (2 110 lignes, 115 routes) — un module par concern, contrat inchangé (Bearer+scopes, pagination, RFC7807, audit + idempotency). """ from __future__ import annotations import json import logging from fastapi import APIRouter, Body, Header, HTTPException, Request from app.db import get_conn from app.services.api_v2_helpers import ( # noqa: F401 — require_scope est utilisé par les handlers audit_log, check_idempotency, check_v2_rate_limit, get_bearer_user, has_scope, paginate_headers, parse_pagination, require_scope, row_to_dict, store_idempotency, to_iso8601, validate_scopes_input, ) from app.services.automations import fire_event as _fire_event from app.services.automations import run_event_sync from ._common import _v2_rate_check logger = logging.getLogger(__name__) router = APIRouter(tags=["api-v2"]) @router.post("/collections/{collection_id}/views") def create_view_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) name = (body.get("name") or "New View").strip() vtype = body.get("view_type") or body.get("type") or "table" config = body.get("config") or body.get("config_json") or {} with get_conn() as conn: if not conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone(): raise HTTPException(404, "Collection not found") max_pos = conn.execute("SELECT COALESCE(MAX(position), -1)+1 FROM collection_views WHERE collection_id=?", (collection_id,)).fetchone()[0] cur = conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?, ?)", (collection_id, name, vtype, json.dumps(config), max_pos, user["id"])) vid = cur.lastrowid conn.commit() audit_log(user, "view.create", "view", vid, name, request) try: run_event_sync(_fire_event("collection.view.created", {"view_id": vid, "collection_id": collection_id, "name": name, "view_type": vtype})) except Exception: logger.exception("create_view_v2") return {"id": vid, "name": name, "view_type": vtype, "status": "created"} @router.patch("/views/{view_id}") def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not row: raise HTTPException(404, "View not found") cfg = json.loads(row["config_json"] or "{}") if "config" in body: cfg.update(body["config"]) elif "config_json" in body: try: cfg.update(json.loads(body["config_json"]) if isinstance(body["config_json"], str) else body["config_json"]) except Exception: logger.exception("patch_view_v2") # also flat keys for k in ("group_by", "sub_group_by", "wip_limits", "card_size", "cover_property", "cover_mode", "card_properties", "visible_properties", "filters", "sorts", "date_property"): if k in body: cfg[k] = body[k] name = body.get("name", row["name"]) vtype = body.get("view_type") or body.get("type") or row["view_type"] conn.execute("UPDATE collection_views SET name=?, view_type=?, config_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (name, vtype, json.dumps(cfg), view_id)) conn.commit() audit_log(user, "view.update", "view", view_id, "", request) return {"id": view_id, "status": "updated"} @router.delete("/views/{view_id}") def delete_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: if not conn.execute("SELECT id FROM collection_views WHERE id=?", (view_id,)).fetchone(): raise HTTPException(404, "View not found") conn.execute("DELETE FROM collection_views WHERE id=?", (view_id,)) conn.commit() audit_log(user, "view.delete", "view", view_id, "", request) return {"id": view_id, "status": "deleted"} @router.post("/views/{view_id}/save-as") def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) name = (body.get("name") or "").strip() or "Copy" with get_conn() as conn: row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not row: raise HTTPException(404, "View not found") max_pos = conn.execute("SELECT COALESCE(MAX(position), -1)+1 FROM collection_views WHERE collection_id=?", (row["collection_id"],)).fetchone()[0] cur = conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?, ?)", (row["collection_id"], name, row["view_type"], row["config_json"], max_pos, user["id"])) nid = cur.lastrowid conn.commit() return {"id": nid, "name": name, "status": "created"} @router.get("/collections/{collection_id}/dashboards") def list_dashboards_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: rows = conn.execute("SELECT * FROM collection_dashboards WHERE collection_id=? ORDER BY created_at", (collection_id,)).fetchall() return {"dashboards": [row_to_dict(r) for r in rows]} @router.post("/collections/{collection_id}/dashboards") def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) name = (body.get("name") or "Dashboard").strip() layout = body.get("layout") or body.get("layout_json") or {"columns": 1, "widgets": []} with get_conn() as conn: cur = conn.execute("INSERT INTO collection_dashboards (collection_id, name, layout_json) VALUES (?, ?, ?)", (collection_id, name, json.dumps(layout))) did = cur.lastrowid conn.commit() return {"id": did, "name": name, "status": "created"} @router.patch("/dashboards/{dashboard_id}") def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: row = conn.execute("SELECT * FROM collection_dashboards WHERE id=?", (dashboard_id,)).fetchone() if not row: raise HTTPException(404, "Dashboard not found") name = body.get("name", row["name"]) layout = body.get("layout") or body.get("layout_json") if layout is not None: layout_json = json.dumps(layout) else: layout_json = row["layout_json"] conn.execute("UPDATE collection_dashboards SET name=?, layout_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (name, layout_json, dashboard_id)) conn.commit() return {"id": dashboard_id, "status": "updated"} @router.delete("/dashboards/{dashboard_id}") def delete_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: conn.execute("DELETE FROM collection_dashboards WHERE id=?", (dashboard_id,)) conn.commit() return {"id": dashboard_id, "status": "deleted"}