Enrichit le reporting SCORM/xAPI et met en conformité RGAA le player
Build and deploy / test-python (push) Successful in 5m56s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

SCORM/xAPI :
- Ajoute l'export SCORM 2004 (3rd/4th edition) au choix, en plus du 1.2
  par défaut : sépare completion_status/success_status (un échec reste
  "completed" au lieu de retomber à tort en "incomplete" comme force la
  1.2), remonte aussi cmi.interactions.n.* par question répondue.
- Fournit cmi.core.score.min/max (1.2 et 2004), calculé depuis les
  récompenses de quiz du jeu, pour que le LMS affiche un vrai pourcentage
  au lieu du score brut à tort étiqueté "%".
- Libellés de verbes xAPI en français en plus de l'anglais.

Accessibilité (RGAA/WCAG 2.1 AA) sur le player :
- Navigation clavier des objets de scène "au clic"/"au survol"
  (tabindex, role, Entrée/Espace, focus/blur).
- alt sur les images (nom auteur ou décoratif), aria-hidden sur les
  icônes seules, role="dialog"/aria-live sur les boîtes de dialogue/quiz.
- Landmark <main> + titre de page, respect de prefers-reduced-motion.
- Le quiz n'avance plus automatiquement après un délai fixe : bouton
  "Continuer →" explicite (RGAA 2.2.1).
