Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -1,12 +1,13 @@
|
||||
import html as html_lib
|
||||
from typing import Any
|
||||
|
||||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||||
from ..rendering.quiz_box_config import resolve_quiz_box_config
|
||||
from ..rendering.quiz_box_templates import render_quiz_box
|
||||
|
||||
|
||||
def render_scene_object(obj):
|
||||
def render_scene_object(obj: dict[str, Any]) -> str:
|
||||
"""HTML d'un objet de scène — positionnement en PIXELS ABSOLUS (pas de
|
||||
% fluide, voir ensure_scene_schema.py), mirror volontairement plus
|
||||
simple que render_element_html.py (pas d'arbre de nesting, pas de
|
||||
@@ -52,12 +53,12 @@ def render_scene_object(obj):
|
||||
# collision "dialogue" ait un dialogue à afficher. Toujours VISIBLE
|
||||
# dans l'éditeur (avec un texte d'exemple) pour pouvoir le
|
||||
# positionner/styliser.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; "
|
||||
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};"
|
||||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])};"
|
||||
)
|
||||
return (
|
||||
# role="dialog"/aria-live (RGAA/WCAG 4.1.3) : le texte de
|
||||
@@ -65,12 +66,17 @@ def render_scene_object(obj):
|
||||
# static/js/play/dialogue-box-controller.js) sans jamais
|
||||
# recharger la page — sans annonce explicite, un lecteur
|
||||
# d'écran ne détecte aucun changement.
|
||||
f'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" style="background:{html_lib.escape(style["header_bg"])}">Nom du personnage</div>'
|
||||
f'<div class="dialogueBoxBody" data-dialogue-role="body" style="background:{html_lib.escape(style["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||||
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" style="background:{html_lib.escape(style["footer_bg"])}">'
|
||||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||||
f'</div></div>'
|
||||
f'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" '
|
||||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" '
|
||||
f'style="background:{html_lib.escape(box_theme["header_bg"])}">Nom du personnage</div>'
|
||||
f'<div class="dialogueBoxBody" data-dialogue-role="body" '
|
||||
f'style="background:{html_lib.escape(box_theme["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||||
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" '
|
||||
f'style="background:{html_lib.escape(box_theme["footer_bg"])}">'
|
||||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" '
|
||||
f'onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||||
f"</div></div>"
|
||||
)
|
||||
if obj["kind"] == "quiz_box":
|
||||
# Widget "❓ Boîte à quiz" (voir "🖥️ Interface") — réutilise le
|
||||
@@ -85,9 +91,9 @@ def render_scene_object(obj):
|
||||
# respectent le même contrat de data-quiz-role, lu EN JEU par
|
||||
# static/js/play/dialogue-box-controller.js. "defaut" (Classique)
|
||||
# reste pixel-identique au rendu d'avant cette extension.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
config = resolve_quiz_box_config(obj)
|
||||
return render_quiz_box(obj, config, style)
|
||||
return render_quiz_box(obj, config, box_theme)
|
||||
if obj["kind"] == "score_widget":
|
||||
# Widget "🏆 Score" (voir "🖥️ Interface") — toujours VISIBLE une
|
||||
# fois posé (pas de collision/déclencheur, contrairement aux deux
|
||||
@@ -95,18 +101,18 @@ def render_scene_object(obj):
|
||||
# cours (voir static/js/play/dialogue-box-controller.js::
|
||||
# forgeUpdateAllScoreWidgets). Même style que les deux autres
|
||||
# widgets, header_bg sert de fond unique (pas de zone body/footer).
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_theme = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; "
|
||||
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])}; "
|
||||
f"background:{html_lib.escape(style['header_bg'])};"
|
||||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])}; "
|
||||
f"background:{html_lib.escape(box_theme['header_bg'])};"
|
||||
)
|
||||
return (
|
||||
f'<div class="scoreWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'🏆 <span data-score-role="value">0</span>'
|
||||
f'</div>'
|
||||
f"</div>"
|
||||
)
|
||||
if obj["kind"] == "personnage":
|
||||
src = resolve_personnage_idle_frame(obj)
|
||||
@@ -117,8 +123,10 @@ def render_scene_object(obj):
|
||||
# un dialogue de quête réutilise le même nom). alt="" (décoratif)
|
||||
# sinon, jamais le nom de fichier brut par défaut du navigateur.
|
||||
alt_attr = f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||||
return (f'<img class="sceneObjectSprite" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}{alt_attr}>')
|
||||
return (
|
||||
f'<img class="sceneObjectSprite" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}{alt_attr}>'
|
||||
)
|
||||
src = (obj.get("attributes") or {}).get("src", "")
|
||||
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
|
||||
# "fond" (voir add_scene_object.py) : même image fixe que "decor",
|
||||
@@ -132,5 +140,7 @@ def render_scene_object(obj):
|
||||
# information à transmettre) — un "decor" nommé par l'auteur utilise
|
||||
# ce nom, sinon alt="" plutôt qu'aucun attribut du tout.
|
||||
alt_attr = ' alt=""' if obj["kind"] == "fond" else f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||||
return (f'<img class="{css_class}" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" style="{style}"{src_attr}{alt_attr}>')
|
||||
return (
|
||||
f'<img class="{css_class}" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-anim-target="{obj["id"]}" style="{style}"{src_attr}{alt_attr}>'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user