Files
williamandClaude Sonnet 5 c57420c8c9 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>
2026-09-15 16:06:15 +02:00

102 lines
4.7 KiB
Python

import json
from typing import Any
# Réglages "❓ Boîte à quiz" propres au quiz AUTONOME/plein écran (demande
# explicite : "étendre les fonctionnalités de la boîte à quiz pour les RH
# qui veulent juste faire un quiz") — DISTINCT de "🎨 Style"
# (dialogue_box_style.py, partagé avec dialogue_box/score_widget) : ce
# fichier ne gère QUE plein écran/minuteur/modèle, jamais police/couleur.
#
# DEUX catégories de modèles bien distinctes (demande explicite, précisée
# après coup — les 5 premiers modèles construits avant cette précision
# étaient tous de la première catégorie) :
# - "dialog_template" (modèles "boîte de dialogue", visibles SEULEMENT
# quand fullscreen=False) : variantes de forme/couleur/interaction sur
# une structure HTML qui reste globalement la même que "Classique".
# - "page_template" (modèles "page de quiz", visibles SEULEMENT quand
# fullscreen=True) : thème complet et autonome — police, formes,
# couleurs ET structure HTML totalement propres à CHAQUE modèle,
# aucune structure partagée entre eux ni avec les modèles "boîte".
# Les deux réglages sont conservés INDÉPENDAMMENT (jamais écrasés l'un par
# l'autre) : activer/désactiver le plein écran change juste lequel des
# deux s'applique, sans jamais perdre le choix de l'autre.
DEFAULT_QUIZ_BOX_CONFIG: dict[str, Any] = {
"fullscreen": False,
"timer_mode": "aucun",
"timer_seconds": 20,
"dialog_template": "defaut",
"page_template": "classique",
}
# "aucun" : pas de minuteur (réglage par défaut, jamais imposé — demande
# explicite du créateur). "question" : redémarre à chaque question.
# "quiz" : un seul compte à rebours pour l'ensemble du quiz.
TIMER_MODES = ["aucun", "question", "quiz"]
# "defaut" (Classique) = le CSS/HTML déjà existant, AUCUN changement
# visuel pour un quiz non reconfiguré (voir quiz_box_templates.py) —
# "Verre dépoli"/"Néon"/"Minimal"/"Ludique" ont été retirés (demande
# explicite : "supprime tous les modele de boite sauf le base").
# "manga_dialogue" (Manga) : pendant, en petite carte flottante (jamais
# plein écran), du modèle "page de quiz" "Manga" — validé par le créateur
# via une maquette interactive avant intégration (voir
# screens/rendering/quiz_box_templates.py::render_quiz_box_manga_dialogue).
# Clé DIFFÉRENTE du modèle "page" "manga" (registre d'effets JS partagé
# entre les deux catégories, voir static/js/play/quiz-box-templates.js)
# — reste proche de la structure de "Classique" (demande explicite :
# les modèles "boîte" ne changent que forme/couleur/interaction).
QUIZ_BOX_DIALOG_TEMPLATES = ["defaut", "manga_dialogue"]
QUIZ_BOX_DIALOG_TEMPLATE_LABELS = {
"defaut": "Classique",
"manga_dialogue": "Manga",
}
# "classique" (Classique) : modèle de PAGE DE QUIZ de base — sobre/
# professionnel, reprend les couleurs par défaut du modèle "boîte de
# dialogue" de base (demande explicite : "respecte les couleurs du modele
# de boite de base pour le modele de page de base") plutôt que sa propre
# palette, contrairement à "manga" qui est un thème pleinement autonome.
# "manga" : thème/typographie/structure entièrement dédiés, validé par le
# créateur via une maquette interactive avant intégration (voir
# screens/rendering/quiz_box_templates.py::render_quiz_box_page_manga).
QUIZ_BOX_PAGE_TEMPLATES = ["classique", "manga"]
QUIZ_BOX_PAGE_TEMPLATE_LABELS = {
"classique": "Classique",
"manga": "Manga",
}
_MIN_TIMER_SECONDS = 3
_MAX_TIMER_SECONDS = 600
def sanitize_quiz_box_config(raw_config: Any) -> dict[str, Any]:
config = dict(DEFAULT_QUIZ_BOX_CONFIG)
if not isinstance(raw_config, dict):
return config
config["fullscreen"] = bool(raw_config.get("fullscreen"))
if raw_config.get("timer_mode") in TIMER_MODES:
config["timer_mode"] = raw_config["timer_mode"]
raw_timer_seconds = raw_config.get("timer_seconds")
try:
if raw_timer_seconds is None:
raise ValueError
config["timer_seconds"] = max(_MIN_TIMER_SECONDS, min(_MAX_TIMER_SECONDS, int(raw_timer_seconds)))
except (TypeError, ValueError):
pass
if raw_config.get("dialog_template") in QUIZ_BOX_DIALOG_TEMPLATES:
config["dialog_template"] = raw_config["dialog_template"]
if raw_config.get("page_template") in QUIZ_BOX_PAGE_TEMPLATES:
config["page_template"] = raw_config["page_template"]
return config
def resolve_quiz_box_config(obj: dict[str, Any]) -> dict[str, Any]:
raw = (obj.get("attributes") or {}).get("_quiz_box_config")
if not raw:
return dict(DEFAULT_QUIZ_BOX_CONFIG)
try:
parsed = json.loads(raw)
except (TypeError, ValueError):
return dict(DEFAULT_QUIZ_BOX_CONFIG)
return sanitize_quiz_box_config(parsed)