- Avertissement de contraste dans l'éditeur de style de dialogue.
- Déclaration d'accessibilité téléchargeable depuis la modale d'export.
This commit is contained in:
william
2026-09-06 00:17:51 +02:00
parent f7a50e5afe
commit c504ace167
36 changed files with 1581 additions and 79 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Déclaration d'accessibilité exportable (obligation légale RGAA pour
tout service numérique concerné, voir roadmap phase 1.2) — PAS un audit
certifié : un résumé honnête, généré à partir de ce que le moteur de jeu
implémente RÉELLEMENT aujourd'hui (voir static/js/play/triggers.js,
dialogue-box-controller.js, screens/scenes/render_scene_object.py), à
compléter/faire valider par le créateur du jeu avant publication."""
from datetime import date
_DECLARATION_TEMPLATE = """Déclaration d'accessibilité — {game_name}
Générée le {today} par Forge Engine
Cette déclaration résume l'état d'accessibilité du jeu de formation
"{game_name}" au regard du RGAA (Référentiel Général d'Amélioration de
l'Accessibilité, version française de WCAG 2.1 niveau AA). Elle décrit ce
que le moteur Forge Engine implémente pour TOUT jeu exporté — elle ne
remplace pas un audit RGAA complet réalisé par un expert accessibilité, et
ne couvre pas le contenu propre à ce jeu (texte, images, choix de
couleurs) qui reste sous la responsabilité de son créateur.
CRITÈRES PRIS EN CHARGE PAR LE MOTEUR
--------------------------------------
- Navigation clavier : tout objet de scène réagissant "Au clic" ou "Au
survol" est focusable (Tab) et activable au clavier (Entrée/Espace),
en plus de la souris. Les boutons de dialogue/quiz sont de vrais
<button> HTML.
- Lecteurs d'écran : les boîtes de dialogue et de quiz sont annoncées
(role="dialog", contenu dynamique signalé par aria-live). Les images de
personnages/décors reçoivent le nom donné par le créateur du jeu comme
alternative textuelle, ou sont marquées décoratives (alt="") si aucun
nom n'a été renseigné. Les icônes seules sont marquées décoratives.
- Structure de page : landmark <main> et titre de page (masqué
visuellement, lu par les lecteurs d'écran).
- Délai réglable : la révélation de la bonne réponse à une question de
quiz n'avance plus automatiquement après un délai fixe — l'apprenant
clique sur "Continuer →" quand il est prêt.
- Confort visuel : les animations en boucle infinie de l'interface
respectent le réglage système "Réduire les animations"
(prefers-reduced-motion).
- Contraste des couleurs : un avertissement est affiché au créateur du
jeu dans l'éditeur de style si le contraste texte/fond de la boîte de
dialogue/quiz est insuffisant (< 4.5:1) — un choix de couleurs reste
possible malgré l'avertissement, la vérification finale revient au
créateur du jeu.
POINTS RESTANT À LA CHARGE DU CRÉATEUR DE CE JEU
--------------------------------------------------
- Choisir des couleurs de texte/fond suffisamment contrastées (le
moteur avertit, mais n'empêche pas un choix insuffisant).
- Fournir des noms clairs aux personnages/décors qui portent une
information (le moteur ne peut pas deviner un contenu manquant).
- Ajouter des sous-titres/transcriptions à tout contenu audio ajouté au
jeu (hors du champ du moteur lui-même).
- Vérifier que le texte des scènes reste compréhensible sans dépendre
uniquement de la couleur ou de la position.
Cette déclaration a été générée automatiquement et n'engage que la
description de ce que le moteur Forge Engine implémente à la date
ci-dessus — elle ne constitue pas une certification de conformité RGAA
complète du jeu "{game_name}".
"""
def build_accessibility_declaration(game_name):
"""Texte brut (jamais de mise en forme HTML/Markdown à interpréter :
prévu pour être lu/imprimé/collé tel quel dans un document interne,
voir routes/publish/accessibility_declaration.py)."""
return _DECLARATION_TEMPLATE.format(game_name=game_name, today=date.today().isoformat())
+35 -2
View File
@@ -258,6 +258,37 @@ def build_scorm_zip(slug):
"activity_id": f"urn:forge-engine:game:{slug}",
"activity_name": game["name"],
}
# Seuil de réussite (voir db/games/get_success_threshold.py) : % de
# bonnes réponses au quiz requis pour que la fin de partie soit
# reportée "reussi" plutôt que "echoue" (SCORM lesson_status ET
# statement xAPI de fin, voir static/js/play/dialogue-box-controller.js
# ::forgeSyncAllQuestsCompletionToScorm) — s'applique même sans xAPI
# configuré, c'est aussi ce qui alimente le SCORM classique.
success_threshold_percent = db.get_success_threshold(slug)
# Version SCORM (voir db/games/get_scorm_version.py) : '1.2' (défaut,
# compatibilité la plus large) ou '2004' — seule la 2004 sépare
# completion_status/success_status (static/js/play/offline/
# scorm2004-api.js), corrige le "echoue" à tort reporté "incomplete"
# en SCORM 1.2 (un seul champ pour coder complétion ET réussite).
scorm_version = db.get_scorm_version(slug)
scorm_api_wrapper_url = (
"static/js/play/offline/scorm2004-api.js" if scorm_version == "2004"
else "static/js/play/offline/scorm-api.js"
)
# Score max possible (voir db/quests/reward_budget.py) : sans lui, un
# LMS SCORM 1.2 n'a que cmi.core.score.raw (un total de points brut,
# sans barème) et l'affiche à tort avec un "%" — demande utilisateur
# "5% pour 1.2 mais 100% pour 2004 avec 1 seule question, c'est un
# défaut de la 1.2 qu'on ne peut pas régler ?". En fournissant aussi
# min/max (voir static/js/play/offline/scorm-api.js), le LMS peut
# calculer raw/max lui-même, un vrai pourcentage au lieu du score brut
# mal étiqueté.
max_quiz_score = sum(
db.quest_dialogues_total_reward(quest["dialogues"]) for quest in db.list_quests(slug)
)
# gameData.icon_data_uris (voir forgeRenderIcone,
# static/js/play/offline/render-special-widgets.js) : ajouté au
# payload AVANT le rendu pour qu'il fasse partie du même payload_json
@@ -272,13 +303,15 @@ def build_scorm_zip(slug):
html = render_template(
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
offline_mode=True, asset_url=lambda filename: "static/" + filename,
scorm_api_wrapper_url="static/js/play/offline/scorm-api.js",
scorm_api_wrapper_url=scorm_api_wrapper_url,
# "</" -> "<\\/" : endpoint/login sont saisis librement par le
# créateur du jeu (voir routes/publish/xapi_settings.py) — sans
# cet échappement, une valeur contenant "</script>" refermerait
# prématurément la balise <script> de templates/play.html qui
# embarque ce JSON (json.dumps n'échappe jamais "<"/">"/"&").
xapi_config_json=json.dumps(xapi_config).replace("</", "<\\/"),
success_threshold_percent=success_threshold_percent,
max_quiz_score=max_quiz_score,
)
html = _relativize_absolute_urls(html, slug)
html = _inline_icon_urls(html, payload["icon_data_uris"])
@@ -286,7 +319,7 @@ def build_scorm_zip(slug):
f.write(html)
package_files = _list_package_files(staging_dir)
manifest_xml = build_manifest_xml(slug, game["name"], package_files)
manifest_xml = build_manifest_xml(slug, game["name"], package_files, version=scorm_version)
with open(os.path.join(staging_dir, "imsmanifest.xml"), "w", encoding="utf-8") as f:
f.write(manifest_xml)
+50 -8
View File
@@ -1,11 +1,15 @@
"""Génère imsmanifest.xml (SCORM 1.2) pour le paquet Web/SCORM (voir
build_scorm_package.py). SCORM 1.2 plutôt que 2004 : support le plus
large chez les clients visés (Moodle, la plupart des LMS d'entreprise
acceptent les deux, l'inverse est faux)."""
"""Génère imsmanifest.xml pour le paquet Web/SCORM (voir
build_scorm_package.py) — SCORM 1.2 par défaut (support le plus large
chez les clients visés, Moodle et la plupart des LMS d'entreprise
acceptent les deux, l'inverse est faux) ou 2004 3rd/4th edition au choix
du créateur du jeu (voir db/games/get_scorm_version.py) : seule la 2004
sépare cmi.completion_status/cmi.success_status, utile quand un
"échoué" doit rester "completed" (l'apprenant est allé au bout) plutôt
que retomber en "incomplete" comme l'impose le champ unique de la 1.2."""
import os
from xml.sax.saxutils import escape
_MANIFEST_TEMPLATE = """<?xml version="1.0" standalone="no" ?>
_MANIFEST_TEMPLATE_1_2 = """<?xml version="1.0" standalone="no" ?>
<manifest identifier="FORGE_{identifier}" version="1"
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2"
@@ -33,16 +37,54 @@ _MANIFEST_TEMPLATE = """<?xml version="1.0" standalone="no" ?>
</manifest>
"""
_MANIFEST_TEMPLATE_2004 = """<?xml version="1.0" standalone="no" ?>
<manifest identifier="FORGE_{identifier}" version="1"
xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_v1p3"
xmlns:adlseq="http://www.adlnet.org/xsd/adlseq_v1p3"
xmlns:adlnav="http://www.adlnet.org/xsd/adlnav_v1p3"
xmlns:imsss="http://www.imsglobal.org/xsd/imsss"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imscp_v1p1 imscp_v1p1.xsd
http://www.adlnet.org/xsd/adlcp_v1p3 adlcp_v1p3.xsd
http://www.adlnet.org/xsd/adlseq_v1p3 adlseq_v1p3.xsd
http://www.adlnet.org/xsd/adlnav_v1p3 adlnav_v1p3.xsd
http://www.imsglobal.org/xsd/imsss imsss_v1p0.xsd">
<metadata>
<schema>ADL SCORM</schema>
<schemaversion>2004 4th Edition</schemaversion>
</metadata>
<organizations default="FORGE_ORG_{identifier}">
<organization identifier="FORGE_ORG_{identifier}">
<title>{title}</title>
<item identifier="FORGE_ITEM_{identifier}" identifierref="FORGE_RES_{identifier}">
<title>{title}</title>
</item>
</organization>
</organizations>
<resources>
<resource identifier="FORGE_RES_{identifier}" type="webcontent" adlcp:scormType="sco" href="index.html">
<file href="index.html"/>
{file_entries}
</resource>
</resources>
</manifest>
"""
def build_manifest_xml(slug, game_name, package_files):
_MANIFEST_TEMPLATES = {"1.2": _MANIFEST_TEMPLATE_1_2, "2004": _MANIFEST_TEMPLATE_2004}
def build_manifest_xml(slug, game_name, package_files, version="1.2"):
"""`package_files` : chemins relatifs (depuis la racine du paquet, tel
qu'écrits dans le zip) de TOUS les fichiers autres que index.html —
un LMS strict (SCORM Cloud) rejette un manifeste qui omet un fichier
réellement présent dans le paquet."""
réellement présent dans le paquet. `version` : '1.2' (défaut) ou
'2004' — voir db/games/get_scorm_version.py."""
file_entries = "\n".join(
' <file href="{}"/>'.format(escape(rel.replace(os.sep, "/")))
for rel in package_files
)
return _MANIFEST_TEMPLATE.format(
template = _MANIFEST_TEMPLATES.get(version, _MANIFEST_TEMPLATE_1_2)
return template.format(
identifier=escape(slug), title=escape(game_name), file_entries=file_entries,
)