Deux fonctionnalités demandées, développées et corrigées dans cet
échange :
1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
screens/clause_list_codec.py). Rétrocompatible avec les anciens
éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
volée à la lecture, sans migration. Après un premier essai à la
présentation trop compacte et technique (retour utilisateur : "pas de
champ technique, pas de notation bizarre {{ }}"), la présentation
finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
"...cette valeur", même sélecteur de valeur fixe/dynamique/variable
déjà existant, jamais la syntaxe brute), simplement répétée par
condition (templates/partials/clause_row.html), avec un bouton
"+ Ajouter une condition" bien visible et une liste scrollable
(static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
rows.py généralisé) profite aussi au Répéteur de données en interne.
2. Nœud Condition de la Logique de la scène : peut désormais tester une
VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
CLIENT (templates/play.html, evaluateConditionClause), contre un
nouveau gameData.variables exposé par full_game_payload.py — tenu à
jour par refreshRuntimeData() après toute action qui modifie une
variable, sans changement supplémentaire nécessaire. Le panneau de
condition reste utilisable même sans aucun objet défini dans le jeu
(avant, il disparaissait entièrement).
Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
6.6 KiB
Python
100 lines
6.6 KiB
Python
import db
|
|
|
|
from .widget_meta import widget_meta
|
|
from .control_value import _control_value
|
|
from .data_definition_options import data_definition_options
|
|
from ..element_types.list_element_types import list_element_types
|
|
|
|
|
|
def _field_label(f):
|
|
"""Libellé affiché dans une liste déroulante de champs — précise l'objet
|
|
visé pour un champ "relation" (ex. "parcour (→ parcours)"), pour qu'on
|
|
sache ce qu'on choisit sans avoir à aller vérifier dans l'objet."""
|
|
if f["type"] == "relation" and f.get("relation_definition_name"):
|
|
return f"{f['name']} (→ {f['relation_definition_name']})"
|
|
return f["name"]
|
|
|
|
|
|
def controls_with_values(slug, el, template_definition_id=None):
|
|
"""Pour le panneau de propriétés : la liste des réglages du widget de
|
|
cet élément, chacun avec sa valeur actuelle déjà résolue. Le réglage
|
|
"définition_id" du Répéteur de données (et de la Jauge) a besoin du jeu
|
|
courant pour lister les objets disponibles — d'où le paramètre slug.
|
|
|
|
`template_definition_id` : l'objet lié à l'ÉLÉMENT DE JEU en cours
|
|
d'édition (bound_definition_id, réglé à sa création — voir
|
|
element_types.html), passé par screen_edit.py seulement quand on
|
|
édite un écran-modèle qui en a un. Sert de repli pour le champ
|
|
"Contenu" (voir plus bas) : si CE widget n'a pas sa propre "Donnée
|
|
liée" réglée, ses {{champ}} font quand même référence à l'objet lié
|
|
de l'élément de jeu (c'est lui qui fournira les valeurs une fois posé
|
|
dans un Répéteur), donc le sélecteur de champ doit lister SES champs
|
|
par défaut plutôt que de rester vide."""
|
|
meta = widget_meta(el.get("widget"))
|
|
attributes = el.get("attributes") or {}
|
|
# Pour la Jauge : quel enregistrement suivre (row_id) et quels champs
|
|
# afficher (champ/champ_nom) ne peuvent être proposés qu'une fois un
|
|
# objet choisi (definition_id, déjà sur CET élément) — voir
|
|
# render_jauge.py. La mise à jour EN DIRECT quand on change d'objet
|
|
# sans quitter le panneau est gérée côté client (onJaugeDefinitionChange
|
|
# dans screen_edit.html), ceci ne couvre que le premier affichage/une
|
|
# navigation vers un élément déjà réglé.
|
|
jauge_options = data_definition_options(slug, attributes.get("_definition_id"))
|
|
# Le Texte/Titre lié à une donnée (voir DATA_BINDING_CONTROLS) garde SON
|
|
# objet dans un attribut séparé (_data_definition_id) — jamais le même
|
|
# élément que le Répéteur/la Jauge, donc jamais besoin des deux options
|
|
# en même temps, mais un calcul dédié quand même pour rester correct si
|
|
# ça change un jour.
|
|
data_binding_options = data_definition_options(slug, attributes.get("_data_definition_id"))
|
|
# Repli pour "content" (voir plus bas) quand ce widget n'a pas sa
|
|
# propre "Donnée liée" : les champs de l'objet lié à l'élément de jeu.
|
|
template_bound_options = data_definition_options(slug, template_definition_id) if template_definition_id else None
|
|
# Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) : elle
|
|
# aussi garde SON objet dans un attribut séparé (_visibilite_cond_
|
|
# definition_id), jamais partagé avec le Répéteur/la Jauge/Donnée liée.
|
|
visibility_cond_options = data_definition_options(slug, attributes.get("_visibilite_cond_definition_id"))
|
|
global_variables = db.list_global_variables(slug)
|
|
result = []
|
|
for control in meta.get("controls", []):
|
|
control = dict(control)
|
|
if control["key"] in ("definition_id", "data_definition_id", "visibilite_cond_definition_id"):
|
|
defs = db.list_definitions(slug)
|
|
control["options"] = [("", "(sélectionner un objet)")] + [(str(d["id"]), d["name"]) for d in defs]
|
|
if control["key"] == "visibilite_cond_champ":
|
|
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in visibility_cond_options["fields"]]
|
|
if control["key"] == "visibilite_cond_variable":
|
|
control["options"] = [("", "(sélectionner une variable)")] + [(v["name"], v["name"]) for v in global_variables]
|
|
if control["key"] == "template_type_id":
|
|
types = list_element_types(slug)
|
|
control["options"] = [("", "(aucun — utiliser le modèle de texte ci-dessous)")] + [(str(t["id"]), t["name"]) for t in types]
|
|
if control["key"] == "row_id":
|
|
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(str(r["id"]), r["label"]) for r in jauge_options["rows"]]
|
|
if control["key"] in ("champ", "champ_nom"):
|
|
# La Jauge affiche une VALEUR (champ) ou un NOM (champ_nom) tel
|
|
# quel — un champ "relation" n'y a pas de sens (c'est un id, pas
|
|
# une valeur à afficher), donc exclu ici uniquement.
|
|
non_relation_fields = [f for f in jauge_options["fields"] if f["type"] != "relation"]
|
|
empty_label = "— aucun —" if control["key"] == "champ_nom" else "(sélectionner un objet d'abord)"
|
|
control["options"] = [("", empty_label)] + [(f["name"], _field_label(f)) for f in non_relation_fields]
|
|
if control["key"] in ("filtre_champ", "filtre2_champ"):
|
|
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in jauge_options["fields"]]
|
|
if control["type"] == "clause_list":
|
|
# Options du sélecteur "champ" de CHAQUE ligne de condition —
|
|
# les mêmes champs que data_filtre_champ ci-dessus (même objet
|
|
# lié, attr:_data_definition_id), exposées une seule fois ici
|
|
# plutôt que dupliquées par ligne (nombre de lignes variable).
|
|
control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
|
|
if control["key"] == "content":
|
|
# Une fois "Lier à un objet de données" réglé (voir
|
|
# DATA_BINDING_CONTROLS), propose les champs de CET objet en
|
|
# liste déroulante à côté du texte — plutôt que de devoir taper
|
|
# "{{nom_du_champ}}" à la main sans savoir quels noms existent
|
|
# (voir insertFieldAtCursor() dans screen_edit.html). À défaut,
|
|
# retombe sur l'objet lié à l'élément de jeu (voir docstring).
|
|
if attributes.get("_data_definition_id"):
|
|
control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
|
|
elif template_bound_options:
|
|
control["field_options"] = [(f["name"], _field_label(f)) for f in template_bound_options["fields"]]
|
|
result.append({**control, "value": _control_value(el, control)})
|
|
return result
|