Corrige un plantage à la suppression d'un élément référencé par la Logique de la scène
Bug remonté : supprimer un élément plantait avec sqlite3.IntegrityError: FOREIGN KEY constraint failed. Cause : trigger_element_id/target_element_id (_flow_nodes, voir ensure_flow_schema.py - un nœud "clic sur cet élément" ou "Modifier cet élément"/"Activer cet onglet") et target_element_id (l'ancien système _actions, conservé pour compatibilité) référencent _screen_elements(id) SANS ON DELETE CASCADE - volontairement, un élément ne doit pas pouvoir disparaître "par erreur" en cascade depuis un nœud de logique qu'on modifie ailleurs. Mais ça veut dire que delete_element.py, qui ne supprimait jusqu'ici que la ligne elle-même, faisait échouer PRAGMA foreign_keys=ON (db/connection.py) dès qu'un nœud de logique existant référençait encore l'élément. Fix : delete_element.py nettoie maintenant ces références AVANT de supprimer l'élément - pas seulement pour l'élément explicitement supprimé, mais pour tous ses DESCENDANTS aussi (leur suppression est cascadée automatiquement au niveau SQL via parent_id, sans repasser par ce fichier, donc sans ce nettoyage si on ne le fait pas explicitement). Ajoute tests/test_delete_element_referenced_by_flow.py (élément référencé comme déclencheur, comme cible d'action, et cas d'un conteneur supprimé dont un descendant est référencé). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4dc42a7f3b
commit
1e86077e45
@@ -0,0 +1,79 @@
|
||||
"""Régression : supprimer un élément référencé par un nœud de la Logique
|
||||
de la scène (comme déclencheur "clic sur cet élément", ou comme cible
|
||||
d'une action "Modifier cet élément"/"Activer cet onglet") plantait avec
|
||||
sqlite3.IntegrityError: FOREIGN KEY constraint failed — trigger_element_id
|
||||
et target_element_id (voir screens/flow/ensure_flow_schema.py) n'ont pas
|
||||
de ON DELETE CASCADE."""
|
||||
import json
|
||||
import re
|
||||
|
||||
import screens
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||
|
||||
|
||||
def _add_element(client, slug, screen_id, widget="conteneur"):
|
||||
resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False)
|
||||
return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||
|
||||
|
||||
def test_deleting_an_element_used_as_a_flow_trigger_does_not_crash(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
element_id = _add_element(client, game, screen_id, "bouton")
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({"node_type": "trigger", "trigger_event": "clic", "trigger_element_id": element_id}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = client.post(f"/game/{game}/elements/{element_id}/delete", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
assert screens.get_element(game, element_id) is None
|
||||
|
||||
|
||||
def test_deleting_an_element_used_as_a_flow_action_target_does_not_crash(client, game):
|
||||
screen_id = _create_screen(client, game)
|
||||
other_element_id = _add_element(client, game, screen_id, "bouton")
|
||||
target_element_id = _add_element(client, game, screen_id, "texte")
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({
|
||||
"node_type": "action", "action_type": "modifier_element", "target_element_id": target_element_id,
|
||||
"element_property": "visibilite", "element_value": "cache",
|
||||
}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = client.post(f"/game/{game}/elements/{target_element_id}/delete", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
assert screens.get_element(game, target_element_id) is None
|
||||
# L'autre élément (non concerné) reste intact.
|
||||
assert screens.get_element(game, other_element_id) is not None
|
||||
|
||||
|
||||
def test_deleting_a_container_cleans_up_flow_references_to_its_descendants(client, game):
|
||||
"""Un conteneur supprimé cascade la suppression de ses enfants au
|
||||
niveau SQL (parent_id ON DELETE CASCADE) - sans passer par
|
||||
delete_element.py pour chacun d'eux. Le nettoyage doit donc couvrir
|
||||
aussi les DESCENDANTS, pas seulement l'élément explicitement supprimé."""
|
||||
screen_id = _create_screen(client, game)
|
||||
container_id = _add_element(client, game, screen_id, "conteneur")
|
||||
resp = client.post(f"/game/{game}/elements/{container_id}/children/add", data={"widget": "bouton"}, follow_redirects=False)
|
||||
child_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||
|
||||
resp = client.post(
|
||||
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps({"node_type": "trigger", "trigger_event": "clic", "trigger_element_id": child_id}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = client.post(f"/game/{game}/elements/{container_id}/delete", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
assert screens.get_element(game, container_id) is None
|
||||
assert screens.get_element(game, child_id) is None
|
||||
Reference in New Issue
Block a user