Dev #1
@@ -1,4 +1,4 @@
|
||||
from flask import jsonify
|
||||
from flask import jsonify, request
|
||||
|
||||
import screens
|
||||
|
||||
@@ -10,9 +10,17 @@ def flow_node_run_data(slug, node_id):
|
||||
"""Exécuté côté serveur au moment du clic, en mode jouable, pour un nœud
|
||||
Action de type "Modifier une donnée d'un objet" — identique au
|
||||
fonctionnement de l'ancienne action_run_data, mais lit sa config depuis
|
||||
un nœud du graphe de logique plutôt que depuis la table _actions."""
|
||||
un nœud du graphe de logique plutôt que depuis la table _actions.
|
||||
|
||||
Si le nœud vise CLICKED_ROW_ID (voir "Ligne cliquée (Répéteur)" dans
|
||||
screen_edit.html), la vraie ligne n'est connue qu'au moment du clic —
|
||||
le client (play.html) la transmet dans le corps JSON de cette requête,
|
||||
et elle remplace ici target_row_id avant d'exécuter l'action."""
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
|
||||
return jsonify({"ok": False}), 400
|
||||
if node.get("target_row_id") == screens.CLICKED_ROW_ID:
|
||||
body = request.get_json(silent=True) or {}
|
||||
node = {**node, "target_row_id": body.get("clicked_row_id")}
|
||||
ok = screens.apply_data_action(slug, node)
|
||||
return jsonify({"ok": ok})
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ from .labels.element_action_properties import (
|
||||
from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBILITY_LABELS
|
||||
from .labels.data_operations import DATA_OPERATIONS, DATA_OPERATION_LABELS
|
||||
|
||||
from .flow.constants import TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS
|
||||
from .flow.constants import TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID
|
||||
|
||||
from .rendering.render_element_html import render_element_html
|
||||
from .rendering.trigger_for import _trigger_for
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
# casser sur les jeux déjà créés, mais n'est plus exposé dans l'éditeur ni
|
||||
# utilisé par le mode jouable.
|
||||
|
||||
# Valeur sentinelle pour cond_row_id/target_row_id (nœud Condition/action
|
||||
# "Modifier une donnée") : "la ligne de Répéteur sur laquelle on vient de
|
||||
# cliquer", résolue au moment de l'exécution (window.lastClickedRowId côté
|
||||
# client — voir play.html) plutôt que figée à la création du nœud. Utile
|
||||
# quand le déclencheur est "Au clic" sur un Répéteur : la ligne cliquée
|
||||
# n'est jamais connue à l'avance (chaque ligne du Répéteur exécute le MÊME
|
||||
# graphe), donc choisir une ligne précise dans l'éditeur n'a pas de sens
|
||||
# ici — -1 ne collisionne jamais avec un vrai id de ligne (toujours >= 1,
|
||||
# AUTOINCREMENT SQLite).
|
||||
CLICKED_ROW_ID = -1
|
||||
|
||||
TRIGGER_EVENTS = [
|
||||
("clic", "Au clic"),
|
||||
("soumission", "À la soumission"),
|
||||
|
||||
+13
-1
@@ -533,7 +533,13 @@
|
||||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||||
} else if (node.action_type === 'modifier_donnee') {
|
||||
const runDataUrl = "{{ url_for('flow_node_run_data', slug=game.slug, node_id=999999) }}".replace('999999', node.id);
|
||||
return fetch(runDataUrl, {method: 'POST'})
|
||||
// clicked_row_id : utilisé côté serveur SEULEMENT si ce nœud vise la
|
||||
// ligne cliquée (CLICKED_ROW_ID, voir flow_node_run_data.py) — sans
|
||||
// effet sinon, donc toujours transmis sans condition ici.
|
||||
return fetch(runDataUrl, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'modifier_variable') {
|
||||
@@ -549,6 +555,12 @@
|
||||
// Lit la valeur actuelle d'un champ d'objet dans l'instantané de données
|
||||
// du jeu (gameData.data), pour l'évaluation d'une condition.
|
||||
function readFieldValue(definitionId, rowId, fieldName) {
|
||||
// CLICKED_ROW_ID (-1, voir flow_node_run_data.py) : "la ligne de
|
||||
// Répéteur sur laquelle on vient de cliqué" — jamais connue à l'avance
|
||||
// dans l'éditeur (choisie ici via "🖱️ Ligne cliquée (Répéteur)"),
|
||||
// résolue seulement au moment de l'évaluation via le dernier clic
|
||||
// capturé (voir bindClicks() plus haut).
|
||||
if (rowId === -1) rowId = window.lastClickedRowId;
|
||||
const rows = gameData.data[String(definitionId)] || [];
|
||||
const row = rows.find(r => r.id === rowId);
|
||||
return row ? row[fieldName] : undefined;
|
||||
|
||||
@@ -2175,15 +2175,31 @@ function closeNodeForm() {
|
||||
currentNodeFormType = null;
|
||||
}
|
||||
|
||||
// CLICKED_ROW_ID (-1, voir screens/flow/constants.py) : "la ligne de
|
||||
// Répéteur sur laquelle on vient de cliquer" — pour un déclencheur "Au
|
||||
// clic" posé sur un Répéteur, chaque ligne exécute le MÊME graphe et la
|
||||
// ligne cliquée n'est jamais connue à l'avance ; choisir une ligne fixe
|
||||
// dans l'éditeur n'a donc pas de sens pour ce cas. Résolue au moment de
|
||||
// l'exécution (window.lastClickedRowId, voir play.html), pas à la
|
||||
// création du nœud — proposée en tête de TOUTE liste "Ligne concernée",
|
||||
// avant les lignes réelles de l'objet.
|
||||
var FLOW_CLICKED_ROW_ID = -1;
|
||||
|
||||
function _flowRowOptionsHtml(info) {
|
||||
var options = '<option value="' + FLOW_CLICKED_ROW_ID + '">🖱️ Ligne cliquée (Répéteur)</option>';
|
||||
options += info.rows.length
|
||||
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
|
||||
: '<option value="" disabled>Aucune donnée dans cet objet</option>';
|
||||
return options;
|
||||
}
|
||||
|
||||
function onFlowDefinitionChange(prefix) {
|
||||
var defSel = document.getElementById(prefix === 'cond' ? 'nf-cond-definition' : 'nf-action-data-definition');
|
||||
if (!defSel) return;
|
||||
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
||||
var rowSel = document.getElementById(prefix === 'cond' ? 'nf-cond-row' : 'nf-action-data-row');
|
||||
var fieldSel = document.getElementById(prefix === 'cond' ? 'nf-cond-field' : 'nf-action-data-field');
|
||||
rowSel.innerHTML = info.rows.length
|
||||
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
|
||||
: '<option value="">Aucune donnée dans cet objet</option>';
|
||||
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
||||
fieldSel.innerHTML = info.fields.length
|
||||
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
||||
: '<option value="">Aucun champ simple</option>';
|
||||
@@ -2247,9 +2263,7 @@ function onFlowClauseDefinitionChange(idx) {
|
||||
if (!defSel || !rowSel || !fieldSel) return;
|
||||
flowConditionExtraClauses[idx].definition_id = defSel.value;
|
||||
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
||||
rowSel.innerHTML = info.rows.length
|
||||
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
|
||||
: '<option value="">Aucune donnée dans cet objet</option>';
|
||||
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
||||
fieldSel.innerHTML = info.fields.length
|
||||
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
||||
: '<option value="">Aucun champ simple</option>';
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""CLICKED_ROW_ID (-1, voir screens/flow/constants.py) : une action
|
||||
"Modifier une donnée" (ou un nœud Condition) peut viser "la ligne de
|
||||
Répéteur sur laquelle on vient de cliquer" plutôt qu'une ligne fixe -
|
||||
indispensable quand le déclencheur "Au clic" est posé sur un Répéteur,
|
||||
puisque chaque ligne exécute le même graphe et la ligne cliquée n'est
|
||||
jamais connue à l'avance dans l'éditeur."""
|
||||
import json
|
||||
import re
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||
|
||||
|
||||
def _create_email_object(client, slug):
|
||||
resp = client.post(f"/game/{slug}/objects/new", data={
|
||||
"object_name": "Email", "field_name[]": ["is_opened"], "field_type[]": ["booleen"],
|
||||
"field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""],
|
||||
}, follow_redirects=False)
|
||||
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||
client.post(f"/game/{slug}/objects/{def_id}/data/new", data={})
|
||||
client.post(f"/game/{slug}/objects/{def_id}/data/new", data={})
|
||||
return def_id
|
||||
|
||||
|
||||
def test_modifier_donnee_targeting_clicked_row_updates_only_that_row(client, game):
|
||||
def_id = _create_email_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({
|
||||
"node_type": "action", "action_type": "modifier_donnee",
|
||||
"target_definition_id": def_id, "target_row_id": -1, "target_field": "is_opened",
|
||||
"data_operation": "definir_bool_vrai", "data_value": "",
|
||||
}),
|
||||
content_type="application/json",
|
||||
)
|
||||
node = resp.get_json()
|
||||
assert node["target_row_id"] == -1
|
||||
|
||||
resp = client.post(
|
||||
f"/game/{game}/flow/nodes/{node['id']}/run-data",
|
||||
data=json.dumps({"clicked_row_id": 2}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.get_json() == {"ok": True}
|
||||
|
||||
definition = db.get_definition(game, def_id)
|
||||
row1 = db.get_row(game, definition, 1)
|
||||
row2 = db.get_row(game, definition, 2)
|
||||
assert not row1["is_opened"]
|
||||
assert row2["is_opened"]
|
||||
|
||||
|
||||
def test_modifier_donnee_targeting_clicked_row_without_a_click_is_a_noop(client, game):
|
||||
"""Pas de ligne cliquée transmise (ex. déclenché autrement qu'un vrai
|
||||
clic sur une ligne de Répéteur) -> ne doit rien modifier, pas planter."""
|
||||
def_id = _create_email_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({
|
||||
"node_type": "action", "action_type": "modifier_donnee",
|
||||
"target_definition_id": def_id, "target_row_id": -1, "target_field": "is_opened",
|
||||
"data_operation": "definir_bool_vrai", "data_value": "",
|
||||
}),
|
||||
content_type="application/json",
|
||||
)
|
||||
node = resp.get_json()
|
||||
|
||||
resp = client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data", data=json.dumps({}), content_type="application/json")
|
||||
assert resp.get_json() == {"ok": False}
|
||||
|
||||
definition = db.get_definition(game, def_id)
|
||||
assert not db.get_row(game, definition, 1)["is_opened"]
|
||||
assert not db.get_row(game, definition, 2)["is_opened"]
|
||||
|
||||
|
||||
def test_ordinary_fixed_row_target_is_unaffected(client, game):
|
||||
"""Non-régression : une action visant une ligne FIXE (pas -1) continue
|
||||
d'ignorer tout clicked_row_id transmis."""
|
||||
def_id = _create_email_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({
|
||||
"node_type": "action", "action_type": "modifier_donnee",
|
||||
"target_definition_id": def_id, "target_row_id": 1, "target_field": "is_opened",
|
||||
"data_operation": "definir_bool_vrai", "data_value": "",
|
||||
}),
|
||||
content_type="application/json",
|
||||
)
|
||||
node = resp.get_json()
|
||||
client.post(f"/game/{game}/flow/nodes/{node['id']}/run-data", data=json.dumps({"clicked_row_id": 2}), content_type="application/json")
|
||||
|
||||
definition = db.get_definition(game, def_id)
|
||||
assert db.get_row(game, definition, 1)["is_opened"]
|
||||
assert not db.get_row(game, definition, 2)["is_opened"]
|
||||
|
||||
|
||||
def test_clicked_row_option_appears_in_the_flow_node_editor(client, game):
|
||||
_create_email_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||
assert "FLOW_CLICKED_ROW_ID" in html
|
||||
assert "Ligne cliquée" in html
|
||||
Reference in New Issue
Block a user