Ajoute un suivi xAPI optionnel (bolt-on) au paquet SCORM exporté
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Greffe l'envoi de statements xAPI vers un LRS configurable par le
créateur du jeu, en parallèle du reporting SCORM existant : réglages
stockés en base (_meta), formulaire dans la modale d'export, injection
dans le paquet exporté. Relié aussi bien à l'action de flow "Modifier
un score/statut" qu'au parcours quête/quiz (qui alimentait déjà le
SCORM classique via un chemin séparé).

L'export ne se lance plus automatiquement à l'ouverture de la modale,
pour laisser le temps d'enregistrer les réglages xAPI avant de générer
le paquet.
This commit is contained in:
william
2026-09-05 10:00:57 +02:00
parent 7ebc9b143f
commit f7a50e5afe
13 changed files with 422 additions and 3 deletions
+24
View File
@@ -8,6 +8,7 @@ plan — validation ADL SCORM Cloud/Moodle hors du champ de la suite
automatisée)."""
import zipfile
import db
from tests.test_auth import anon_client # noqa: F401
@@ -59,6 +60,29 @@ def test_export_scorm_manifest_lists_index_html_as_the_sco(client, game):
assert "adlcp:scormtype=\"sco\"" in manifest
def test_export_scorm_index_html_has_no_xapi_config_when_unconfigured(client, game):
resp = client.post(f"/game/{game}/export-scorm")
import io
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
html = zf.read("index.html").decode("utf-8")
assert "window.FORGE_XAPI_CONFIG = null;" in html
# Toujours chargé, même sans réglage (no-op tant que la config est null).
assert 'src="static/js/play/offline/xapi-client.js"' in html
def test_export_scorm_index_html_embeds_the_xapi_config_when_set(client, game):
db.set_xapi_settings(game, "https://lrs.example.com/xapi/", "monlogin", "monmotdepasse")
resp = client.post(f"/game/{game}/export-scorm")
import io
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
html = zf.read("index.html").decode("utf-8")
assert "window.FORGE_XAPI_CONFIG = {" in html
assert "https://lrs.example.com/xapi/" in html
assert "monlogin" in html
assert "monmotdepasse" in html
assert 'src="static/js/play/offline/xapi-client.js"' in html
def test_export_scorm_route_is_isolated_like_other_game_routes(anon_client):
"""Même garde d'accès que /game/<slug>/publish (core/auth_guard.py) —
aucune vérification supplémentaire écrite pour cette route."""
+66
View File
@@ -0,0 +1,66 @@
"""Réglages xAPI (bolt-on, voir db/games/get_xapi_settings.py,
routes/publish/xapi_settings.py) — le mot de passe ne doit JAMAIS
ressortir en clair (seulement un booléen `password_set`), et un POST
sans mot de passe ne doit pas écraser celui déjà enregistré."""
from tests.test_auth import anon_client # noqa: F401
def test_xapi_settings_get_defaults_to_empty(client, game):
resp = client.get(f"/game/{game}/xapi-settings")
assert resp.status_code == 200
body = resp.get_json()
assert body == {"endpoint": "", "login": "", "password_set": False}
def test_xapi_settings_post_then_get_round_trip(client, game):
resp = client.post(f"/game/{game}/xapi-settings", json={
"endpoint": "https://lrs.example.com/xapi/",
"login": "monlogin",
"password": "monmotdepasse",
})
assert resp.status_code == 200
assert resp.get_json() == {"ok": True}
resp = client.get(f"/game/{game}/xapi-settings")
body = resp.get_json()
assert body["endpoint"] == "https://lrs.example.com/xapi/"
assert body["login"] == "monlogin"
assert body["password_set"] is True
assert "password" not in body
def test_xapi_settings_post_with_blank_password_keeps_the_existing_one(client, game):
client.post(f"/game/{game}/xapi-settings", json={
"endpoint": "https://lrs.example.com/xapi/",
"login": "monlogin",
"password": "monmotdepasse",
})
resp = client.post(f"/game/{game}/xapi-settings", json={
"endpoint": "https://lrs.example.com/xapi/v2/",
"login": "monlogin",
"password": "",
})
assert resp.status_code == 200
import db
settings = db.get_xapi_settings(game)
assert settings["endpoint"] == "https://lrs.example.com/xapi/v2/"
assert settings["password"] == "monmotdepasse"
def test_xapi_settings_route_is_isolated_like_other_game_routes(anon_client):
"""Même garde d'accès que /game/<slug>/publish (core/auth_guard.py) —
aucune vérification supplémentaire écrite pour cette route."""
from tests.test_auth import _register, _confirm_2fa, _complete_onboarding, _cleanup_project
_register(anon_client, "xapisettingsisolation@example.com")
_confirm_2fa(anon_client)
_complete_onboarding(anon_client)
try:
resp = anon_client.get("/game/un-projet-qui-nest-pas-le-sien/xapi-settings")
assert resp.status_code == 403
resp = anon_client.post("/game/un-projet-qui-nest-pas-le-sien/xapi-settings", json={})
assert resp.status_code == 403
finally:
_cleanup_project("xapisettingsisolation@example.com")