Dev #1
@@ -47,7 +47,7 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
||||
# ce cas d'office.
|
||||
has_visibility_condition = attrs_raw.get("_visibilite_cond_mode") in ("variable", "objet")
|
||||
play_mode = bool((ctx or {}).get("_forge_play_mode"))
|
||||
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw):
|
||||
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw, ctx):
|
||||
return f'<div class="visibilityGated" data-element-id="{el["id"]}" style="display:none;"></div>'
|
||||
|
||||
def _mark(html_str):
|
||||
|
||||
@@ -3,17 +3,29 @@ import db
|
||||
from .filter_repeater_rows import _resolve_filter_value, _compare, _field_column
|
||||
|
||||
|
||||
def is_element_visible(slug, attrs):
|
||||
def is_element_visible(slug, attrs, ctx=None):
|
||||
"""Condition de visibilité (panneau de propriétés, tout widget — voir
|
||||
VISIBILITY_CONDITION_CONTROLS) : masque un élément selon soit une
|
||||
VARIABLE GLOBALE (une seule valeur pour tout le jeu, voir
|
||||
db/global_vars/), soit le champ d'un objet de données (convention "1
|
||||
seule ligne = état de partie", déjà utilisée par la Jauge/Donnée liée).
|
||||
Absente de réglage (_visibilite_cond_mode vide) => toujours visible,
|
||||
comportement inchangé pour tous les éléments déjà créés. Réutilise
|
||||
db/global_vars/), soit le champ d'un objet de données. Absente de
|
||||
réglage (_visibilite_cond_mode vide) => toujours visible, comportement
|
||||
inchangé pour tous les éléments déjà créés. Réutilise
|
||||
_compare/_resolve_filter_value/_field_column de filter_repeater_rows.py
|
||||
pour rester cohérent avec les mêmes opérateurs et la même syntaxe
|
||||
"{{Objet.champ}}" pour une valeur de comparaison dynamique."""
|
||||
"{{Objet.champ}}" pour une valeur de comparaison dynamique.
|
||||
|
||||
`ctx` porte les {{champ}} de la ligne en cours (voir render_repeater.py)
|
||||
quand cet élément est descendant d'un Répéteur de données — en mode
|
||||
"objet", si le champ réglé s'y trouve déjà, sa valeur est utilisée telle
|
||||
quelle plutôt que d'aller chercher la ligne la plus récente de l'objet
|
||||
en base : sans ça, TOUS les exemplaires d'un même modèle de ligne (ex.
|
||||
une "Mail card" répétée pour chaque e-mail) évalueraient la même ligne
|
||||
(la plus récente de l'objet) au lieu de chacun la sienne, et
|
||||
n'afficheraient donc jamais rien de cohérent. Hors Répéteur (mode
|
||||
"objet" sur un élément posé directement sur l'écran), le champ n'est
|
||||
pas dans ctx et on retombe sur la convention "1 seule ligne = état de
|
||||
partie" (déjà utilisée par la Jauge/Donnée liée) : la ligne la plus
|
||||
récente de l'objet choisi."""
|
||||
mode = attrs.get("_visibilite_cond_mode")
|
||||
if mode not in ("variable", "objet"):
|
||||
return True
|
||||
@@ -36,9 +48,13 @@ def is_element_visible(slug, attrs):
|
||||
definition = db.get_definition(slug, int(definition_id))
|
||||
if not definition:
|
||||
return True
|
||||
field_type = next((f["type"] for f in definition["fields"] if f["name"] == field_name), "texte")
|
||||
|
||||
if ctx and field_name in ctx:
|
||||
return _compare(ctx.get(field_name), operator, expected, field_type)
|
||||
|
||||
rows = db.list_rows(slug, definition)
|
||||
if not rows:
|
||||
return True # objet sans donnée pour l'instant : ne bloque pas
|
||||
row = rows[0] # convention "état de partie" : une seule ligne, voir data_definition_options.py
|
||||
field_type = next((f["type"] for f in definition["fields"] if f["name"] == field_name), "texte")
|
||||
return _compare(row.get(_field_column(definition, field_name)), operator, expected, field_type)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Tests de la "condition de visibilité" (panneau de propriétés, tout
|
||||
widget) : masque un élément en mode jouable selon une variable globale ou
|
||||
le champ d'un objet de données. Jamais appliquée dans l'éditeur."""
|
||||
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_element(client, slug, screen_id, widget="texte"):
|
||||
resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False)
|
||||
return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||
|
||||
|
||||
def _rendered_html(client, slug):
|
||||
payload = client.get(f"/game/{slug}/runtime-payload").get_json()
|
||||
return payload["screens"][0]["elements"][0]["rendered_html"]
|
||||
|
||||
|
||||
def test_variable_mode_hides_and_shows_element(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
element_id = _add_element(client, game, screen_id)
|
||||
client.post(f"/game/{game}/variables/new", data={"name": "porte_ouverte", "type": "booleen", "default_value": "0"})
|
||||
client.post(f"/game/{game}/elements/{element_id}/save", data={
|
||||
"ctrl_visibilite_cond_mode": "variable",
|
||||
"ctrl_visibilite_cond_variable": "porte_ouverte",
|
||||
"ctrl_visibilite_cond_operateur": "egal",
|
||||
"ctrl_visibilite_cond_valeur": "1",
|
||||
})
|
||||
|
||||
html = _rendered_html(client, game)
|
||||
assert "display:none" in html
|
||||
|
||||
db.update_global_variable_value(game, "porte_ouverte", "1")
|
||||
html = _rendered_html(client, game)
|
||||
assert "display:none" not in html
|
||||
|
||||
|
||||
def test_visibility_condition_never_hides_in_the_editor(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
element_id = _add_element(client, game, screen_id)
|
||||
client.post(f"/game/{game}/variables/new", data={"name": "porte_ouverte", "type": "booleen", "default_value": "0"})
|
||||
client.post(f"/game/{game}/elements/{element_id}/save", data={
|
||||
"ctrl_visibilite_cond_mode": "variable",
|
||||
"ctrl_visibilite_cond_variable": "porte_ouverte",
|
||||
"ctrl_visibilite_cond_operateur": "egal",
|
||||
"ctrl_visibilite_cond_valeur": "1",
|
||||
})
|
||||
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||
assert 'class="visibilityGated"' not in html
|
||||
assert f'id="el-{element_id}"' in html
|
||||
|
||||
|
||||
def test_objet_mode_uses_most_recent_row_outside_a_repeater(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
element_id = _add_element(client, game, screen_id)
|
||||
resp = client.post(f"/game/{game}/objects/new", data={
|
||||
"object_name": "Partie", "field_name[]": ["score"], "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/{game}/objects/{def_id}/data/new", data={"score": "3"})
|
||||
|
||||
client.post(f"/game/{game}/elements/{element_id}/save", data={
|
||||
"ctrl_visibilite_cond_mode": "objet",
|
||||
"ctrl_visibilite_cond_definition_id": str(def_id),
|
||||
"ctrl_visibilite_cond_champ": "score",
|
||||
"ctrl_visibilite_cond_operateur": "superieur",
|
||||
"ctrl_visibilite_cond_valeur": "10",
|
||||
})
|
||||
assert "display:none" in _rendered_html(client, game)
|
||||
|
||||
client.post(f"/game/{game}/objects/{def_id}/data/1/edit", data={"score": "20"})
|
||||
assert "display:none" not in _rendered_html(client, game)
|
||||
|
||||
|
||||
def test_objet_mode_inside_a_repeater_evaluates_each_row_independently(client, game):
|
||||
"""Régression : une "Mail card" (élément de jeu réutilisable posé dans
|
||||
un Répéteur d'e-mails) doit afficher l'icône "ouvert"/"fermé" qui
|
||||
correspond à SA PROPRE ligne — pas toujours la même ligne (la plus
|
||||
récente de l'objet) pour tous les exemplaires du Répéteur."""
|
||||
resp = client.post(f"/game/{game}/objects/new", data={
|
||||
"object_name": "Email", "field_name[]": ["sujet", "is_opened"], "field_type[]": ["texte", "booleen"],
|
||||
"field_relation[]": ["", ""], "field_required[]": ["0", "0"], "field_min[]": ["", ""], "field_max[]": ["", ""],
|
||||
}, follow_redirects=False)
|
||||
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"sujet": "Fermé"})
|
||||
client.post(f"/game/{game}/objects/{def_id}/data/new", data={"sujet": "Ouvert", "is_opened": "on"})
|
||||
|
||||
resp = client.post(f"/game/{game}/element-types", data={"name": "Mail card", "icon": "📧"}, follow_redirects=False)
|
||||
et = next(t for t in screens.list_element_types(game) if t["name"] == "Mail card")
|
||||
template_screen_id = et["template_screen_id"]
|
||||
|
||||
closed_icon_id = _add_element(client, game, template_screen_id, "icone")
|
||||
client.post(f"/game/{game}/elements/{closed_icon_id}/save", data={
|
||||
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
|
||||
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "0",
|
||||
})
|
||||
open_icon_id = _add_element(client, game, template_screen_id, "icone")
|
||||
client.post(f"/game/{game}/elements/{open_icon_id}/save", data={
|
||||
"ctrl_visibilite_cond_mode": "objet", "ctrl_visibilite_cond_definition_id": str(def_id),
|
||||
"ctrl_visibilite_cond_champ": "is_opened", "ctrl_visibilite_cond_operateur": "egal", "ctrl_visibilite_cond_valeur": "1",
|
||||
})
|
||||
|
||||
screen_id = _create_screen(client, game, "Boîte de réception")
|
||||
repeater_id = _add_element(client, game, screen_id, "repeteur_donnees")
|
||||
client.post(f"/game/{game}/elements/{repeater_id}/save", data={
|
||||
"ctrl_definition_id": str(def_id), "ctrl_template_type_id": str(et["id"]),
|
||||
})
|
||||
|
||||
html = _rendered_html(client, game)
|
||||
items = html.split('class="repeaterItem"')
|
||||
assert len(items) == 3
|
||||
row_closed, row_open = items[1], items[2]
|
||||
assert row_closed.count('class="visibilityGated"') == 1
|
||||
assert row_open.count('class="visibilityGated"') == 1
|
||||
assert row_closed != row_open
|
||||
Reference in New Issue
Block a user