Retour utilisateur : "il faut implementer un systeme de page". Un support est desormais compose de PLUSIEURS pages (_document_pages), chacune un document independant affiche seul sur le canevas -- chaque element appartient a exactement une page via page_id (document_engine/ elements/*, routes/document/document_element_add.py/document_render.py revalident desormais un page_id explicite). Migration automatique et silencieuse pour les supports crees avant cette fonctionnalite (db/supports/ensure_document_pages_schema.py, meme convention que les ensure_X_schema.py existants) : leurs elements deviennent tous les enfants d'une "Page 1" creee a la volee, aucune perte de contenu. Nouveau paquet document_engine/pages/ (add/list/get/rename/move/delete) et 4 routes dediees (routes/document/document_page_*.py) -- supprimer la DERNIERE page restante est refuse (garde-fou pose a la route, meme decoupage que routes/game/screens/screen_delete.py cote jeu, jamais dans la fonction bas niveau). Cote editeur : une bande d'ONGLETS au-dessus du canevas (jamais un panneau lateral, choix explicite de l'utilisateur) -- clic pour changer de page, double-clic pour renommer (contenteditable), glisser pour reordonner, "+" pour ajouter, "x" pour supprimer. Changer de page vide la pile Annuler/Retablir (une commande empilee sur une autre page n'a plus de sens). Mode Apercu : navigation Page precedente/suivante avec indicateur "Page X / N" (choix explicite : page par page, pas de defilement continu), jamais affichee s'il n'y a qu'une seule page. Verifie : suite pytest complete (702 tests, dont 14 nouveaux pour les routes de pages), simulation DOM reelle (jsdom, 25 assertions couvrant tout le cycle de vie cote client -- creation/bascule/renommage/ reordonnancement/suppression de page, portee correcte des elements par page, pile Annuler/Retablir videe au changement de page, pilule de navigation en Apercu), et un test de fumee HTTP reel contre le serveur de dev en marche (creation/ajout d'element/rendu/renommage/suppression d'une page, refus de supprimer la derniere page, page inconnue -> 404). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.5 KiB
document_engine/elements/
CRUD des éléments d'UNE PAGE d'un support de formation
(_document_elements, voir db/supports/create_support.py) — un
support est composé de plusieurs pages (voir document_engine/pages/),
chaque élément appartient à exactement une page via page_id, jamais
partagé entre pages. Un élément est soit un élément de contenu/rangée du
flux (parent_id = groupe de frères, toujours dans la MÊME page), soit
une forme libre superposée en position absolue (toujours parent_id = NULL).
add_document_element(slug: str, kind: str, page_id: int, parent_id: int | None = None) -> int
Insère un nouvel élément dans page_id, en fin de son groupe de frères
(même parent_id, recherché uniquement à l'intérieur de cette page).
Pose les attributs de départ via element_default_attributes(kind)
(voir document_engine/labels/element_kind_labels.py).
- Retour : l'
iddu nouvel élément. - Exceptions : aucune levée explicitement ; une connexion invalide (support inexistant) lève l'erreur SQLite sous-jacente.
list_document_elements(slug: str, page_id: int) -> list[dict[str, Any]]
Renvoie tous les éléments de page_id, à plat, triés par (parent_id, order_index) — les éléments top-niveau (parent_id NULL) groupés en
premier (tri SQLite : NULL avant toute valeur), puis chaque rangée
groupant ses propres enfants. attributes est décodé en dict. Le
canevas n'affiche jamais qu'une seule page à la fois, d'où ce filtrage.
- Retour : liste de dicts (une ligne de table chacun,
attributesdéjà en JSON décodé). - Exceptions : aucune.
get_document_element(slug: str, element_id: int) -> dict[str, Any] | None
Récupère un seul élément par id.
- Retour : le dict de l'élément, ou
Nonesi l'id n'existe pas. - Exceptions : aucune.
update_document_element_attributes(slug: str, element_id: int, attributes: dict[str, Any]) -> None
Remplace intégralement le JSON attributes d'un élément — chaque
formulaire du panneau Propriétés envoie l'état complet de ses champs,
jamais un patch partiel.
- Retour : aucun.
- Exceptions : aucune levée explicitement ; un
element_idinexistant ne modifie silencieusement aucune ligne (UPDATEsans correspondance).
move_document_element(slug: str, element_id: int, new_parent_id: int | None, new_index: int) -> None
Réinsertion réelle appelée par l'algorithme de glisser-déposer du moteur
de layout : dépose l'élément dans un groupe de frères (nouvelle rangée,
un groupe existant, ou le top-niveau) à une position précise, puis
renumérote intégralement le ou les groupes concernés (ancien et nouveau
si le parent change, un seul sinon) pour rester correct même en cas de
réordonnancement dans le même groupe. new_parent_id doit toujours
désigner un élément de la MÊME page que element_id — jamais vérifié
ici (le client ne propose que des cibles de la page actuellement
affichée).
- Retour : aucun.
- Exceptions : aucune levée explicitement ; un
element_idinexistant ne fait rien (retour silencieux après vérification de son existence).
delete_document_element(slug: str, element_id: int) -> None
Supprime un élément. La contrainte FOREIGN KEY ... ON DELETE CASCADE
(voir db/supports/create_support.py) retire automatiquement ses
enfants si l'élément supprimé était une rangée.
- Retour : aucun.
- Exceptions : aucune levée explicitement ; un
element_idinexistant ne modifie silencieusement aucune ligne.