Enrichit le reporting SCORM/xAPI et met en conformité RGAA le player
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:
@@ -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())
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user