Files
flowdeck/app/routers/collections/data_api.py
T
bruno adf56a2dd8
FlowDeck CI / docker (push) Successful in 1m54s
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m28s
refactor: A28 lot 3 — collections.py (2 622 L) → package 13 fichiers (v7.31.0)
Découpe par concern de l'ancien app/routers/collections.py (2 622 lignes,
53 endpoints / 52 fonctions) en package `app/routers/collections/` :

- 10 modules de routes : crud 337 L (6 r.), properties 322 (8),
  linked 286 (7), structure 267 (8), dashboard_views 214 (3),
  meta 197 (5), views 187 (6), pages 184 (4), data_api 122 (2),
  boards 61 (3)
- _common.py (220 L) : 8 helpers auth/permissions/validation
- _renderers.py (667 L) : 15 rendus HTML des vues + CHART_MAX_GROUPS
- __init__.py : ré-exports connus (_validate_page_properties pour
  automations ; _chart_values/_chart_aggregate/_fmt_number/_render_chart
  pour les tests) + __all__

Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE
byte-à-byte (509 chemins, ordre préservé).

Pièges rattrapés :
- docstring d'origine conservée dans le header copié → F404
  (from __future__ après un statement) → slice [1:30]
- décorateurs empilés (view_collection ×2) : segment sans def →
  skip du 2e décorateur (53 endpoints = 52 unités)
- CHART_MAX_GROUPS hors détection des helpers (F821) → import ._renderers
- test_csp_no_cdn_and_vendor lisait collections.py → balayage du package

Reste A28 : board.py 2 101 L (lot 4).

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-02 08:16:03 -04:00

123 lines
4.0 KiB
Python

"""FlowDeck — Collections : data_api.
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")
@router.get("/{collection_id}/api")
def get_collection_api(request: Request, collection_id: int):
"""API: get a single collection with its pages."""
# v6.0.0: granular collection permissions — hide restricted collections.
_require_view(collection_id, _session_user(request))
with get_conn() as conn:
collection = conn.execute(
"SELECT * FROM collections WHERE id=?", (collection_id,)
).fetchone()
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
pages = conn.execute(
"SELECT * FROM collection_pages WHERE collection_id=? ORDER BY position",
(collection_id,),
).fetchall()
views = conn.execute(
"SELECT * FROM collection_views WHERE collection_id=? ORDER BY position",
(collection_id,),
).fetchall()
return {
"collection": dict(collection),
"pages": [dict(p) for p in pages],
"views": [dict(v) for v in views],
}
@router.post("/{collection_id}/pages/api")
def create_page_api(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: create a page in a collection."""
# v6.0.0: granular collection permissions — viewer/commenter cannot create.
_require_view(collection_id, _session_user(request))
_require_edit(collection_id, _session_user(request))
title = body.get("title", "").strip()
if not title:
raise HTTPException(status_code=400, detail="title is required")
icon = body.get("icon", "📄")
property_values = body.get("properties", {})
cover_url = body.get("cover_url", "")
gitea_issue_id = body.get("gitea_issue_id")
gitea_issue_number = body.get("gitea_issue_number")
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")
_validate_page_properties(conn, collection_id, property_values)
_validate_meta_keys(conn, collection_id, property_values)
apply_auto_properties(
_collection_properties(conn, collection_id),
property_values,
_current_user(request),
is_create=True,
)
max_pos = conn.execute(
"SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages WHERE collection_id=?",
(collection_id,),
).fetchone()[0]
cur = conn.execute(
"""INSERT INTO collection_pages
(collection_id, title, icon, cover_url, position, gitea_issue_id, gitea_issue_number, property_values_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(collection_id, title, icon, cover_url, max_pos, gitea_issue_id, gitea_issue_number,
json.dumps(property_values)),
)
conn.commit()
page_id = cur.lastrowid
run_event_sync(fire_event("page.created", {
"page_id": page_id,
"collection_id": collection_id,
"title": title,
"icon": icon,
"properties": property_values,
}))
run_event_sync(fire_event("collection.page.created", {
"page_id": page_id,
"collection_id": collection_id,
"title": title,
}))
return {"id": page_id, "title": title, "status": "created"}