Dev #1

Merged
w-vandal merged 62 commits from dev into main 2026-08-26 04:56:44 +00:00
5 changed files with 121 additions and 6 deletions
Showing only changes of commit 9c9e3c5379 - Show all commits
+6 -1
View File
@@ -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"):
+1
View File
@@ -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
@@ -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
+22 -5
View File
@@ -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
@@ -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