Files
Forge-Engine/document_engine/labels/labels.md
T
williamandClaude Sonnet 5 7e504b7865
Build and deploy / test-python (push) Successful in 11m5s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / lint-python (push) Successful in 6m29s
Build and deploy / lint-js (push) Failing after 1m39s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 5m18s
Sanitize les attributs en LECTURE aussi, pas seulement a l'ecriture
Bug reel constate le 21/09/2026 : un element Scenario cree avant la
refonte en arbre de decision (ancien schema plat "situation"/"choices"/
"correct_index") faisait planter silencieusement le panneau Proprietes
cote client des la selection - scenario.nodes etait inexistant sur
l'ancienne forme, aucune erreur visible, juste "il ne se passe rien".
Cause : document_edit.py et document_render.py renvoyaient les
attributs BRUTS de la base au client, jamais revalides - contrairement
a la route d'ecriture qui, elle, sanitize deja avant de persister.

Ajoute document_engine.sanitize_element_attributes(kind, attributes),
point d'entree unique de dispatch kind -> sanitize_X_config, utilise
desormais a la fois en ecriture (document_element_update.py, qui
reutilise ce nouveau dispatch au lieu de son if/elif duplique) ET en
lecture (document_edit.py/document_render.py). Elimine toute la classe
de bug "schema devenu obsolete apres une evolution du modele de
donnees d'un mini-jeu, donnee jamais retouchee depuis" - present et
futur, pas seulement pour Scenario.

Migre les donnees reelles deja affectees (support de test, element 46)
vers le nouveau schema en arbre, en preservant integralement le
contenu deja redige par l'utilisateur (situation + 3 choix/consequences
du scenario "chat sur la route").

Ajoute un test de non-regression qui ecrit delibirement l'ancien schema
en base (en contournant le sanitize de la route d'ecriture, pour
simuler une donnee reellement ancienne jamais nettoyee) puis verifie
que /edit et /render renvoient une structure saine au client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 08:07:06 +02:00

263 lines
14 KiB
Markdown

