Phase 4 : ajout dynamique d'une ligne à l'exécution
Nouveau sentinel LAST_INSERTED_ROW_ID = -3 (screens/flow/constants.py), aux côtés de CLICKED_ROW_ID = -1 — même principe : un id de ligne n'existe qu'APRÈS l'insertion, jamais connu à la création du nœud. Permet d'enchaîner un nœud "Ajouter une ligne" (crée une ligne VIDE) puis un ou plusieurs nœuds "Modifier une donnée" déjà existants (ciblant "➕ Dernière ligne ajoutée" dans le sélecteur "Ligne concernée", partagé avec les conditions) pour renseigner ses champs — réutilise 100% du mécanisme actuel, aucun nouveau format de payload multi-champs. screens/data_actions/apply_add_row_action.py : db.get_definition + db.insert_row(slug, definition, {}, player_id) — la ligne appartient au joueur qui agit pour un objet per_player (Phase 1). Nouvelles routes (créateur ET publique, comme prévu dès la Phase 1) : POST /game/<slug>/flow/nodes/<id>/run-add-row et son miroir /jouer/<slug>/.../run-add-row — renvoient {"ok", "row_id"}. routes/flow/flow_node_run_data.py (+ son miroir public) : résout aussi LAST_INSERTED_ROW_ID (en plus de CLICKED_ROW_ID déjà en place) via last_inserted_row_id transmis par le client. static/js/play/actions.js : runActionNode branche "ajouter_ligne" -> fetch la nouvelle route, pose window.lastInsertedRowId, puis refreshRuntimeData() (un Répéteur lié affiche la nouvelle ligne au prochain rendu, confirmé par l'audit préalable — aucun ajustement du mécanisme de rafraîchissement nécessaire). La branche "modifier_donnee" transmet désormais aussi last_inserted_row_id, comme clicked_row_id. templates/screen_edit.html + static/js/screen_edit/flow-editor.js : nouveau type d'action "Ajouter une ligne à un objet" (juste un sélecteur d'objet, aucun champ à remplir — le rappel du fonctionnement enchaîné est affiché directement dans le formulaire) ; le sélecteur "Ligne concernée" (partagé Condition/Modifier une donnée) gagne l'option "➕ Dernière ligne ajoutée" à côté de "🖱️ Ligne cliquée". Vérifié : 259 tests passent (4 nouveaux, dont un bout-en-bout via HTTP qui enchaîne réellement les deux nœuds et vérifie le champ renseigné, et un qui verrouille l'isolation par joueur de la ligne créée), 13 tests node:test toujours au vert, syntaxe JS validée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
727c3c97ba
commit
92fbfbc9dd
@@ -31,6 +31,7 @@ _PUBLIC_ENDPOINTS = {
|
||||
"forgot_password", "reset_password",
|
||||
"game_play_public", "runtime_payload_public",
|
||||
"flow_node_run_data_public", "flow_node_run_variable_public",
|
||||
"flow_node_run_add_row_public",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ from . import (
|
||||
flow_edge_delete,
|
||||
flow_node_run_data,
|
||||
flow_node_run_variable,
|
||||
flow_node_run_add_row,
|
||||
runtime_payload,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from flask import jsonify
|
||||
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
|
||||
|
||||
@app.route("/game/<slug>/flow/nodes/<int:node_id>/run-add-row", methods=["POST"])
|
||||
def flow_node_run_add_row(slug, node_id):
|
||||
"""Exécuté côté serveur au moment du clic, en mode jouable, pour un
|
||||
nœud Action de type "ajouter_ligne" (Phase 4) — même principe que
|
||||
flow_node_run_data.py, mais crée une ligne VIDE plutôt que d'en
|
||||
modifier une existante. Renvoie l'id de la nouvelle ligne, à
|
||||
transmettre par le client (voir runActionNode() dans
|
||||
static/js/play/actions.js) aux nœuds suivants qui la ciblent via
|
||||
LAST_INSERTED_ROW_ID."""
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "ajouter_ligne":
|
||||
return jsonify({"ok": False}), 400
|
||||
row_id = screens.apply_add_row_action(slug, node)
|
||||
return jsonify({"ok": row_id is not None, "row_id": row_id})
|
||||
@@ -22,5 +22,13 @@ def flow_node_run_data(slug, node_id):
|
||||
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")}
|
||||
elif node.get("target_row_id") == screens.LAST_INSERTED_ROW_ID:
|
||||
# Phase 4 — enchaînement "Ajouter une ligne" -> "Modifier une
|
||||
# donnée" : la vraie ligne (créée juste avant par le nœud
|
||||
# précédent, voir flow_node_run_add_row.py) n'est connue qu'au
|
||||
# moment du clic, transmise par le client (voir runActionNode()
|
||||
# dans static/js/play/actions.js).
|
||||
body = request.get_json(silent=True) or {}
|
||||
node = {**node, "target_row_id": body.get("last_inserted_row_id")}
|
||||
ok = screens.apply_data_action(slug, node)
|
||||
return jsonify({"ok": ok})
|
||||
|
||||
@@ -21,4 +21,5 @@ def game_play(slug):
|
||||
runtime_payload_url=url_for("runtime_payload", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable", slug=slug, node_id=999999),
|
||||
flow_node_run_add_row_url=url_for("flow_node_run_add_row", slug=slug, node_id=999999),
|
||||
)
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
from . import game_play_public, runtime_payload_public, flow_node_run_data_public, flow_node_run_variable_public
|
||||
from . import (
|
||||
game_play_public,
|
||||
runtime_payload_public,
|
||||
flow_node_run_data_public,
|
||||
flow_node_run_variable_public,
|
||||
flow_node_run_add_row_public,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask import jsonify, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/flow/nodes/<int:node_id>/run-add-row", methods=["POST"])
|
||||
def flow_node_run_add_row_public(slug, node_id):
|
||||
"""Miroir public de routes/flow/flow_node_run_add_row.py (aperçu
|
||||
créateur) — voir routes/public_play/game_play_public.py pour le
|
||||
principe général."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "ajouter_ligne":
|
||||
return jsonify({"ok": False}), 400
|
||||
row_id = screens.apply_add_row_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": row_id is not None, "row_id": row_id}))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -22,6 +22,10 @@ def flow_node_run_data_public(slug, node_id):
|
||||
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")}
|
||||
elif node.get("target_row_id") == screens.LAST_INSERTED_ROW_ID:
|
||||
# Phase 4 — voir routes/flow/flow_node_run_data.py.
|
||||
body = request.get_json(silent=True) or {}
|
||||
node = {**node, "target_row_id": body.get("last_inserted_row_id")}
|
||||
ok = screens.apply_data_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": ok}))
|
||||
if is_new:
|
||||
|
||||
@@ -29,6 +29,7 @@ def game_play_public(slug):
|
||||
runtime_payload_url=url_for("runtime_payload_public", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data_public", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable_public", slug=slug, node_id=999999),
|
||||
flow_node_run_add_row_url=url_for("flow_node_run_add_row_public", slug=slug, node_id=999999),
|
||||
)
|
||||
)
|
||||
if is_new:
|
||||
|
||||
@@ -37,6 +37,10 @@ def screen_edit(slug, screen_id):
|
||||
|
||||
definitions = db.list_definitions(slug)
|
||||
definitions_data = {str(d["id"]): screens.data_definition_options(slug, d["id"]) for d in definitions}
|
||||
# Phase 4 — nodeLabel() (static/js/screen_edit/flow-editor.js) affiche
|
||||
# le nom de l'objet visé par un nœud "Ajouter une ligne" ; DEFINITIONS_DATA
|
||||
# (ci-dessus) ne porte que lignes/champs, jamais le nom lui-même.
|
||||
definitions_names = {d["id"]: d["name"] for d in definitions}
|
||||
|
||||
# list_flow_blocks() rattache paresseusement les nœuds "orphelins"
|
||||
# (block_id NULL, jeux créés avant les blocs) à un "Bloc principal" —
|
||||
@@ -94,4 +98,5 @@ def screen_edit(slug, screen_id):
|
||||
custom_events_map_json=json.dumps(custom_events_map),
|
||||
blocks_view=blocks_view,
|
||||
flow_blocks_json=json.dumps(flow_blocks),
|
||||
definitions_names_json=json.dumps(definitions_names),
|
||||
)
|
||||
|
||||
+2
-1
@@ -56,7 +56,7 @@ from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBI
|
||||
from .labels.data_operations import DATA_OPERATIONS, DATA_OPERATION_LABELS
|
||||
|
||||
from .flow.constants import (
|
||||
TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID,
|
||||
TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, LAST_INSERTED_ROW_ID,
|
||||
)
|
||||
|
||||
from .rendering.render_element_html import render_element_html
|
||||
@@ -117,6 +117,7 @@ from .flow.blocks.blocks_view import blocks_view
|
||||
|
||||
from .data_actions.apply_data_action import apply_data_action
|
||||
from .data_actions.apply_variable_action import apply_variable_action
|
||||
from .data_actions.apply_add_row_action import apply_add_row_action
|
||||
|
||||
from .animations.ensure_animation_schema import ensure_animation_schema
|
||||
from .animations.list_animation_clips import list_animation_clips
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import db
|
||||
|
||||
|
||||
def apply_add_row_action(slug, action, player_id=db.PLAYER_SHARED):
|
||||
"""Exécute au moment du clic (mode jouable) une action "ajouter_ligne" :
|
||||
crée une ligne VIDE dans l'objet visé (aucun champ renseigné ici —
|
||||
c'est à un ou plusieurs nœuds "Modifier une donnée" enchaînés,
|
||||
ciblant LAST_INSERTED_ROW_ID, de le faire ensuite, voir
|
||||
screens/flow/constants.py). `player_id` (état par joueur, Phase 1) :
|
||||
la ligne appartient à CE joueur pour un objet per_player (voir
|
||||
db/rows/insert_row.py). Renvoie l'id de la nouvelle ligne, ou None en
|
||||
cas d'échec (objet introuvable)."""
|
||||
definition_id = action.get("target_definition_id")
|
||||
if not definition_id:
|
||||
return None
|
||||
definition = db.get_definition(slug, definition_id)
|
||||
if not definition:
|
||||
return None
|
||||
return db.insert_row(slug, definition, {}, player_id)
|
||||
@@ -21,6 +21,17 @@
|
||||
# AUTOINCREMENT SQLite).
|
||||
CLICKED_ROW_ID = -1
|
||||
|
||||
# Phase 4 (moteur) — "la ligne qu'un nœud Action 'Ajouter une ligne'
|
||||
# vient de créer" : un id de ligne n'existe qu'APRÈS l'insertion, donc
|
||||
# jamais connu à la création du nœud, exactement comme CLICKED_ROW_ID
|
||||
# ci-dessus. Résolue au moment de l'exécution
|
||||
# (window.lastInsertedRowId, voir static/js/play/actions.js) — permet
|
||||
# d'enchaîner "Ajouter une ligne" -> "Modifier une donnée"/condition
|
||||
# ciblant cette même ligne fraîchement créée, sans avoir à inventer un
|
||||
# nouveau format de payload multi-champs pour le nœud "Ajouter une
|
||||
# ligne" lui-même (qui ne crée qu'une ligne VIDE).
|
||||
LAST_INSERTED_ROW_ID = -3
|
||||
|
||||
TRIGGER_EVENTS = [
|
||||
("clic", "Au clic"),
|
||||
("soumission", "À la soumission"),
|
||||
|
||||
@@ -9,5 +9,6 @@ ACTION_TYPE_LABELS = {
|
||||
"activer_onglet": "Afficher cet élément, masquer tous ses frères",
|
||||
"attendre": "Attendre quelques secondes avant de continuer",
|
||||
"declencher_evenement": "Déclencher un événement",
|
||||
"ajouter_ligne": "Ajouter une ligne à un objet",
|
||||
"rien": "Ne rien faire",
|
||||
}
|
||||
|
||||
@@ -173,12 +173,13 @@ function runActionNode(node) {
|
||||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||||
} else if (node.action_type === 'modifier_donnee') {
|
||||
const runDataUrl = FORGE_PLAY_URLS.flowNodeRunData.replace('999999', node.id);
|
||||
// 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.
|
||||
// clicked_row_id/last_inserted_row_id : utilisés côté serveur
|
||||
// SEULEMENT si ce nœud vise respectivement CLICKED_ROW_ID ou
|
||||
// LAST_INSERTED_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 })
|
||||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId, last_inserted_row_id: window.lastInsertedRowId })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
@@ -187,6 +188,19 @@ function runActionNode(node) {
|
||||
return fetch(runVarUrl, {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'ajouter_ligne') {
|
||||
// Phase 4 — crée une ligne VIDE côté serveur ; son id devient
|
||||
// window.lastInsertedRowId pour que les nœuds "Modifier une donnée"
|
||||
// enchaînés (ciblant LAST_INSERTED_ROW_ID, "➕ Dernière ligne
|
||||
// ajoutée") sachent quelle ligne renseigner. refreshRuntimeData()
|
||||
// affiche la nouvelle ligne dans tout Répéteur lié dès ce round-trip.
|
||||
const runAddRowUrl = FORGE_PLAY_URLS.flowNodeRunAddRow.replace('999999', node.id);
|
||||
return fetch(runAddRowUrl, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(function(json) {
|
||||
window.lastInsertedRowId = json.row_id;
|
||||
return refreshRuntimeData();
|
||||
});
|
||||
}
|
||||
// 'rien' : aucune action volontairement.
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -101,6 +101,8 @@ function nodeLabel(node) {
|
||||
// n'est jamais transmis au déclenchement.
|
||||
var dispatchedEvent = CUSTOM_EVENTS_MAP[node.target_custom_event_id];
|
||||
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
|
||||
} else if (node.action_type === 'ajouter_ligne') {
|
||||
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
|
||||
}
|
||||
return base;
|
||||
}
|
||||
@@ -320,9 +322,18 @@ function closeNodeForm() {
|
||||
// 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;
|
||||
// LAST_INSERTED_ROW_ID (-3, voir screens/flow/constants.py) : "la ligne
|
||||
// qu'un nœud Action 'Ajouter une ligne' vient de créer" (Phase 4) — même
|
||||
// principe que CLICKED_ROW_ID ci-dessus, résolue au moment de l'exécution
|
||||
// (window.lastInsertedRowId, voir static/js/play/actions.js) puisqu'un
|
||||
// id de ligne n'existe qu'APRÈS l'insertion, jamais connu à la création
|
||||
// du nœud. Permet d'enchaîner "Ajouter une ligne" -> "Modifier une
|
||||
// donnée"/condition ciblant cette même ligne fraîchement créée.
|
||||
var FLOW_LAST_INSERTED_ROW_ID = -3;
|
||||
|
||||
function _flowRowOptionsHtml(info) {
|
||||
var options = '<option value="' + FLOW_CLICKED_ROW_ID + '">🖱️ Ligne cliquée (Répéteur)</option>';
|
||||
options += '<option value="' + FLOW_LAST_INSERTED_ROW_ID + '">➕ Dernière ligne ajoutée</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>';
|
||||
@@ -468,6 +479,7 @@ function toggleFlowActionFields() {
|
||||
var variableFields = document.getElementById('nf-action-variable-fields');
|
||||
var waitFields = document.getElementById('nf-action-wait-fields');
|
||||
var eventFields = document.getElementById('nf-action-event-fields');
|
||||
var addRowFields = document.getElementById('nf-action-add-row-fields');
|
||||
var isOnglet = typeSel.value === 'activer_onglet';
|
||||
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
||||
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
||||
@@ -475,6 +487,7 @@ function toggleFlowActionFields() {
|
||||
variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
|
||||
waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
|
||||
if (eventFields) eventFields.style.display = typeSel.value === 'declencher_evenement' ? 'block' : 'none';
|
||||
if (addRowFields) addRowFields.style.display = typeSel.value === 'ajouter_ligne' ? 'block' : 'none';
|
||||
document.getElementById('nf-action-onglet-hint').style.display = isOnglet ? 'block' : 'none';
|
||||
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
|
||||
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
|
||||
@@ -641,6 +654,11 @@ function submitNodeForm() {
|
||||
// screens/custom_events/) — c'est à l'écouteur de décider quoi
|
||||
// faire, avec ses propres réglages habituels.
|
||||
payload.target_custom_event_id = parseInt(document.getElementById('nf-action-custom-event').value, 10);
|
||||
} else if (actionType === 'ajouter_ligne') {
|
||||
// Phase 4 — crée une ligne VIDE ; ses champs se renseignent via un ou
|
||||
// plusieurs nœuds "Modifier une donnée" enchaînés, ciblant
|
||||
// FLOW_LAST_INSERTED_ROW_ID ("➕ Dernière ligne ajoutée").
|
||||
payload.target_definition_id = parseInt(document.getElementById('nf-action-add-row-definition').value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -81,17 +81,19 @@
|
||||
// les URLs de routes dont ces fichiers ont besoin (actions.js,
|
||||
// bindings.js), construites ici une seule fois — passées par la route
|
||||
// elle-même (runtime_payload_url/flow_node_run_data_url/
|
||||
// flow_node_run_variable_url) plutôt que construites ici avec des noms
|
||||
// de endpoint fixes : ce même template sert aussi bien l'aperçu
|
||||
// créateur (/game/<slug>/play, routes/play/game_play.py) que la partie
|
||||
// publique par joueur (/jouer/<slug>, routes/public_play/), deux
|
||||
// familles de routes distinctes (voir état par joueur, Phase 1).
|
||||
// flow_node_run_variable_url/flow_node_run_add_row_url) plutôt que
|
||||
// construites ici avec des noms de endpoint fixes : ce même template
|
||||
// sert aussi bien l'aperçu créateur (/game/<slug>/play,
|
||||
// routes/play/game_play.py) que la partie publique par joueur
|
||||
// (/jouer/<slug>, routes/public_play/), deux familles de routes
|
||||
// distinctes (voir état par joueur, Phase 1).
|
||||
let gameData = {{ payload_json|safe }};
|
||||
let screensData = gameData.screens;
|
||||
var FORGE_PLAY_URLS = {
|
||||
runtimePayload: {{ runtime_payload_url|tojson }},
|
||||
flowNodeRunData: {{ flow_node_run_data_url|tojson }},
|
||||
flowNodeRunVariable: {{ flow_node_run_variable_url|tojson }},
|
||||
flowNodeRunAddRow: {{ flow_node_run_add_row_url|tojson }},
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/play/screens.js') }}"></script>
|
||||
|
||||
@@ -439,6 +439,19 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="nf-action-add-row-fields" style="display:none;">
|
||||
{% if not definitions %}
|
||||
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='objects') }}">crées-en un</a>.</p>
|
||||
{% else %}
|
||||
<select id="nf-action-add-row-definition">
|
||||
{% for d in definitions %}
|
||||
<option value="{{ d.id }}">{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="hint">Crée une ligne vide dans cet objet. Enchaîne ensuite un ou plusieurs nœuds "Modifier une donnée" ciblant "➕ Dernière ligne ajoutée" (Ligne concernée) pour renseigner ses champs.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="nf-action-variable-fields" style="display:none;">
|
||||
{% if not global_variables %}
|
||||
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
|
||||
@@ -1072,6 +1085,7 @@
|
||||
var ELEMENT_ADD_URL = "{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}";
|
||||
var CURRENT_SELECTED_ID = {{ selected_id or 'null' }};
|
||||
var DEFINITIONS_DATA = {{ definitions_json|safe }};
|
||||
var DEFINITIONS_NAMES = {{ definitions_names_json|safe }};
|
||||
var FLOW_NODES_INITIAL = {{ flow_nodes_json|safe }};
|
||||
var FLOW_EDGES_INITIAL = {{ flow_edges_json|safe }};
|
||||
var FLOW_BLOCKS = {{ flow_blocks_json|safe }};
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Tests de l'action "Ajouter une ligne" (ajouter_ligne, Phase 4) : crée
|
||||
une ligne VIDE, dont l'id devient LAST_INSERTED_ROW_ID pour un nœud
|
||||
"Modifier une donnée" enchaîné dans le MÊME flow — verrouille le
|
||||
mécanisme complet (route -> DB -> répéteur) plutôt que juste la fonction
|
||||
Python isolée."""
|
||||
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 _add_flow_node(client, slug, screen_id, **fields):
|
||||
resp = client.post(
|
||||
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps(fields),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
return resp.get_json()["id"]
|
||||
|
||||
|
||||
def _create_inventory_object(client, slug):
|
||||
resp = client.post(
|
||||
f"/game/{slug}/objects/new",
|
||||
data={
|
||||
"object_name": "Inventaire",
|
||||
"field_name[]": ["objet"],
|
||||
"field_type[]": ["texte"],
|
||||
"field_relation[]": [""],
|
||||
"field_required[]": ["0"],
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
return int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||
|
||||
|
||||
def test_run_add_row_route_creates_an_empty_row(client, game):
|
||||
definition_id = _create_inventory_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
node_id = _add_flow_node(
|
||||
client, game, screen_id, node_type="action", action_type="ajouter_ligne", target_definition_id=definition_id,
|
||||
)
|
||||
resp = client.post(f"/game/{game}/flow/nodes/{node_id}/run-add-row")
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()
|
||||
assert body["ok"] is True
|
||||
assert isinstance(body["row_id"], int)
|
||||
|
||||
definition = db.get_definition(game, definition_id)
|
||||
rows = db.list_rows(game, definition)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["id"] == body["row_id"]
|
||||
assert rows[0]["objet"] is None
|
||||
|
||||
|
||||
def test_run_add_row_route_rejects_wrong_action_type(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
node_id = _add_flow_node(client, game, screen_id, node_type="action", action_type="attendre", data_value="1")
|
||||
resp = client.post(f"/game/{game}/flow/nodes/{node_id}/run-add-row")
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()["ok"] is False
|
||||
|
||||
|
||||
def test_chained_ajouter_ligne_then_modifier_donnee_sets_the_new_row_fields(client, game):
|
||||
"""Le scénario réel : "Ajouter une ligne" crée une ligne vide, puis un
|
||||
"Modifier une donnée" ciblant LAST_INSERTED_ROW_ID (transmis par le
|
||||
client via last_inserted_row_id, voir static/js/play/actions.js) la
|
||||
renseigne — exactement comme CLICKED_ROW_ID pour une ligne cliquée."""
|
||||
definition_id = _create_inventory_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
add_node_id = _add_flow_node(
|
||||
client, game, screen_id, node_type="action", action_type="ajouter_ligne", target_definition_id=definition_id,
|
||||
)
|
||||
set_node_id = _add_flow_node(
|
||||
client, game, screen_id, node_type="action", action_type="modifier_donnee",
|
||||
target_definition_id=definition_id, target_row_id=screens.LAST_INSERTED_ROW_ID,
|
||||
target_field="objet", data_operation="definir_texte", data_value="épée",
|
||||
)
|
||||
|
||||
add_resp = client.post(f"/game/{game}/flow/nodes/{add_node_id}/run-add-row")
|
||||
new_row_id = add_resp.get_json()["row_id"]
|
||||
|
||||
set_resp = client.post(
|
||||
f"/game/{game}/flow/nodes/{set_node_id}/run-data",
|
||||
data=json.dumps({"last_inserted_row_id": new_row_id}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert set_resp.get_json()["ok"] is True
|
||||
|
||||
definition = db.get_definition(game, definition_id)
|
||||
row = db.get_row(game, definition, new_row_id)
|
||||
assert row["objet"] == "épée"
|
||||
|
||||
|
||||
def test_new_row_is_scoped_to_the_acting_player(client, game):
|
||||
"""Un objet per_player (par défaut) : la ligne créée appartient au
|
||||
joueur qui a agi, pas partagée avec les autres — voir db/rows/
|
||||
resolve_row_player_key.py."""
|
||||
from conftest import flask_app
|
||||
|
||||
definition_id = _create_inventory_object(client, game)
|
||||
screen_id = _create_screen(client, game)
|
||||
node_id = _add_flow_node(
|
||||
client, game, screen_id, node_type="action", action_type="ajouter_ligne", target_definition_id=definition_id,
|
||||
)
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
|
||||
alice = flask_app.test_client()
|
||||
alice.get(f"/jouer/{game}")
|
||||
resp = alice.post(f"/jouer/{game}/flow/nodes/{node_id}/run-add-row")
|
||||
assert resp.get_json()["ok"] is True
|
||||
|
||||
definition = db.get_definition(game, definition_id)
|
||||
assert db.list_rows(game, definition, player_id="__shared__") == []
|
||||
bob_rows = db.list_rows(game, definition, player_id="quelquun-dautre")
|
||||
assert bob_rows == []
|
||||
Reference in New Issue
Block a user