Dev #10

Merged
w-vandal merged 22 commits from dev into main 2026-09-02 19:17:50 +00:00
4 changed files with 66 additions and 2 deletions
Showing only changes of commit 7bc4c89eaa - Show all commits
+32
View File
@@ -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 &#x27;.
assert.ok(html.includes("--icon-url:url(&#x27;/static/icons/star.svg&#x27;)"));
assert.ok(html.includes("--icon-url:url(&#x27;static/icons/star.svg&#x27;)"));
});
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>";
}
+26
View File
@@ -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