# document_engine/labels/
Catalogue statique des types d'éléments du support de formation : bibliothèque
groupée par catégorie (panneau gauche de l'éditeur), libellés d'affichage, et
attributs par défaut posés à la création de chaque type.
## `SHAPE_KINDS: tuple[str, ...]`
`("rectangle", "cercle", "triangle", "trait")` — couche de formes libres,
toujours en position absolue (`parent_id = NULL`).
## `CONTENT_KINDS: tuple[str, ...]`
`("titre", "paragraphe", "image", "bouton")` — éléments du flux, peuvent
être top-niveau ou enfants d'une rangée.
## `MINIGAME_KINDS: tuple[str, ...]`
`("quiz", "association", "memory", "mots", "scenario", "zones")` —
`"quiz"`, `"association"` et `"memory"` sont implémentés (voir
`quiz_config.py`/`association_config.py`/`memory_config.py` ci-dessous) ;
les 3 autres gardent un panneau Propriétés minimal (emplacement réservé),
formulaires de contenu dédiés hors périmètre de cette passe.
## `ELEMENT_LIBRARY: dict[str, dict[str, Any]]`
Bibliothèque affichée dans le panneau gauche, groupée par catégorie
(`mise_en_page` / `contenu` / `minigames`), chaque entrée portant un
`label` et sa liste de `kinds`. `"row"` n'y apparaît jamais — créé
implicitement par le moteur de layout, jamais choisi directement dans la
bibliothèque.
## `ELEMENT_KIND_LABELS: dict[str, str]`
Libellé d'affichage pour chaque `kind`, y compris `"row"` (pour
l'affichage/debug hors bibliothèque).
## `element_default_attributes(kind: str) -> dict[str, Any]`
Attributs posés à la création d'un élément de ce type (voir
`document_engine/elements/add_document_element.py`).
- **Retour** : un dict d'attributs par défaut, dépendant du `kind` :
formes (`x/y/width/height/rotation/z_index/fill/stroke/stroke_width/label`),
texte (`content/style` + `bold/italic/underline/align/color`),
image (`src/alt`), bouton (`label/target`), rangée (`gap/align/justify`),
quiz (`DEFAULT_QUIZ_CONFIG`, voir `quiz_config.py`), association
(`DEFAULT_ASSOCIATION_CONFIG`, voir `association_config.py`), memory
(`DEFAULT_MEMORY_CONFIG`, voir `memory_config.py`), autre mini-jeu
(`theme_color`), ou `{}` pour un `kind` inconnu.
- **Exceptions** : aucune.
## `quiz_config.py` — modèle de données du mini-jeu Quiz
Seul mini-jeu réellement implémenté (les autres `MINIGAME_KINDS` restent
un emplacement réservé). Même convention `resolve_X`/`sanitize_X` que
`game_engine/rendering/quiz_box_config.py` côté jeu, mais sans aucun
import croisé (voir `docs/plan/PLAN.md`).
### `MIN_CHOICES`, `MAX_CHOICES`, `MIN_TIMER_SECONDS`, `MAX_TIMER_SECONDS`, `DEFAULT_TIMER_SECONDS: int`
Bornes de validation (`2`/`4` choix, `5`/`300` secondes, `30` par défaut).
### `DEFAULT_QUIZ_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "timer_enabled": False, "timer_seconds": 30, "questions": []}`.
### `sanitize_quiz_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config de quiz arbitraire (JSON venu du client) —
jamais lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_QUIZ_CONFIG`. Chaque question de `raw_config["questions"]` est
validée indépendamment (voir `_sanitize_question`, privée) : texte non
vide, au moins 2 choix non vides (tronqués à 4), `correct_index` remis à
0 s'il est absent/hors bornes/non-entier (un `bool` — qui est un `int` en
Python — est explicitement rejeté), `points` codé en entier ≥ 0. Une
question invalide est silencieusement supprimée de la liste (jamais une
levée qui ferait échouer tout le reste du quiz).
- **Retour** : dict complet (mêmes clés que `DEFAULT_QUIZ_CONFIG`).
- **Exceptions** : aucune.
### `quiz_total_points(config: dict[str, Any]) -> int`
Somme du `points` de toutes les questions d'une config déjà sanitizée —
mirroir de `db/dialogue_lines.py::sum_question_rewards` côté jeu, utile
le jour où un export calculera un score maximum.
- **Retour** : entier ≥ 0.
- **Exceptions** : aucune.
## `association_config.py` — modèle de données du mini-jeu Association
Deuxième mini-jeu implémenté : l'apprenant relie chaque carte de gauche
("terme") à sa correspondance de droite ("définition") par glisser-déposer
(voir `document_engine/rendering/render_document_element.py`::
`_render_association_player`). Même convention que `quiz_config.py`.
### `MIN_PAIRS`, `MAX_PAIRS: int`
Bornes de validation (`2`/`8` paires). `MIN_PAIRS` n'est pas imposé par
`sanitize_association_config` (une seule paire valide reste acceptée,
comme un quiz à une seule question) — c'est une recommandation pour le
panneau Propriétés, pas une contrainte technique du rendu.
### `DEFAULT_ASSOCIATION_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "pairs": []}`.
### `sanitize_association_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config d'association arbitraire (JSON venu du client)
— jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_ASSOCIATION_CONFIG`. Chaque paire de `raw_config["pairs"]` est
validée indépendamment (voir `_sanitize_pair`, privée) : les deux côtés
(`left`/`right`) doivent être non vides une fois `.strip()`-és, sinon la
paire entière est silencieusement supprimée de la liste (jamais une
levée qui ferait échouer tout le reste du mini-jeu). La liste finale est
tronquée à `MAX_PAIRS`.
- **Retour** : dict complet (mêmes clés que `DEFAULT_ASSOCIATION_CONFIG`).
- **Exceptions** : aucune.
## `memory_config.py` — modèle de données du mini-jeu Memory
Troisième mini-jeu implémenté : l'apprenant retourne des cartes pour
constituer des paires identiques (mode `"paire"`) ou simplement révéler
chaque carte une fois (mode `"single"`, sans appariement — voir
`document_engine/rendering/render_document_element.py`::
`_render_memory_player`). Même convention que `quiz_config.py`.
### `MIN_CARDS`, `MAX_CARDS: int`
Bornes de validation (`2`/`8` cartes DÉFINIES par le créateur — en mode
`"paire"`, le plateau affiche le double, chaque carte étant dupliquée).
`MIN_CARDS` n'est pas imposé par `sanitize_memory_config` (même logique
que `MIN_PAIRS` côté Association) — recommandation pour le panneau
Propriétés, pas une contrainte technique du rendu.
### `CARD_MODES: tuple[str, ...]`
`("paire", "single")`.
### `DEFAULT_MEMORY_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "mode": "paire", "cards": []}`.
### `sanitize_memory_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config de memory arbitraire (JSON venu du client) —
jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_MEMORY_CONFIG`. `mode` retombe sur `"paire"` s'il n'est pas dans
`CARD_MODES`. Chaque carte de `raw_config["cards"]` est validée
indépendamment (voir `_sanitize_card`/`_sanitize_card_face`, privées) :
chaque face (`recto`/`verso`) a un `image` et un `text` indépendants et
tous deux optionnels, MAIS le `verso` doit avoir au moins l'un des deux
non vide (rien à révéler/apparier sinon) — le `recto`, lui, peut rester
entièrement vide (dos de carte générique "?" par défaut côté rendu). Une
carte invalide est silencieusement supprimée de la liste. La liste finale
est tronquée à `MAX_CARDS`.
- **Retour** : dict complet (mêmes clés que `DEFAULT_MEMORY_CONFIG`).
- **Exceptions** : aucune.
## `mots_config.py` — modèle de données du mini-jeu Mots mêlés
Quatrième mini-jeu implémenté : l'apprenant retrouve chaque mot caché
dans une grille de lettres (horizontalement, verticalement, ou en
diagonale — voir `document_engine/rendering/render_document_element.py`::
`_render_mots_player`, qui construit la grille elle-même). Même
convention que `quiz_config.py`.
### `MIN_WORDS`, `MAX_WORDS: int`
Bornes de validation (`5`/`10` mots). `MIN_WORDS` n'est pas imposé par
`sanitize_mots_config` (même logique que `MIN_PAIRS`/`MIN_CARDS`
côté Association/Memory) — recommandation pour le panneau Propriétés,
pas une contrainte technique du rendu : un seul mot valide reste
accepté, la grille se construit quand même autour de lui.
### `MIN_WORD_LENGTH`, `MAX_WORD_LENGTH: int`
Bornes de longueur d'un mot individuel une fois nettoyé (`2`/`20`
lettres) — un mot trop court n'a pas de sens à chercher, un mot trop
long compliquerait inutilement le calcul de la taille de la grille
(voir `_mots_grid_size_for_words` dans `render_document_element.py`).
### `DEFAULT_MOTS_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "words": []}`.
### `sanitize_mots_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config de mots mêlés arbitraire (JSON venu du
client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_MOTS_CONFIG`. Chaque mot de `raw_config["words"]` est nettoyé
indépendamment (voir `_sanitize_word`, privée) : mis en MAJUSCULES,
les accents sont retirés (décomposition NFKD + filtrage ASCII, pour que
deux mots qui se croisent sur une même case de la grille puissent
partager exactement la même lettre), seules les lettres sont
conservées. Un mot qui ne contient plus rien d'exploitable (ou moins de
`MIN_WORD_LENGTH` lettres) une fois nettoyé est silencieusement
supprimé de la liste, de même qu'un doublon exact d'un mot déjà retenu
(un mot répété deux fois n'aurait rien de plus à faire trouver). La
liste finale est tronquée à `MAX_WORDS`.
- **Retour** : dict complet (mêmes clés que `DEFAULT_MOTS_CONFIG`).
- **Exceptions** : aucune.
## `scenario_config.py` — modèle de données du mini-jeu Scénario
Cinquième mini-jeu implémenté : l'apprenant lit une situation initiale,
choisit une option, puis découvre la conséquence de SON choix — cette
conséquence peut elle-même mener à de nouveaux choix, et ainsi de suite
(arbre de décision, voir `document_engine/rendering/
render_document_element.py`::`_render_scenario_player`). Aucune notion
de bonne/mauvaise réponse (retour utilisateur du 20/09/2026 : "il n'y a
pas de notion vrai/faux, l'utilisateur observe les conséquences") —
l'apprenant explore simplement les branches jusqu'à une fin. Le
créateur peut définir plusieurs scénarios (plusieurs arbres
indépendants), joués les uns après les autres dans l'ORDRE d'écriture
(jamais mélangés, contrairement à Association/Memory/Mots mêlés). Même
convention resolve_X/sanitize_X que `quiz_config.py`.
**Structure d'un scénario** : `{"title": str, "nodes": [...]}`.
`nodes[0]` est TOUJOURS la situation initiale (racine de l'arbre, jamais
supprimable depuis le panneau Propriétés — voir
`forgeDocRenderScenarioNodeHtml`, static/document/js/document-editor.js).
Chaque nœud est `{"id": str, "text": str, "choices": [...]}` — `id` est
une référence STABLE (générée côté client, jamais recalculée côté
serveur) vers laquelle un choix d'un AUTRE nœud peut pointer via son
`target_id` ; un nœud SANS choix est une fin de branche (état normal,
pas une erreur — contrairement à l'ancien modèle qui exigeait 2 à 4
choix par scénario). Un choix est `{"text": str, "target_id": str |
None}` — `target_id` à `None` signifie "fin de branche à ce choix".
### `MAX_SCENARIO_CHOICES: int`
Borne technique du nombre de choix par nœud (`4`) — évite une UI
disproportionnée, aucun lien avec une notion de bonne/mauvaise réponse.
Nommée explicitement "SCENARIO" (pas juste `MAX_CHOICES`) pour éviter
toute collision avec la constante de même nom de `quiz_config.py`, qui
borne un concept différent (le nombre de réponses d'une question de
quiz) — `document_engine/__init__.py` ré-exporte l'intégralité de l'API
publique du paquet à plat, un nom générique entrerait en collision.
Contrairement à l'ancien modèle, il n'existe PAS de minimum : un nœud
peut avoir 0 choix (fin de branche), c'est un état valide, pas une
erreur à filtrer.
### `DEFAULT_SCENARIO_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "scenarios": []}`.
### `sanitize_scenario_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config de scénario arbitraire (JSON venu du client) —
jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_SCENARIO_CONFIG`. Chaque scénario de `raw_config["scenarios"]`
est validé indépendamment (voir `_sanitize_scenario`/
`_sanitize_scenario_node`/`_sanitize_scenario_choice`, privées) : le
`title` doit être non vide, chaque nœud doit avoir un `id` (chaîne non
vide) et un `text` non vide une fois `.strip()`-é (sinon le nœud entier
est supprimé), et ses choix sont tronqués à `MAX_SCENARIO_CHOICES`. Le
scénario entier est supprimé s'il ne reste plus aucun nœud valide après
nettoyage (il faut au moins la situation initiale). Une fois l'ensemble
des ids valides connu, tout `target_id` qui ne pointe plus vers un nœud
existant (nœud invalide/supprimé) est silencieusement remis à `None`
("fin de branche") plutôt que de faire échouer tout le scénario — même
philosophie que le reste de ce module : ne jamais faire échouer une
structure entière pour une seule référence cassée.
- **Retour** : dict complet (mêmes clés que `DEFAULT_SCENARIO_CONFIG`).
- **Exceptions** : aucune.
## `sanitize_element_attributes.py` — point d'entrée unique de revalidation par kind
Dispatch `kind -> sanitize_X_config` (`_SANITIZERS`, privée) pour les 5
mini-jeux à structure garantie (Quiz/Association/Memory/Mots
mêlés/Scénario) — les autres kinds n'ont pas de sanitizer et sont
renvoyés tels quels. Utilisé à la fois à l'ÉCRITURE
(`routes/document/document_element_update.py`) ET à la LECTURE
(`routes/document/document_edit.py`, `routes/document/document_render.py`)
: sans ce second usage, des attributs stockés dans un schéma devenu
obsolète (ex. Scénario, passé d'une liste plate à un arbre de décision)
atteindraient le panneau Propriétés côté client TELS QUELS, qui suppose
la forme ACTUELLE — bug réel constaté le 21/09/2026, un élément Scénario
créé avant la refonte en arbre faisait planter silencieusement le
panneau Propriétés (`scenario.nodes` inexistant sur l'ancienne forme).
### `sanitize_element_attributes(kind: str, attributes: Any) -> Any`
- **Retour** : `attributes` revalidé si `kind` a un sanitizer, sinon
`attributes` tel quel.
- **Exceptions** : aucune (délègue à des sanitizers qui ne lèvent jamais).