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,4 +1,5 @@
|
||||
import html as html_lib
|
||||
from typing import Any, Callable
|
||||
|
||||
# ---------- "❓ Boîte à quiz" — DEUX catégories de modèles bien distinctes
|
||||
# (voir screens/rendering/quiz_box_config.py) :
|
||||
@@ -43,7 +44,7 @@ import html as html_lib
|
||||
# render_quiz_box_defaut.
|
||||
|
||||
|
||||
def _quiz_data_attrs(obj, config, active_template):
|
||||
def _quiz_data_attrs(obj: dict[str, Any], config: dict[str, Any], active_template: str) -> str:
|
||||
return (
|
||||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-quiz-template="{active_template}" '
|
||||
@@ -52,7 +53,7 @@ def _quiz_data_attrs(obj, config, active_template):
|
||||
)
|
||||
|
||||
|
||||
def _quiz_box_style(obj, style):
|
||||
def _quiz_box_style(obj: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
return (
|
||||
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']}; "
|
||||
@@ -61,16 +62,16 @@ def _quiz_box_style(obj, style):
|
||||
)
|
||||
|
||||
|
||||
def _quiz_topbar_html():
|
||||
def _quiz_topbar_html() -> str:
|
||||
return (
|
||||
'<div class="quizBoxTopBar" data-quiz-role="topbar">'
|
||||
'<span class="quizBoxScoreDisplay" data-quiz-role="score">Score : 0</span>'
|
||||
'<span class="quizBoxTimerDisplay" data-quiz-role="timer"></span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def _quiz_header_row_html(header_text, header_bg=None):
|
||||
def _quiz_header_row_html(header_text: str, header_bg: str | None = None) -> str:
|
||||
# Le score/minuteur (topbar) est un FRÈRE du header, jamais son enfant
|
||||
# (voir le commentaire d'en-tête) — les deux partagent ici le MÊME
|
||||
# bandeau (fond commun posé sur ".quizBoxHeaderRow", jamais sur le
|
||||
@@ -79,29 +80,28 @@ def _quiz_header_row_html(header_text, header_bg=None):
|
||||
# part au-dessus (demande explicite). `header_bg=None` : aucun fond
|
||||
# posé, le bandeau reste transparent (repli disponible pour un futur
|
||||
# modèle qui n'en aurait pas besoin).
|
||||
bg_style = f' style="background:{html_lib.escape(header_bg)}"' if header_bg else ''
|
||||
bg_style = f' style="background:{html_lib.escape(header_bg)}"' if header_bg else ""
|
||||
return (
|
||||
f'<div class="quizBoxHeaderRow"{bg_style}>'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header">{header_text}</div>'
|
||||
+ _quiz_topbar_html() +
|
||||
'</div>'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header">{header_text}</div>' + _quiz_topbar_html() + "</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_defaut(obj, config, style):
|
||||
def render_quiz_box_defaut(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
return (
|
||||
f'<div class="quizBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
+ _quiz_header_row_html("Quête : Titre de la quête", style["header_bg"]) +
|
||||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<div class="quizBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" '
|
||||
f'{_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
+ _quiz_header_row_html("Quête : Titre de la quête", style["header_bg"])
|
||||
+ f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
f'</div></div></div>'
|
||||
f"</div></div></div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
def render_quiz_box_manga_dialogue(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Manga" (boîte de dialogue) — pendant, en petite carte flottante
|
||||
# JAMAIS plein écran, du modèle "page de quiz" "Manga" (voir
|
||||
# render_quiz_box_page_manga), validé par le créateur via une
|
||||
@@ -123,10 +123,12 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# doublerait dans l'aperçu éditeur (jamais réécrit par le JS avant le
|
||||
# premier vrai jeu).
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--mangaDialogue" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--mangaDialogue" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="quizMangaDialogueFrame">'
|
||||
+ _quiz_header_row_html("Titre de la quête", style["header_bg"]) +
|
||||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
+ _quiz_header_row_html("Titre de la quête", style["header_bg"])
|
||||
+ f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
@@ -138,8 +140,8 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# demande explicite), donc ce repère est maintenant fidèle au
|
||||
# comportement réel, pas juste à l'aperçu éditeur.
|
||||
'<button type="button" class="quizBoxContinueBtn" disabled>Continuer →</button>'
|
||||
'</div></div>'
|
||||
'</div>'
|
||||
"</div></div>"
|
||||
"</div>"
|
||||
# Bandeau de pied de page TOUJOURS visible (comme la maquette
|
||||
# validée) — purement décoratif (aria-hidden, jamais un
|
||||
# data-quiz-role), le moteur JS partagé ne le touche JAMAIS : le
|
||||
@@ -152,11 +154,11 @@ def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# l'intérieur au lieu d'être positionnés par-dessus TOUT le
|
||||
# widget (.quizBoxWidget--mangaDialogue est position:relative).
|
||||
'<div class="quizMangaDialogueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_manga(obj, config, style):
|
||||
def render_quiz_box_page_manga(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Manga" — premier modèle "PAGE DE QUIZ" (voir screens/rendering/
|
||||
# quiz_box_config.py::QUIZ_BOX_PAGE_TEMPLATES) : thème complet et
|
||||
# autonome, validé par le créateur via une maquette interactive avant
|
||||
@@ -185,18 +187,20 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
# dédié pour le maintenir à jour question après question.
|
||||
timer_placeholder = "Aucun" if config["timer_mode"] == "aucun" else ""
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--manga" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--manga" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="mangaTopbar">'
|
||||
'<div class="mangaPlate mangaPlate--timer">'
|
||||
'<span class="mangaPlateLabel">Temps</span>'
|
||||
f'<span class="mangaPlateValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="mangaTitlemark"><span>Manga Quiz</span></div>'
|
||||
'<div class="mangaPlate mangaPlate--score">'
|
||||
'<span class="mangaPlateLabel">Score</span>'
|
||||
'<span class="mangaPlateValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
# Barre d'étapes (voir static/js/play/quiz-box-templates.js::
|
||||
# forgeQuizMangaOnShow) — remplie EN JEU (nombre de questions total
|
||||
# connu seulement à l'exécution, jamais à la pose de l'objet) ;
|
||||
@@ -206,11 +210,11 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
'<div class="mangaPanelQuestion">'
|
||||
'<span class="mangaKicker" data-quiz-role="header">Titre de la quête</span>'
|
||||
'<p class="mangaQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="mangaChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
# Écran de résultat de fin de quiz — voir forgeQuizMangaOnQuizComplete
|
||||
# (static/js/play/quiz-box-templates.js), même mécanique que
|
||||
# "classique" (render_quiz_box_page_classique) mais dans le thème
|
||||
@@ -221,11 +225,11 @@ def render_quiz_box_page_manga(obj, config, style):
|
||||
# bandeau topbar/score + la barre d'étapes visibles. data-manga-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="mangaResultZone" data-manga-role="result"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_classique(obj, config, style):
|
||||
def render_quiz_box_page_classique(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
# "Classique" — modèle de PAGE DE QUIZ DE BASE (sobre/professionnel),
|
||||
# validé par le créateur via une maquette interactive (formation
|
||||
# sécurité incendie comme exemple). Reprend VOLONTAIREMENT les
|
||||
@@ -249,23 +253,27 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# dans [data-quiz-role="choices"]) se pose en bandeau bas façon
|
||||
# pied de page, dans la couleur configurée via "🎨 Style"
|
||||
# (footer_bg), jamais une couleur figée en dur.
|
||||
f'<div class="quizBoxWidget quizBoxWidget--classique" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)} --classique-footer-bg:{html_lib.escape(style["footer_bg"])}; --classique-body-bg:{html_lib.escape(style["body_bg"])};">'
|
||||
f'<div class="quizBoxWidget quizBoxWidget--classique" role="dialog" aria-live="polite" '
|
||||
f'aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} '
|
||||
f'style="{_quiz_box_style(obj, style)} '
|
||||
f"--classique-footer-bg:{html_lib.escape(style['footer_bg'])}; "
|
||||
f'--classique-body-bg:{html_lib.escape(style["body_bg"])};">'
|
||||
f'<div class="classiqueHeaderBar" style="background:{html_lib.escape(style["header_bg"])}">'
|
||||
'<div class="classiqueHeaderTitleWrap">'
|
||||
'<span class="classiqueHeaderEyebrow">❓ Quiz</span>'
|
||||
f'<span class="classiqueHeaderTitle">{title}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="classiqueHeaderChips">'
|
||||
'<div class="classiqueChip classiqueChip--timer">'
|
||||
'<span class="classiqueChipLabel">Temps</span>'
|
||||
f'<span class="classiqueChipValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
'<div class="classiqueChip classiqueChip--score">'
|
||||
'<span class="classiqueChipLabel">Score</span>'
|
||||
'<span class="classiqueChipValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
f'<div class="classiqueBody" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
'<div class="classiqueContent">'
|
||||
# Barre d'étapes — même principe que "manga" (voir
|
||||
@@ -282,7 +290,7 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
'<div class="classiqueChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
# Encart d'explication après réponse — RÉSERVÉ pour une
|
||||
# fonctionnalité FUTURE, PAS ENCORE implémentée (demande
|
||||
# explicite : "tu peut le mettre dans le modele mais ne
|
||||
@@ -299,8 +307,8 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# CSS tout le reste : progression/question/choix). data-classique-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="classiqueResultZone" data-classique-role="result"></div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
"</div>"
|
||||
# Bandeau de pied de page TOUJOURS visible (footer_bg, même
|
||||
# esprit que header/body ci-dessus) — purement décoratif
|
||||
# (aria-hidden, jamais un data-quiz-role), le moteur JS partagé
|
||||
@@ -309,22 +317,24 @@ def render_quiz_box_page_classique(obj, config, style):
|
||||
# (voir static/style.css), pas DANS ce bandeau — demande
|
||||
# explicite : "il manque le footer foncé avec le bouton".
|
||||
'<div class="classiqueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
QUIZ_BOX_DIALOG_RENDERERS = {
|
||||
_QuizRenderer = Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], str]
|
||||
|
||||
QUIZ_BOX_DIALOG_RENDERERS: dict[str, _QuizRenderer] = {
|
||||
"defaut": render_quiz_box_defaut,
|
||||
"manga_dialogue": render_quiz_box_manga_dialogue,
|
||||
}
|
||||
|
||||
QUIZ_BOX_PAGE_RENDERERS = {
|
||||
QUIZ_BOX_PAGE_RENDERERS: dict[str, _QuizRenderer] = {
|
||||
"classique": render_quiz_box_page_classique,
|
||||
"manga": render_quiz_box_page_manga,
|
||||
}
|
||||
|
||||
|
||||
def render_quiz_box(obj, config, style):
|
||||
def render_quiz_box(obj: dict[str, Any], config: dict[str, Any], style: dict[str, Any]) -> str:
|
||||
"""Point d'entrée UNIQUE (voir screens/scenes/render_scene_object.py)
|
||||
— bascule entre les deux catégories de modèles selon `fullscreen`
|
||||
(demande explicite : ces modèles ne se voient JAMAIS dans l'autre
|
||||
|
||||
Reference in New Issue
Block a user