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>
102 lines
4.7 KiB
Python
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)
|