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)
|
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):
|
def _list_package_files(staging_dir):
|
||||||
"""Chemins relatifs de tout ce qui a déjà été écrit dans 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
|
(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_")
|
staging_dir = tempfile.mkdtemp(prefix="forge_scorm_")
|
||||||
try:
|
try:
|
||||||
_copy_static_assets(staging_dir, slug)
|
_copy_static_assets(staging_dir, slug)
|
||||||
|
_copy_uploads(staging_dir, slug)
|
||||||
|
|
||||||
html = render_template(
|
html = render_template(
|
||||||
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
||||||
offline_mode=True, asset_url=lambda filename: "static/" + filename,
|
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:
|
with open(os.path.join(staging_dir, "index.html"), "w", encoding="utf-8") as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ test('icône — repli sur "star" si _icon_slug absent', () => {
|
|||||||
const html = forgeRenderElementHtml(el, baseGameData(), {}, {});
|
const html = forgeRenderElementHtml(el, baseGameData(), {}, {});
|
||||||
// le style entier passe par forgeHtmlEscape (comme html.escape() côté
|
// le style entier passe par forgeHtmlEscape (comme html.escape() côté
|
||||||
// serveur) : l'apostrophe de url('...') devient '.
|
// 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', () => {
|
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 || "";
|
var existingClass = attrs.class || "";
|
||||||
delete attrs.class;
|
delete attrs.class;
|
||||||
attrs.class = (existingClass + " icon-svg").trim();
|
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 + "');";
|
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');";
|
||||||
return "<i " + forgeAttrString(attrs, style) + "></i>";
|
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
|
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):
|
def test_export_scorm_manifest_lists_index_html_as_the_sco(client, game):
|
||||||
resp = client.post(f"/game/{game}/export-scorm")
|
resp = client.post(f"/game/{game}/export-scorm")
|
||||||
import io
|
import io
|
||||||
|
|||||||
Reference in New Issue
Block a user