diff --git a/screens/elements/list_elements.py b/screens/elements/list_elements.py index 69b17a0f..9025346b 100644 --- a/screens/elements/list_elements.py +++ b/screens/elements/list_elements.py @@ -30,6 +30,36 @@ def list_elements(slug, screen_id, enforce_visibility=False): d["style"] = json.loads(d["style"] or "{}") d["attributes"] = json.loads(d["attributes"] or "{}") result.append(d) + + # Descendant d'un exemplaire d'élément de jeu (element_type_id réglé sur + # un ancêtre) : un ancien enfant copié en base par l'ex-instantiate_ + # template_tree (mécanisme retiré, voir add_element.py) — son contenu + # vient désormais TOUJOURS en direct de l'écran-modèle au moment du + # rendu (voir render_element_html.py/_render_element_type_children), + # jamais de ces lignes, qui peuvent encore traîner en base pour un + # exemplaire posé avant ce changement. Exclu ici pour ne plus jamais + # apparaître sélectionnable dans l'arborescence de l'éditeur — sinon + # ses réglages, plus jamais synchronisés avec le modèle, donnaient + # l'impression trompeuse que "les propriétés ne sont pas prises en + # compte" (une taille de texte modifiée dans le modèle ne se reflétant + # jamais sur cette ligne fantôme, toujours sélectionnable à l'écran). + by_id = {d["id"]: d for d in result} + + def _has_linked_ancestor(d): + seen = set() + parent_id = d.get("parent_id") + while parent_id and parent_id not in seen: + seen.add(parent_id) + parent = by_id.get(parent_id) + if not parent: + return False + if parent.get("element_type_id"): + return True + parent_id = parent.get("parent_id") + return False + + result = [d for d in result if not _has_linked_ancestor(d)] + children_map = {} for d in result: if d.get("parent_id"): diff --git a/tests/test_element_type_live_instances.py b/tests/test_element_type_live_instances.py index 35249f6c..f4a8f8d9 100644 --- a/tests/test_element_type_live_instances.py +++ b/tests/test_element_type_live_instances.py @@ -56,6 +56,47 @@ def test_editing_the_template_updates_the_play_page_too(client, game): assert "Contenu mis à jour" in html +def test_leftover_orphaned_children_no_longer_appear_in_the_editor(client, game): + """Régression : un exemplaire posé AVANT ce mécanisme (ou tout ancien + enfant copié en base par l'ex-instantiate_template_tree, retiré) + laisse d'éventuelles lignes orphelines en base sous l'exemplaire — leur + contenu n'est plus jamais rendu (voir render_element_html.py), mais + elles restaient sélectionnables dans l'arborescence de l'éditeur, avec + leurs propres réglages jamais synchronisés avec le modèle : une taille + de texte modifiée dans le modèle ne s'y reflétait jamais, donnant + l'impression trompeuse que "les propriétés ne sont pas prises en + compte".""" + import json + + import db + + et, _titre_id = _create_card_element_type(client, game) + screen_id = _create_screen(client, game) + resp = client.post( + f"/game/{game}/screens/{screen_id}/elements/add", + data={"widget": "__catalogue__", "element_type_id": str(et["id"])}, + follow_redirects=False, + ) + instance_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) + + conn = db.connect(game) + conn.execute( + """INSERT INTO _screen_elements (screen_id, parent_id, widget, content, attributes, x, y, width, height, z_index, style) + VALUES (?, ?, 'titre', 'Ancien contenu figé', '{}', 10, 10, 50, 20, 1, ?)""", + (screen_id, instance_id, json.dumps({"font-size": "21px"})), + ) + orphan_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"] + conn.commit() + conn.close() + + html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() + assert f'id="elt-{orphan_id}"' not in html + assert "Ancien contenu figé" not in html + + html_selected = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={orphan_id}").data.decode() + assert 'value="21' not in html_selected + + def test_instance_position_on_screen_is_independent_per_instance(client, game): """Non-régression : la position/taille sur l'écran reste propre à chaque exemplaire (seul le CONTENU intérieur est désormais partagé)."""