Donnée liée : conditions ET/OU illimitées + conditions de logique sur une variable globale

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>
This commit is contained in:
william
2026-08-28 09:05:02 +02:00
co-authored by Claude Sonnet 5
parent 7c237d6f1c
commit 8cffbeac68
21 changed files with 971 additions and 93 deletions
+45
View File
@@ -0,0 +1,45 @@
import json
# Partagé par control_value.py (widgets) et filter_repeater_rows.py
# (rendering) — module "feuille" sans import de l'un ou l'autre, pour ne
# jamais introduire de dépendance circulaire entre ces deux packages.
def decode_clauses(attrs, clauses_attr, combinator_attr, legacy_prefix=None):
"""Lit une liste de clauses {"champ", "operateur", "valeur"} depuis un
attribut JSON (clauses_attr). Repli sur l'ANCIEN format à 2 filtres
fixes (legacy_prefix + "_champ"/"_operateur"/"_valeur", et
legacy_prefix + "2_champ"/"2_operateur"/"2_valeur") si clauses_attr est
absent — pour ne rien casser sur les éléments créés avant l'ajout des
clauses illimitées (aucune migration de données nécessaire, la
conversion se fait à la volée, ici, à la lecture). Renvoie toujours
(clauses, combinateur), combinateur valant "et" par défaut."""
raw = attrs.get(clauses_attr)
clauses = None
if raw:
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
clauses = parsed
except (TypeError, ValueError):
clauses = None
if clauses is None and legacy_prefix:
clauses = []
for suffix in ("", "2"):
champ = (attrs.get(f"{legacy_prefix}{suffix}_champ") or "").strip()
if champ:
clauses.append({
"champ": champ,
"operateur": attrs.get(f"{legacy_prefix}{suffix}_operateur") or "egal",
"valeur": attrs.get(f"{legacy_prefix}{suffix}_valeur") or "",
})
combinateur = attrs.get(combinator_attr) or "et"
return clauses or [], combinateur
def encode_clauses(clauses):
"""Sérialise une liste de clauses en JSON, en retirant celles sans nom
de champ (une ligne ajoutée puis jamais remplie) — "" si la liste
nettoyée est vide, pour ne rien écrire de superflu en base."""
cleaned = [c for c in clauses if (c.get("champ") or "").strip()]
return json.dumps(cleaned) if cleaned else ""