Export Web/SCORM : corrige la résolution des champs "relation" hors ligne
Bug signalé : une Donnée liée/un filtre référençant un champ "relation"
affichait "{{champ}}" tel quel une fois exporté, alors qu'il fonctionnait
en ligne. Deux causes cumulées :
- full_game_payload.py lisait la colonne SQL "<champ>" au lieu de
"<champ>_id" (seule vraie colonne d'un champ relation, voir
_field_column dans filter_repeater_rows.py) en construisant
gameData.data — une valeur toujours None. Invisible en ligne (les
filtres y requêtent la base fraîche, jamais cette snapshot), mais
fatal hors ligne (aucune base à requêter).
- Une fois ce None corrigé, le port JS (forgeFieldColumn) relisait
cette même valeur sous une clé slugifiée+suffixée ("boss_id") alors
que gameData.data est déjà indexé par le nom D'AFFICHAGE du champ
("boss") — mismatch qui ne se voyait que sur un champ relation (le
seul cas où slugify(nom)+suffixe diverge du nom original). Supprime
ce port erroné, lit directement row[fieldName] partout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7bc4c89eaa
commit
e08c53e042
@@ -96,7 +96,23 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
|||||||
for d in definitions:
|
for d in definitions:
|
||||||
full = db.get_definition(slug, d["id"])
|
full = db.get_definition(slug, d["id"])
|
||||||
rows = db.list_rows(slug, full, player_id)
|
rows = db.list_rows(slug, full, player_id)
|
||||||
cols = {f["name"]: db.slugify(f["name"]).replace("-", "_") for f in full["fields"]}
|
# Un champ "relation" vit dans une colonne SQL "<champ>_id", jamais
|
||||||
|
# sous son nom "propre" (voir _field_column dans
|
||||||
|
# filter_repeater_rows.py, la même règle) — sans ce cas particulier,
|
||||||
|
# un champ relation lisait toujours une colonne inexistante et
|
||||||
|
# renvoyait systématiquement None ici. Inoffensif EN LIGNE (cette
|
||||||
|
# snapshot n'est jamais utilisée pour un filtre/une Donnée liée : ces
|
||||||
|
# derniers requêtent la base fraîche via filter_repeater_rows.py) —
|
||||||
|
# mais fatal pour le port hors ligne (export Web/SCORM), qui n'a
|
||||||
|
# QUE cette snapshot, aucune base à requêter (bug signalé par un
|
||||||
|
# utilisateur : un filtre sur un champ relation ne matchait jamais
|
||||||
|
# rien une fois exporté, laissant "{{champ}}" affiché tel quel).
|
||||||
|
cols = {}
|
||||||
|
for f in full["fields"]:
|
||||||
|
col = db.slugify(f["name"]).replace("-", "_")
|
||||||
|
if f["type"] == "relation":
|
||||||
|
col += "_id"
|
||||||
|
cols[f["name"]] = col
|
||||||
data[str(d["id"])] = [
|
data[str(d["id"])] = [
|
||||||
{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]}
|
{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]}
|
||||||
for r in rows
|
for r in rows
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
forgeDecodeClauses, forgeResolveVariablePath, forgeFieldColumn, forgeSlugifyFieldName,
|
forgeDecodeClauses, forgeResolveVariablePath,
|
||||||
forgeResolveFilterValue, forgeFilterRowsByClauses, forgeFilterRepeaterRows,
|
forgeResolveFilterValue, forgeClauseMatches, forgeFilterRowsByClauses, forgeFilterRepeaterRows,
|
||||||
} = require('../filter-repeater-rows.js');
|
} = require('../filter-repeater-rows.js');
|
||||||
|
|
||||||
function baseGameData() {
|
function baseGameData() {
|
||||||
@@ -17,11 +17,15 @@ function baseGameData() {
|
|||||||
},
|
},
|
||||||
definition_names: { '1': 'Progression', '2': 'Ennemis' },
|
definition_names: { '1': 'Progression', '2': 'Ennemis' },
|
||||||
fields_meta: {
|
fields_meta: {
|
||||||
|
// "boss" (relation) : gameData.data (voir full_game_payload.py) est
|
||||||
|
// déjà indexé par le nom D'AFFICHAGE du champ ("boss"), JAMAIS par sa
|
||||||
|
// colonne SQL réelle ("boss_id") — contrairement à côté serveur
|
||||||
|
// (filter_repeater_rows.py::_field_column), qui lit la base fraîche.
|
||||||
'1': [{ name: 'niveau_actuel', type: 'nombre_entier' }, { name: 'boss', type: 'relation' }],
|
'1': [{ name: 'niveau_actuel', type: 'nombre_entier' }, { name: 'boss', type: 'relation' }],
|
||||||
'2': [{ name: 'nom', type: 'texte' }, { name: 'pv', type: 'nombre_entier' }],
|
'2': [{ name: 'nom', type: 'texte' }, { name: 'pv', type: 'nombre_entier' }],
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
'1': [{ id: 1, niveau_actuel: 4, boss_id: 7 }],
|
'1': [{ id: 1, niveau_actuel: 4, boss: 7 }],
|
||||||
'2': [
|
'2': [
|
||||||
{ id: 1, nom: 'Slime', pv: 10 },
|
{ id: 1, nom: 'Slime', pv: 10 },
|
||||||
{ id: 2, nom: 'Dragon', pv: 100 },
|
{ id: 2, nom: 'Dragon', pv: 100 },
|
||||||
@@ -30,15 +34,21 @@ function baseGameData() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('forgeSlugifyFieldName correspond à db.slugify(...).replace("-","_")', () => {
|
// Régression signalée par un utilisateur : un filtre/une Donnée liée sur
|
||||||
assert.equal(forgeSlugifyFieldName('Nom Complet'), 'nom_complet');
|
// un champ "relation" ne matchait jamais rien une fois exporté (le port
|
||||||
assert.equal(forgeSlugifyFieldName('Âge (ans)'), 'ge_ans');
|
// hors ligne lisait à tort row[nom_slugifié + "_id"], une clé qui n'existe
|
||||||
|
// pas dans gameData.data), laissant "{{champ}}" affiché tel quel au lieu
|
||||||
|
// de la valeur résolue.
|
||||||
|
test('forgeClauseMatches — un champ "relation" se lit directement par son nom (pas de colonne SQL à reconstruire)', () => {
|
||||||
|
const gd = baseGameData();
|
||||||
|
const row = gd.data['1'][0];
|
||||||
|
const clause = { champ: 'boss', operateur: 'egal', valeur: '7' };
|
||||||
|
assert.equal(forgeClauseMatches(gd, '1', row, clause), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('forgeFieldColumn ajoute _id pour un champ relation', () => {
|
test('forgeResolveFilterValue — {{Objet.champ}} sur un champ "relation"', () => {
|
||||||
const fields = [{ name: 'boss', type: 'relation' }, { name: 'nom', type: 'texte' }];
|
const gd = baseGameData();
|
||||||
assert.equal(forgeFieldColumn(fields, 'boss'), 'boss_id');
|
assert.equal(forgeResolveFilterValue(gd, '{{Progression.boss}}'), 7);
|
||||||
assert.equal(forgeFieldColumn(fields, 'nom'), 'nom');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('forgeResolveVariablePath — chemin objet/tableau, chemin vide, JSON invalide', () => {
|
test('forgeResolveVariablePath — chemin objet/tableau, chemin vide, JSON invalide', () => {
|
||||||
|
|||||||
@@ -65,22 +65,19 @@ function forgeResolveVariablePath(rawValue, path) {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port de _field_column — un champ "relation" vit dans une colonne
|
// PAS d'équivalent JS de _field_column (screens/rendering/
|
||||||
// "<champ>_id" (voir screens/data_actions/apply_data_action.py) ; le
|
// filter_repeater_rows.py) : côté serveur, cette fonction traduit un nom
|
||||||
// payload hors ligne (gameData.fields_meta) indexe déjà les lignes par le
|
// de champ vers la VRAIE colonne SQL ("<champ>_id" pour une relation),
|
||||||
// nom SLUGIFIÉ du champ (voir full_game_payload.py), jamais par son nom
|
// nécessaire puisque _resolve_filter_value/_clause_matches lisent la base
|
||||||
// affiché — slugifyLike() reproduit db.slugify()+replace("-","_").
|
// FRAÎCHE à chaque appel (SELECT * -> colonnes réelles). Hors ligne, il
|
||||||
function forgeSlugifyFieldName(name) {
|
// n'y a AUCUNE base à requêter : gameData.data (voir full_game_payload.py,
|
||||||
return String(name).trim().toLowerCase()
|
// `{fname: r.get(col) ...}`) est déjà ré-indexé par le nom D'AFFICHAGE du
|
||||||
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-/g, "_") || "jeu";
|
// champ, PAS par sa colonne SQL — donc `row[fieldName]` suffit partout ici
|
||||||
}
|
// (une ancienne version de ce fichier appelait à tort une slugification
|
||||||
|
// ici, un bug jamais visible en ligne mais qui rendait tout champ
|
||||||
function forgeFieldColumn(fieldsMetaForDef, fieldName) {
|
// "relation" illisible une fois exporté : le filtre/la Donnée liée
|
||||||
var col = forgeSlugifyFieldName(fieldName);
|
// correspondants ne matchaient jamais rien, laissant "{{champ}}" affiché
|
||||||
var field = (fieldsMetaForDef || []).find(function (f) { return f.name === fieldName; });
|
// tel quel — bug signalé par un utilisateur).
|
||||||
if (field && field.type === "relation") col += "_id";
|
|
||||||
return col;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Port de _resolve_filter_value — {{Objet.champ}} (ligne la plus récente
|
// Port de _resolve_filter_value — {{Objet.champ}} (ligne la plus récente
|
||||||
// d'un AUTRE objet, convention "1 objet = 1 ligne = état de partie") ou
|
// d'un AUTRE objet, convention "1 objet = 1 ligne = état de partie") ou
|
||||||
@@ -106,8 +103,7 @@ function forgeResolveFilterValue(gameData, rawValue) {
|
|||||||
if (names[defId] === objectName) {
|
if (names[defId] === objectName) {
|
||||||
var rows = gameData.data && gameData.data[defId];
|
var rows = gameData.data && gameData.data[defId];
|
||||||
if (rows && rows.length) {
|
if (rows && rows.length) {
|
||||||
var col = forgeFieldColumn(gameData.fields_meta && gameData.fields_meta[defId], fieldName);
|
return rows[0][fieldName];
|
||||||
return rows[0][col];
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -124,8 +120,7 @@ function forgeClauseMatches(gameData, definitionId, row, clause) {
|
|||||||
var fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
|
var fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||||
var field = (fieldsMetaForDef || []).find(function (f) { return f.name === fieldName; });
|
var field = (fieldsMetaForDef || []).find(function (f) { return f.name === fieldName; });
|
||||||
var fieldType = field ? field.type : "texte";
|
var fieldType = field ? field.type : "texte";
|
||||||
var col = forgeFieldColumn(fieldsMetaForDef, fieldName);
|
return compareValues(row[fieldName], operator, expected, fieldType);
|
||||||
return compareValues(row[col], operator, expected, fieldType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port de filter_rows_by_clauses.
|
// Port de filter_rows_by_clauses.
|
||||||
@@ -151,7 +146,7 @@ function forgeFilterRepeaterRows(gameData, definitionId, rows, attrs) {
|
|||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
forgeDecodeClauses, forgeResolveVariablePath, forgeFieldColumn, forgeSlugifyFieldName,
|
forgeDecodeClauses, forgeResolveVariablePath,
|
||||||
forgeResolveFilterValue, forgeClauseMatches, forgeFilterRowsByClauses, forgeFilterRepeaterRows,
|
forgeResolveFilterValue, forgeClauseMatches, forgeFilterRowsByClauses, forgeFilterRepeaterRows,
|
||||||
};
|
};
|
||||||
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ function forgeResolveBoundRowCtx(gameData, attrs) {
|
|||||||
var row = rows[0];
|
var row = rows[0];
|
||||||
var ctx = {};
|
var ctx = {};
|
||||||
fieldsMetaForDef.forEach(function (f) {
|
fieldsMetaForDef.forEach(function (f) {
|
||||||
var v = row[forgeFieldColumn(fieldsMetaForDef, f.name)];
|
// row[f.name] : gameData.data est déjà indexé par le nom D'AFFICHAGE
|
||||||
|
// du champ (voir full_game_payload.py), jamais par sa colonne SQL —
|
||||||
|
// voir filter-repeater-rows.js pour le détail de ce piège.
|
||||||
|
var v = row[f.name];
|
||||||
ctx[f.name] = (v === null || v === undefined || v === "") ? "" : String(v);
|
ctx[f.name] = (v === null || v === undefined || v === "") ? "" : String(v);
|
||||||
});
|
});
|
||||||
return ctx;
|
return ctx;
|
||||||
@@ -319,7 +322,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
var filterMod = require('./filter-repeater-rows.js');
|
var filterMod = require('./filter-repeater-rows.js');
|
||||||
global.forgeResolveFilterValue = global.forgeResolveFilterValue || filterMod.forgeResolveFilterValue;
|
global.forgeResolveFilterValue = global.forgeResolveFilterValue || filterMod.forgeResolveFilterValue;
|
||||||
global.forgeResolveVariablePath = global.forgeResolveVariablePath || filterMod.forgeResolveVariablePath;
|
global.forgeResolveVariablePath = global.forgeResolveVariablePath || filterMod.forgeResolveVariablePath;
|
||||||
global.forgeFieldColumn = global.forgeFieldColumn || filterMod.forgeFieldColumn;
|
|
||||||
global.forgeDecodeClauses = global.forgeDecodeClauses || filterMod.forgeDecodeClauses;
|
global.forgeDecodeClauses = global.forgeDecodeClauses || filterMod.forgeDecodeClauses;
|
||||||
global.forgeFilterRowsByClauses = global.forgeFilterRowsByClauses || filterMod.forgeFilterRowsByClauses;
|
global.forgeFilterRowsByClauses = global.forgeFilterRowsByClauses || filterMod.forgeFilterRowsByClauses;
|
||||||
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ function forgeRenderRepeater(el, meta, gameData, childrenMap, ctx, parentFlexDir
|
|||||||
|
|
||||||
var rows = gameData.data[definitionId] || [];
|
var rows = gameData.data[definitionId] || [];
|
||||||
rows = forgeFilterRepeaterRows(gameData, definitionId, rows, attrsRaw);
|
rows = forgeFilterRepeaterRows(gameData, definitionId, rows, attrsRaw);
|
||||||
var fieldCols = {};
|
|
||||||
fieldsMetaForDef.forEach(function (f) { fieldCols[f.name] = forgeFieldColumn(fieldsMetaForDef, f.name); });
|
|
||||||
|
|
||||||
var templateTree = null;
|
var templateTree = null;
|
||||||
if (templateTypeId) {
|
if (templateTypeId) {
|
||||||
@@ -35,9 +33,13 @@ function forgeRenderRepeater(el, meta, gameData, childrenMap, ctx, parentFlexDir
|
|||||||
|
|
||||||
var items = rows.map(function (row) {
|
var items = rows.map(function (row) {
|
||||||
var rowCtx = Object.assign({}, ctx || {});
|
var rowCtx = Object.assign({}, ctx || {});
|
||||||
Object.keys(fieldCols).forEach(function (fname) {
|
// gameData.data est déjà indexé par le nom D'AFFICHAGE du champ (voir
|
||||||
var v = row[fieldCols[fname]];
|
// full_game_payload.py), jamais par sa colonne SQL — row[f.name] lit
|
||||||
rowCtx[fname] = v === null || v === undefined ? "" : String(v);
|
// donc directement la bonne valeur, y compris pour un champ "relation"
|
||||||
|
// (voir filter-repeater-rows.js pour le détail de ce piège).
|
||||||
|
fieldsMetaForDef.forEach(function (f) {
|
||||||
|
var v = row[f.name];
|
||||||
|
rowCtx[f.name] = v === null || v === undefined ? "" : String(v);
|
||||||
});
|
});
|
||||||
var inner;
|
var inner;
|
||||||
if (templateTree) {
|
if (templateTree) {
|
||||||
@@ -73,5 +75,4 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
});
|
});
|
||||||
var filterMod = require('./filter-repeater-rows.js');
|
var filterMod = require('./filter-repeater-rows.js');
|
||||||
global.forgeFilterRepeaterRows = global.forgeFilterRepeaterRows || filterMod.forgeFilterRepeaterRows;
|
global.forgeFilterRepeaterRows = global.forgeFilterRepeaterRows || filterMod.forgeFilterRepeaterRows;
|
||||||
global.forgeFieldColumn = global.forgeFieldColumn || filterMod.forgeFieldColumn;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Export Web/SCORM (voir le plan "port complet du runtime jouable côté
|
// Export Web/SCORM (voir le plan "port complet du runtime jouable côté
|
||||||
// navigateur") — port de screens/rendering/visibility_condition.py
|
// navigateur") — port de screens/rendering/visibility_condition.py
|
||||||
// (is_element_visible). Réutilise forgeResolveFilterValue/
|
// (is_element_visible). Réutilise forgeResolveFilterValue/
|
||||||
// forgeResolveVariablePath/forgeFieldColumn (filter-repeater-rows.js) et
|
// forgeResolveVariablePath (filter-repeater-rows.js) et compareValues
|
||||||
// compareValues (conditions.js), chargés avant ce fichier.
|
// (conditions.js), chargés avant ce fichier.
|
||||||
//
|
//
|
||||||
// `ctx` : les {{champ}} de la ligne en cours quand cet élément est
|
// `ctx` : les {{champ}} de la ligne en cours quand cet élément est
|
||||||
// descendant d'un Répéteur (voir render-element.js) — en mode "objet", si
|
// descendant d'un Répéteur (voir render-element.js) — en mode "objet", si
|
||||||
@@ -43,7 +43,9 @@ function forgeIsElementVisibleOffline(gameData, attrs, ctx) {
|
|||||||
var rows = gameData.data && gameData.data[definitionId];
|
var rows = gameData.data && gameData.data[definitionId];
|
||||||
if (!rows || !rows.length) return true; // objet sans donnée pour l'instant : ne bloque pas
|
if (!rows || !rows.length) return true; // objet sans donnée pour l'instant : ne bloque pas
|
||||||
var row = rows[0]; // convention "état de partie" : une seule ligne
|
var row = rows[0]; // convention "état de partie" : une seule ligne
|
||||||
return compareValues(row[forgeFieldColumn(fieldsMetaForDef, fieldName)], operator, expected, fieldType);
|
// row[fieldName] : gameData.data est déjà indexé par le nom D'AFFICHAGE
|
||||||
|
// du champ (voir full_game_payload.py), jamais par sa colonne SQL.
|
||||||
|
return compareValues(row[fieldName], operator, expected, fieldType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
@@ -51,6 +53,5 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
var filterMod = require('./filter-repeater-rows.js');
|
var filterMod = require('./filter-repeater-rows.js');
|
||||||
global.forgeResolveFilterValue = global.forgeResolveFilterValue || filterMod.forgeResolveFilterValue;
|
global.forgeResolveFilterValue = global.forgeResolveFilterValue || filterMod.forgeResolveFilterValue;
|
||||||
global.forgeResolveVariablePath = global.forgeResolveVariablePath || filterMod.forgeResolveVariablePath;
|
global.forgeResolveVariablePath = global.forgeResolveVariablePath || filterMod.forgeResolveVariablePath;
|
||||||
global.forgeFieldColumn = global.forgeFieldColumn || filterMod.forgeFieldColumn;
|
|
||||||
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
global.compareValues = global.compareValues || require('../conditions.js').compareValues;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,75 @@ def test_export_scorm_icon_widget_uses_a_relative_url(client, game):
|
|||||||
assert "/static/icons/star.svg" not in html
|
assert "/static/icons/star.svg" not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_scorm_databound_text_resolves_a_relation_field(client, game):
|
||||||
|
"""Régression signalée par un utilisateur : un Texte "Donnée liée" (ou
|
||||||
|
un filtre de Répéteur) referençant un champ "relation" affichait
|
||||||
|
"{{champ}}" tel quel une fois exporté, alors qu'il fonctionnait très
|
||||||
|
bien EN LIGNE. Cause racine : full_game_payload.py construisait
|
||||||
|
gameData.data en lisant la colonne SQL "<champ>" au lieu de
|
||||||
|
"<champ>_id" (seule vraie colonne d'un champ relation, voir
|
||||||
|
_field_column dans filter_repeater_rows.py) — une valeur toujours None
|
||||||
|
qu'EN LIGNE personne ne remarquait puisque filter_repeater_rows.py
|
||||||
|
requête la base fraîche à chaque rendu, jamais ce snapshot. Le port
|
||||||
|
hors ligne (static/js/play/offline/), lui, n'a QUE ce snapshot."""
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{game}/objects/new",
|
||||||
|
data={
|
||||||
|
"object_name": "Monstre",
|
||||||
|
"field_name[]": ["nom"],
|
||||||
|
"field_type[]": ["texte"],
|
||||||
|
"field_relation[]": [""],
|
||||||
|
"field_required[]": ["0"],
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 302, resp.data
|
||||||
|
monstre_definition_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||||
|
resp = client.post(f"/game/{game}/objects/{monstre_definition_id}/data/new",
|
||||||
|
data={"nom": "Dragon"}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302, resp.data
|
||||||
|
import db
|
||||||
|
monstre_row_id = db.list_rows(game, db.get_definition(game, monstre_definition_id))[0]["id"]
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{game}/objects/new",
|
||||||
|
data={
|
||||||
|
"object_name": "Niveau",
|
||||||
|
"field_name[]": ["boss"],
|
||||||
|
"field_type[]": ["relation"],
|
||||||
|
"field_relation[]": [str(monstre_definition_id)],
|
||||||
|
"field_required[]": ["0"],
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 302, resp.data
|
||||||
|
definition_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||||
|
resp = client.post(f"/game/{game}/objects/{definition_id}/data/new",
|
||||||
|
data={"boss_id": str(monstre_row_id)}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302, resp.data
|
||||||
|
|
||||||
|
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": "texte"}, follow_redirects=False)
|
||||||
|
element_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{game}/elements/{element_id}/save",
|
||||||
|
data={"ctrl_content": "Boss : {{boss}}", "ctrl_data_definition_id": str(definition_id)},
|
||||||
|
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")
|
||||||
|
# Le "content" brut ("Boss : {{boss}}") reste forcément présent tel
|
||||||
|
# quel dans le JSON du payload (c'est le MODÈLE stocké) — seul le
|
||||||
|
# rendered_html déjà substitué compte ici.
|
||||||
|
assert f"Boss : {monstre_row_id}" 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