From 9c9e3c537938e2227b4f04faada372a94c625afd Mon Sep 17 00:00:00 2001 From: william Date: Tue, 25 Aug 2026 13:38:10 +0200 Subject: [PATCH] =?UTF-8?q?Le=20s=C3=A9lecteur=20de=20champ=20appara=C3=AE?= =?UTF-8?q?t=20maintenant=20sur=20tout=20nouveau=20widget=20d'un=20=C3=A9l?= =?UTF-8?q?=C3=A9ment=20de=20jeu=20li=C3=A9=20=C3=A0=20un=20objet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le tour précédent exigeait que CHAQUE widget règle sa propre "Donnée liée" (_data_definition_id) pour voir apparaître le sélecteur de champ - mais un élément de jeu ("Mail card"...) créé avec un "Objet lié" (voir element_types.html, bound_definition_id) a précisément pour but d'éviter ce réglage widget par widget : ses {{champ}} sont censés venir de CET objet, fourni plus tard par le Répéteur qui l'utilisera comme modèle de ligne. D'où le bug remonté : un nouveau Titre/Texte posé dans un tel élément de jeu n'affichait jamais le sélecteur. Ajoute get_element_type_by_template_screen(slug, screen_id), pour retrouver depuis l'éditeur d'un écran-modèle l'entrée du catalogue (et donc l'objet lié) dont il est la recette. screen_edit.py le calcule pour le panneau de propriétés et le passe à controls_with_values(), qui l'utilise comme repli pour le champ "Contenu" SEULEMENT si ce widget n'a pas déjà sa propre "Donnée liée" réglée (priorité conservée au réglage le plus spécifique). Ajoute tests/test_element_type_bound_field_picker.py (apparition sans réglage supplémentaire, absence sans objet lié, priorité à la "Donnée liée" du widget si réglée). Co-Authored-By: Claude Sonnet 5 --- routes/screens/screen_edit.py | 7 +- screens/__init__.py | 1 + .../get_element_type_by_template_screen.py | 16 ++++ screens/widgets/controls_with_values.py | 27 +++++-- tests/test_element_type_bound_field_picker.py | 76 +++++++++++++++++++ 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 screens/element_types/get_element_type_by_template_screen.py create mode 100644 tests/test_element_type_bound_field_picker.py diff --git a/routes/screens/screen_edit.py b/routes/screens/screen_edit.py index e4bbb426..886b4793 100644 --- a/routes/screens/screen_edit.py +++ b/routes/screens/screen_edit.py @@ -22,7 +22,12 @@ def screen_edit(slug, screen_id): if selected_id: selected_el = next((e for e in elements if e["id"] == selected_id), None) if selected_el: - selected_controls = screens.controls_with_values(slug, selected_el) + template_definition_id = None + if screen.get("is_template"): + owning_type = screens.get_element_type_by_template_screen(slug, screen_id) + if owning_type: + template_definition_id = owning_type.get("bound_definition_id") + selected_controls = screens.controls_with_values(slug, selected_el, template_definition_id) children_of = {} for el in elements: if el.get("parent_id"): diff --git a/screens/__init__.py b/screens/__init__.py index 888a4205..e21479e0 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -85,6 +85,7 @@ from .elements.rename_element import rename_element from .elements.element_label import element_label from .element_types.list_element_types import list_element_types +from .element_types.get_element_type_by_template_screen import get_element_type_by_template_screen from .element_types.create_element_type import create_element_type from .element_types.update_element_type import update_element_type from .element_types.element_type_usage_count import element_type_usage_count diff --git a/screens/element_types/get_element_type_by_template_screen.py b/screens/element_types/get_element_type_by_template_screen.py new file mode 100644 index 00000000..1533cced --- /dev/null +++ b/screens/element_types/get_element_type_by_template_screen.py @@ -0,0 +1,16 @@ +import db + +from ..screens_repo.ensure_schema import ensure_schema + + +def get_element_type_by_template_screen(slug, screen_id): + """L'entrée du catalogue (voir _element_types) dont l'écran-modèle EST + cet écran, s'il y en a une — utile pour retrouver, depuis l'éditeur + d'un écran-modèle, l'objet éventuellement lié à cet élément de jeu + (bound_definition_id, réglé à la création — voir element_types.html) + sans devoir tout lister soi-même.""" + ensure_schema(slug) + conn = db.connect(slug) + row = conn.execute("SELECT * FROM _element_types WHERE template_screen_id = ?", (screen_id,)).fetchone() + conn.close() + return dict(row) if row else None diff --git a/screens/widgets/controls_with_values.py b/screens/widgets/controls_with_values.py index f94e4dfc..94ef0093 100644 --- a/screens/widgets/controls_with_values.py +++ b/screens/widgets/controls_with_values.py @@ -15,11 +15,21 @@ def _field_label(f): return f["name"] -def controls_with_values(slug, el): +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.""" + 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 @@ -36,6 +46,9 @@ def controls_with_values(slug, el): # 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. @@ -67,12 +80,16 @@ def controls_with_values(slug, el): control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in jauge_options["fields"]] if control["key"] in ("data_filtre_champ", "data_filtre2_champ"): control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in data_binding_options["fields"]] - if control["key"] == "content" and attributes.get("_data_definition_id"): + 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). - control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]] + # (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 diff --git a/tests/test_element_type_bound_field_picker.py b/tests/test_element_type_bound_field_picker.py new file mode 100644 index 00000000..a1870d9a --- /dev/null +++ b/tests/test_element_type_bound_field_picker.py @@ -0,0 +1,76 @@ +"""Sélecteur de champ du panneau de propriétés ("+ Ajouter" à côté de +"Contenu", voir insertFieldAtCursor() dans screen_edit.html) : doit +apparaître dès qu'un objet est disponible dans le contexte, pas seulement +quand le widget a lui-même une "Donnée liée" réglée.""" +import re + +import screens + + +def _create_email_object(client, slug): + resp = client.post(f"/game/{slug}/objects/new", data={ + "object_name": "Email", "field_name[]": ["sujet"], "field_type[]": ["texte"], + "field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""], + }, follow_redirects=False) + return int(resp.headers["Location"].rstrip("/").split("/")[-1]) + + +def test_new_widget_inside_a_bound_element_type_gets_the_field_picker_without_extra_setup(client, game): + """Régression : une "Mail card" (élément de jeu créé avec "Objet lié" = + Email) doit proposer le sélecteur de champ sur tout nouveau Titre/Texte + posé dans son écran-modèle, sans qu'il faille en plus régler "Lier à un + objet de données" sur CE widget précis — l'objet lié à l'élément de jeu + suffit.""" + def_id = _create_email_object(client, game) + resp = client.post(f"/game/{game}/element-types", data={ + "name": "Mail card", "icon": "📧", "bound_definition_id": str(def_id), + }, follow_redirects=False) + assert resp.status_code == 302 + et = next(t for t in screens.list_element_types(game) if t["name"] == "Mail card") + template_screen_id = et["template_screen_id"] + + resp = client.post(f"/game/{game}/screens/{template_screen_id}/elements/add", data={"widget": "titre"}, follow_redirects=False) + element_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) + + html = client.get(f"/game/{game}/screens/{template_screen_id}/edit?selected={element_id}").data.decode() + assert 'id="fieldPicker-content"' in html + assert '