Export Web/SCORM : corrige les URLs absolues cassées en local (file://)
Bug signalé : icône disparue et texte suivant du dialogue absent après export. render_icone.py (et son miroir JS, forgeRenderIcone) bakaient une URL "/static/icons/..." en dur, correcte en ligne (racine du serveur Flask) mais bloquée par CORS une fois ouverte en local (file:///static/...). Même souci pour les fichiers envoyés par le créateur (/game/<slug>/uploads/..., jamais dans static/, jamais copiés par le paquet). Ajoute _relativize_absolute_urls (réécriture globale post-rendu du HTML) + copie du dossier uploads/, et relativise le port JS de l'icône pour toute régénération après action en mémoire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f3b72d73c9
commit
7bc4c89eaa
@@ -44,6 +44,36 @@ def _copy_static_assets(staging_dir, slug):
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def _copy_uploads(staging_dir, slug):
|
||||
"""Fichiers envoyés par le créateur (widget Image/Vidéo, "Envoyer un
|
||||
fichier" — voir routes/uploads/upload_file.py) : servis EN LIGNE par
|
||||
une route Flask (/game/<slug>/uploads/<fichier>, jamais static/), donc
|
||||
jamais copiés par _copy_static_assets ci-dessus. Absents s'il n'y en a
|
||||
aucun (os.path.isdir garde-fou)."""
|
||||
src = os.path.join(db.game_dir(slug), "uploads")
|
||||
if os.path.isdir(src):
|
||||
shutil.copytree(src, os.path.join(staging_dir, "uploads"))
|
||||
|
||||
|
||||
def _relativize_absolute_urls(html, slug):
|
||||
"""screens/rendering (render_icone.py) et screens/labels (sprite_
|
||||
library.py/animal_sprite_library.py) construisent certaines URLs en
|
||||
DUR avec un préfixe absolu "/static/..." ou "/game/<slug>/uploads/..."
|
||||
(corrects EN LIGNE, où ils partent de la racine du serveur Flask) —
|
||||
déjà bakées dans le HTML/JSON rendu à ce stade, impossible à
|
||||
distinguer d'un bête texte sans réécrire chaque fonction de rendu une
|
||||
à une. Un remplacement global est plus sûr : ces deux préfixes précis
|
||||
(le second inclut le slug du jeu) n'ont aucune raison d'apparaître
|
||||
ailleurs que dans une URL réellement générée par le moteur. Sans ce
|
||||
correctif, un index.html ouvert en local (file://) échouait à charger
|
||||
icônes/sprites/fichiers envoyés (CORS bloque une requête absolue vers
|
||||
file:///static/... — "la requête CORS n'utilise pas HTTP")."""
|
||||
return (
|
||||
html.replace(f"/game/{slug}/uploads/", "uploads/")
|
||||
.replace("/static/", "static/")
|
||||
)
|
||||
|
||||
|
||||
def _list_package_files(staging_dir):
|
||||
"""Chemins relatifs de tout ce qui a déjà été écrit dans staging_dir
|
||||
(hors imsmanifest.xml lui-même) — le manifeste doit les lister TOUS
|
||||
@@ -81,11 +111,13 @@ def build_scorm_zip(slug):
|
||||
staging_dir = tempfile.mkdtemp(prefix="forge_scorm_")
|
||||
try:
|
||||
_copy_static_assets(staging_dir, slug)
|
||||
_copy_uploads(staging_dir, 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,
|
||||
)
|
||||
html = _relativize_absolute_urls(html, slug)
|
||||
with open(os.path.join(staging_dir, "index.html"), "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ test('icône — repli sur "star" si _icon_slug absent', () => {
|
||||
const html = forgeRenderElementHtml(el, baseGameData(), {}, {});
|
||||
// le style entier passe par forgeHtmlEscape (comme html.escape() côté
|
||||
// serveur) : l'apostrophe de url('...') devient '.
|
||||
assert.ok(html.includes("--icon-url:url('/static/icons/star.svg')"));
|
||||
assert.ok(html.includes("--icon-url:url('static/icons/star.svg')"));
|
||||
});
|
||||
|
||||
test('onglets — un panneau actif, les autres masqués', () => {
|
||||
|
||||
@@ -73,7 +73,13 @@ function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirect
|
||||
var existingClass = attrs.class || "";
|
||||
delete attrs.class;
|
||||
attrs.class = (existingClass + " icon-svg").trim();
|
||||
var iconUrl = "/static/icons/" + iconSlug + ".svg";
|
||||
// Chemin RELATIF (jamais "/static/...") : ce fichier ne s'exécute que
|
||||
// dans le paquet exporté (window.FORGE_OFFLINE, voir render-element.js),
|
||||
// ouvert en local (file://) sans racine de serveur — un chemin absolu y
|
||||
// déclenche un blocage CORS ("la requête CORS n'utilise pas HTTP"),
|
||||
// exactement le bug qui affectait aussi le rendu initial côté serveur
|
||||
// (voir _relativize_absolute_urls dans publish/build_scorm_package.py).
|
||||
var iconUrl = "static/icons/" + iconSlug + ".svg";
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');";
|
||||
return "<i " + forgeAttrString(attrs, style) + "></i>";
|
||||
}
|
||||
|
||||
@@ -50,6 +50,32 @@ def test_export_scorm_index_html_is_offline_and_self_contained(client, game):
|
||||
assert "let gameData = " in html
|
||||
|
||||
|
||||
def test_export_scorm_icon_widget_uses_a_relative_url(client, game):
|
||||
"""Régression signalée par un utilisateur : render_icone.py construit
|
||||
son URL en dur avec un préfixe "/static/..." (correct EN LIGNE, où il
|
||||
part de la racine du serveur Flask) — jamais réécrit par le refactor
|
||||
asset_url de templates/play.html (celui-ci ne touche que les <script>/
|
||||
<link> du template, pas le rendered_html déjà bake dans le payload).
|
||||
Sans _relativize_absolute_urls (build_scorm_package.py), un index.html
|
||||
ouvert en local (file://) déclenchait un blocage CORS sur cette icône
|
||||
("la requête CORS n'utilise pas HTTP") — voir aussi le miroir JS
|
||||
(forgeRenderIcone, static/js/play/offline/render-special-widgets.js),
|
||||
qui régénère ce même widget après une action, et devait être corrigé
|
||||
séparément."""
|
||||
import re
|
||||
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
||||
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "icone"}, follow_redirects=False)
|
||||
assert resp.status_code == 302, resp.data
|
||||
|
||||
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 "static/icons/star.svg" in html
|
||||
assert "/static/icons/star.svg" not in html
|
||||
|
||||
|
||||
def test_export_scorm_manifest_lists_index_html_as_the_sco(client, game):
|
||||
resp = client.post(f"/game/{game}/export-scorm")
|
||||
import io
|
||||
|
||||
Reference in New Issue
Block a user