Files
Forge-Engine/publish/scorm_manifest.py
T
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

99 lines
4.1 KiB
Python

"""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 collections.abc import Iterable
from xml.sax.saxutils import escape # nosec B406 - genere de la sortie XML, ne parse jamais d'XML externe
_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"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd
http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd">
<metadata>
<schema>ADL SCORM</schema>
<schemaversion>1.2</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>
"""
_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>
"""
_MANIFEST_TEMPLATES = {"1.2": _MANIFEST_TEMPLATE_1_2, "2004": _MANIFEST_TEMPLATE_2004}
def build_manifest_xml(
slug: str,
game_name: str,
package_files: Iterable[str],
version: str = "1.2",
) -> str:
"""`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. `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
)
template = _MANIFEST_TEMPLATES.get(version, _MANIFEST_TEMPLATE_1_2)
return template.format(
identifier=escape(slug),
title=escape(game_name),
file_entries=file_entries,
)