Phase 4 : ajout dynamique d'une ligne à l'exécution
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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:
william
2026-08-30 17:18:10 +02:00
co-authored by Claude Sonnet 5
parent 727c3c97ba
commit 92fbfbc9dd
19 changed files with 287 additions and 11 deletions
+1
View File
@@ -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",
}
+1
View File
@@ -6,5 +6,6 @@ from . import (
flow_edge_delete,
flow_node_run_data,
flow_node_run_variable,
flow_node_run_add_row,
runtime_payload,
)
+21
View File
@@ -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})
+8
View File
@@ -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})
+1
View File
@@ -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),
)
+7 -1
View File
@@ -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:
+1
View File
@@ -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:
+5
View File
@@ -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
View File
@@ -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)
+11
View File
@@ -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"),
+1
View File
@@ -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",
}
+18 -4
View File
@@ -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();
+18
View File
@@ -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
View File
@@ -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>
+14
View File
@@ -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 }};
+123
View File
@@ -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 == []