"""FlowDeck — Collections : views. 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 ._common import _current_user logger = logging.getLogger(__name__) router = APIRouter(tags=["collections"], prefix="/db") # ── v1.7.0 View Management ── @router.get("/views/{view_id}/api") def get_view_api(request: Request, view_id: int): """API: get a single view config.""" with get_conn() as conn: row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not row: raise HTTPException(status_code=404, detail="View not found") return dict(row) @router.put("/views/{view_id}/config") def update_view_config(request: Request, view_id: int, body: dict = Body(default={})): """API: update view configuration (group_by, card_size, visible_properties, etc.).""" with get_conn() as conn: existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not existing: raise HTTPException(status_code=404, detail="View not found") config = json.loads(existing["config_json"]) for key in ("group_by", "sub_group_by", "wip_limits", "card_size", "cover_property", "cover_mode", "card_properties", "visible_properties", "filters", "sorts", "filter_conjunction", "date_property", "date_range_property", "property_groups", "view_type"): if key in body: config[key] = body[key] new_type = body.get("view_type") or existing["view_type"] conn.execute( "UPDATE collection_views SET config_json=?, name=COALESCE(?, name), view_type=?, " "updated_at=CURRENT_TIMESTAMP WHERE id=?", (json.dumps(config), body.get("name"), new_type, view_id), ) conn.commit() return {"id": view_id, "status": "updated", "config": config, "view_type": new_type} @router.post("/{collection_id}/views/save-as") def save_view_as(request: Request, collection_id: int, body: dict = Body(default={})): """API: save current view state as a new named view.""" name = body.get("name", "New View") config = body.get("config", {}) user = _current_user(request) user_id = user.get("id") if user else None with get_conn() as conn: coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone() if not coll: raise HTTPException(status_code=404, detail="Collection not found") max_pos = conn.execute( "SELECT COALESCE(MAX(position), -1) + 1 FROM collection_views WHERE collection_id=?", (collection_id,), ).fetchone()[0] view_type = body.get("view_type", "table") cur = conn.execute( """INSERT INTO collection_views (collection_id, name, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?, ?)""", (collection_id, name, view_type, json.dumps(config), max_pos, user_id), ) conn.commit() new_view_id = cur.lastrowid run_event_sync(fire_event("collection.view.created", { "view_id": new_view_id, "collection_id": collection_id, "name": name, "view_type": view_type, })) return {"id": new_view_id, "name": name, "view_type": view_type, "config_json": json.dumps(config), "created_by": user_id, "status": "saved"} @router.get("/{collection_id}/views/api") def list_views_api(request: Request, collection_id: int): """API: list views for a collection visible to the current user. Shared/legacy views (``created_by IS NULL``) are visible to everyone; personal views (``created_by = user``) only to their owner. """ user = _current_user(request) user_id = user.get("id") if user else None with get_conn() as conn: if user_id is not None: rows = conn.execute( """SELECT * FROM collection_views WHERE collection_id=? AND (created_by IS NULL OR created_by=?) ORDER BY position""", (collection_id, user_id), ).fetchall() else: rows = conn.execute( "SELECT * FROM collection_views WHERE collection_id=? AND created_by IS NULL ORDER BY position", (collection_id,), ).fetchall() return {"views": [dict(r) for r in rows]} @router.delete("/views/{view_id}/api") def delete_view_api(request: Request, view_id: int): """API: delete a saved view.""" with get_conn() as conn: existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not existing: raise HTTPException(status_code=404, detail="View not found") conn.execute("DELETE FROM collection_views WHERE id=?", (view_id,)) conn.commit() return {"id": view_id, "status": "deleted"} @router.post("/views/{view_id}/duplicate") def duplicate_view_api(request: Request, view_id: int, body: dict = Body(default={})): """API: duplicate a view (config + type), owned by the current user.""" with get_conn() as conn: existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() if not existing: raise HTTPException(status_code=404, detail="View not found") max_pos = conn.execute( "SELECT COALESCE(MAX(position), -1) + 1 FROM collection_views WHERE collection_id=?", (existing["collection_id"],), ).fetchone()[0] user = _current_user(request) user_id = user.get("id") if user else None name = body.get("name") or (existing["name"] + " copy") cur = conn.execute( """INSERT INTO collection_views (collection_id, name, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?, ?)""", (existing["collection_id"], name, existing["view_type"], existing["config_json"], max_pos, user_id), ) conn.commit() dup_view_id = cur.lastrowid run_event_sync(fire_event("collection.view.created", { "view_id": dup_view_id, "collection_id": existing["collection_id"], "name": name, "view_type": existing["view_type"], })) return {"id": dup_view_id, "name": name, "view_type": existing["view_type"], "status": "duplicated"} # ── v1.8.0 Sub-items & Dependencies ──