Le sélecteur de champ apparaît maintenant sur tout nouveau widget d'un élément de jeu lié à un objet
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7e80ff952a
commit
9c9e3c5379
@@ -22,7 +22,12 @@ def screen_edit(slug, screen_id):
|
|||||||
if selected_id:
|
if selected_id:
|
||||||
selected_el = next((e for e in elements if e["id"] == selected_id), None)
|
selected_el = next((e for e in elements if e["id"] == selected_id), None)
|
||||||
if selected_el:
|
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 = {}
|
children_of = {}
|
||||||
for el in elements:
|
for el in elements:
|
||||||
if el.get("parent_id"):
|
if el.get("parent_id"):
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ from .elements.rename_element import rename_element
|
|||||||
from .elements.element_label import element_label
|
from .elements.element_label import element_label
|
||||||
|
|
||||||
from .element_types.list_element_types import list_element_types
|
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.create_element_type import create_element_type
|
||||||
from .element_types.update_element_type import update_element_type
|
from .element_types.update_element_type import update_element_type
|
||||||
from .element_types.element_type_usage_count import element_type_usage_count
|
from .element_types.element_type_usage_count import element_type_usage_count
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -15,11 +15,21 @@ def _field_label(f):
|
|||||||
return f["name"]
|
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
|
"""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
|
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
|
"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"))
|
meta = widget_meta(el.get("widget"))
|
||||||
attributes = el.get("attributes") or {}
|
attributes = el.get("attributes") or {}
|
||||||
# Pour la Jauge : quel enregistrement suivre (row_id) et quels champs
|
# 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
|
# en même temps, mais un calcul dédié quand même pour rester correct si
|
||||||
# ça change un jour.
|
# ça change un jour.
|
||||||
data_binding_options = data_definition_options(slug, attributes.get("_data_definition_id"))
|
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
|
# Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) : elle
|
||||||
# aussi garde SON objet dans un attribut séparé (_visibilite_cond_
|
# 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.
|
# 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"]]
|
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"):
|
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"]]
|
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
|
# Une fois "Lier à un objet de données" réglé (voir
|
||||||
# DATA_BINDING_CONTROLS), propose les champs de CET objet en
|
# DATA_BINDING_CONTROLS), propose les champs de CET objet en
|
||||||
# liste déroulante à côté du texte — plutôt que de devoir taper
|
# liste déroulante à côté du texte — plutôt que de devoir taper
|
||||||
# "{{nom_du_champ}}" à la main sans savoir quels noms existent
|
# "{{nom_du_champ}}" à la main sans savoir quels noms existent
|
||||||
# (voir insertFieldAtCursor() dans screen_edit.html).
|
# (voir insertFieldAtCursor() dans screen_edit.html). À défaut,
|
||||||
control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
|
# 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)})
|
result.append({**control, "value": _control_value(el, control)})
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -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 '<option value="sujet">' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_picker_absent_when_element_type_has_no_bound_object(client, game):
|
||||||
|
resp = client.post(f"/game/{game}/element-types", data={"name": "Carte simple", "icon": "🃏"}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
et = next(t for t in screens.list_element_types(game) if t["name"] == "Carte simple")
|
||||||
|
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"' not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_widget_own_data_binding_still_takes_priority_over_element_type_bound_object(client, game):
|
||||||
|
"""Non-régression : si le widget a SA PROPRE "Donnée liée" réglée (sur
|
||||||
|
un objet différent de celui de l'élément de jeu), c'est elle qui doit
|
||||||
|
fournir les champs du sélecteur, pas l'objet lié de l'élément de jeu."""
|
||||||
|
email_id = _create_email_object(client, game)
|
||||||
|
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)
|
||||||
|
partie_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
||||||
|
|
||||||
|
resp = client.post(f"/game/{game}/element-types", data={
|
||||||
|
"name": "Mail card 2", "icon": "📧", "bound_definition_id": str(email_id),
|
||||||
|
}, follow_redirects=False)
|
||||||
|
et = next(t for t in screens.list_element_types(game) if t["name"] == "Mail card 2")
|
||||||
|
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))
|
||||||
|
client.post(f"/game/{game}/elements/{element_id}/save", data={"ctrl_data_definition_id": str(partie_id)})
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{template_screen_id}/edit?selected={element_id}").data.decode()
|
||||||
|
assert '<option value="score">' in html
|
||||||
|
assert '<option value="sujet">' not in html
|
||||||
Reference in New Issue
Block a user