Ajoute "Variable globale" au sélecteur "Valeur fixe / Donnée d'un autre objet"

Le sélecteur de valeur de comparaison (tout contrôle "..._valeur" : filtre
du Répéteur, Donnée liée, condition de visibilité) proposait déjà une
valeur fixe ou le champ d'un AUTRE objet - manquait la possibilité de
comparer à une variable globale (voir db/global_vars/), qui change elle
aussi en cours de partie mais n'est rattachée à aucun objet précis.

Nouvelle syntaxe interne "{{$nom_variable}}" (le "$" la distingue sans
ambiguïté de "{{Objet.champ}}", qui attend toujours un point) :
_resolve_filter_value (filter_repeater_rows.py) va lire sa valeur actuelle
via db.get_global_variable, comme "{{Objet.champ}}" le fait déjà pour un
champ d'objet. Le panneau de propriétés gagne un troisième mode
"Variable globale" à côté de "Valeur fixe"/"Donnée d'un autre objet",
avec la liste déroulante des variables existantes.

Corrige au passage un bug latent découvert en testant bout en bout : un
Répéteur SANS modèle de ligne (texte brut avec {{champ}}) plantait en
mode jouable avec TypeError - render_repeater.py substitue lui aussi
directement les {{champ}} du ctx dans ce cas (repli), et ce ctx porte
aussi _forge_play_mode (un booléen, voir render_element_html.py) depuis
l'ajout de la condition de visibilité - déjà corrigé pour le chemin
générique (apply_ctx.py) mais pas pour ce chemin séparé.

Le sélecteur de champ pour insérer {{champ}} dans "Contenu" (demandé dans
le même message) existe déjà depuis un tour précédent (voir
insertFieldAtCursor()) - vérifié toujours fonctionnel.

Ajoute tests/test_filter_value_global_variable.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-25 16:51:18 +02:00
co-authored by Claude Sonnet 5
parent 6894c5fc95
commit 7d48445443
4 changed files with 166 additions and 15 deletions
@@ -0,0 +1,95 @@
"""Le sélecteur "Valeur fixe / Donnée d'un autre objet" (tout contrôle
"..._valeur", voir screen_edit.html) propose désormais aussi "Variable
globale" — syntaxe interne "{{$nom_variable}}" (voir _VAR_REF_PATTERN,
filter_repeater_rows.py), au même titre que "{{Objet.champ}}" pour un
objet."""
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_niveau_object(client, slug):
resp = client.post(f"/game/{slug}/objects/new", data={
"object_name": "Niveau", "field_name[]": ["numero"], "field_type[]": ["nombre_entier"],
"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={"numero": "3"})
client.post(f"/game/{slug}/objects/{def_id}/data/new", data={"numero": "5"})
return def_id
def test_repeater_filter_resolves_a_global_variable_reference(client, game):
client.post(f"/game/{game}/variables/new", data={"name": "niveau_actuel", "type": "nombre_entier", "default_value": "3"})
def_id = _create_niveau_object(client, game)
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "repeteur_donnees"}, follow_redirects=False)
rep_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{rep_id}/save", data={
"ctrl_definition_id": str(def_id),
"ctrl_filtre_champ": "numero",
"ctrl_filtre_operateur": "egal",
"ctrl_filtre_valeur": "{{$niveau_actuel}}",
})
payload = client.get(f"/game/{game}/runtime-payload").get_json()
html = payload["screens"][0]["elements"][0]["rendered_html"]
assert html.count("repeaterItem") == 1
db.update_global_variable_value(game, "niveau_actuel", "5")
payload = client.get(f"/game/{game}/runtime-payload").get_json()
html = payload["screens"][0]["elements"][0]["rendered_html"]
assert html.count("repeaterItem") == 1
def test_deleted_variable_reference_matches_nothing_rather_than_crashing(client, game):
def_id = _create_niveau_object(client, game)
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "repeteur_donnees"}, follow_redirects=False)
rep_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{rep_id}/save", data={
"ctrl_definition_id": str(def_id),
"ctrl_filtre_champ": "numero",
"ctrl_filtre_operateur": "egal",
"ctrl_filtre_valeur": "{{$inexistante}}",
})
payload = client.get(f"/game/{game}/runtime-payload").get_json()
html = payload["screens"][0]["elements"][0]["rendered_html"]
assert html.count("repeaterItem") == 0
def test_variable_picker_appears_in_the_properties_panel(client, game):
client.post(f"/game/{game}/variables/new", data={"name": "niveau_actuel", "type": "nombre_entier", "default_value": "3"})
def_id = _create_niveau_object(client, game)
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "repeteur_donnees"}, follow_redirects=False)
rep_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{rep_id}/save", data={"ctrl_definition_id": str(def_id)})
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={rep_id}").data.decode()
assert "filterValueVarSel" in html
assert '<option value="niveau_actuel">niveau_actuel</option>' in html
def test_repeater_without_template_still_renders_in_play_mode(client, game):
"""Non-régression : un Répéteur sans modèle de ligne (texte brut avec
{{champ}}) plantait en mode jouable avec TypeError: replace() argument
2 must be str, not bool - le ctx porte aussi _forge_play_mode (voir
render_element_html.py), un booléen jamais destiné à une substitution
{{champ}}."""
def_id = _create_niveau_object(client, game)
screen_id = _create_screen(client, game)
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add", data={"widget": "repeteur_donnees"}, follow_redirects=False)
rep_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
client.post(f"/game/{game}/elements/{rep_id}/save", data={"ctrl_definition_id": str(def_id), "ctrl_content": "Numéro : {{numero}}"})
resp = client.get(f"/game/{game}/play")
assert resp.status_code == 200
assert b"Num\xc3\xa9ro : 3" in resp.data