From c14f7e9ba5aed0ece6fab2c57fa1f3f6eacdc5bf Mon Sep 17 00:00:00 2001 From: william Date: Thu, 27 Aug 2026 19:10:54 +0200 Subject: [PATCH] =?UTF-8?q?Corrige=20le=20vrai=20bug=20:=20un=20champ=20"R?= =?UTF-8?q?elation"=20sans=20objet=20cible=20plantait=20toute=20la=20cr?= =?UTF-8?q?=C3=A9ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'utilisateur avait raison de contester mon précédent correctif : le problème n'était pas l'absence de champs. Reproduit précisément : créer un objet avec un champ de type "Relation vers un autre objet" SANS avoir de cible valide sélectionnée (ex. le tout premier objet créé dans un jeu — le sélecteur "Objet lié" est alors vide, faute d'un autre objet à pointer) plantait toute la requête avec une ValueError ("invalid literal for int() with base 10: ''") dans create_definition() / add_field_to_definition() (int(relation_definition_id) sans filet). Comme le crash survient APRÈS l'INSERT de la ligne _definitions mais AVANT le commit(), rien n'était jamais persisté (transaction perdue à la fermeture de la connexion) : l'objet entier disparaissait, pas seulement son champ "Relation" — d'où "le panneau recharge la page sans créer d'objet" alors que des champs avaient bien été renseignés. Correctif (routes, pas la couche db) : un champ "Relation" dont la cible n'est ni choisie ni un id valide est maintenant simplement IGNORÉ (comme une ligne sans nom, déjà le cas), dans les deux endroits qui construisent ce payload : - routes/objects/parse_field_rows.py (panneau "+ Nouvel objet") - routes/objects/object_field_add.py (panneau "+ Ajouter un champ" d'un objet déjà créé — même risque de crash dans add_field_to_definition) object_field_edit.py/update_field.py avaient déjà la bonne garde ("if relation_definition_id" avant le int()) — rien à y changer. Deux nouveaux tests, confirmés en échec sur l'ancien code (git stash, même ValueError reproduite) puis au vert avec le correctif. 133 tests au vert au total. Co-Authored-By: Claude Sonnet 5 --- routes/objects/object_field_add.py | 12 +++++-- routes/objects/parse_field_rows.py | 20 +++++++++-- tests/test_object_new_without_fields.py | 44 +++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/routes/objects/object_field_add.py b/routes/objects/object_field_add.py index 6fecc0e4..e383ab53 100644 --- a/routes/objects/object_field_add.py +++ b/routes/objects/object_field_add.py @@ -8,11 +8,17 @@ from core.flask_app import app @app.route("/game//objects//fields/add", methods=["POST"]) def object_field_add(slug, definition_id): name = request.form.get("field_name", "").strip() - if name: + field_type = request.form.get("field_type", "texte") + relation_definition_id = request.form.get("field_relation") + # Voir parse_field_rows.py (même garde) : "Relation" choisi sans objet + # cible valide ferait planter add_field_to_definition (int("") lève), + # ce qui annulait l'ALTER TABLE avant son commit() -- le champ + # n'était alors ajouté du tout, sans le moindre message d'erreur. + if name and not (field_type == "relation" and not (relation_definition_id or "").strip().isdigit()): db.add_field_to_definition(slug, definition_id, { "name": name, - "type": request.form.get("field_type", "texte"), - "relation_definition_id": request.form.get("field_relation"), + "type": field_type, + "relation_definition_id": relation_definition_id, "required": request.form.get("field_required") == "1", "min_value": request.form.get("field_min") or None, "max_value": request.form.get("field_max") or None, diff --git a/routes/objects/parse_field_rows.py b/routes/objects/parse_field_rows.py index c0a9b106..148aab27 100644 --- a/routes/objects/parse_field_rows.py +++ b/routes/objects/parse_field_rows.py @@ -9,10 +9,26 @@ def _parse_field_rows(form): for i, fname in enumerate(field_names): if not fname.strip(): continue + ftype = field_types[i] + relation_definition_id = field_relations[i] if i < len(field_relations) else None + if ftype == "relation" and not (relation_definition_id or "").strip().isdigit(): + # Type "Relation vers un autre objet" choisi sans (ou avant + # d'avoir pu) sélectionner d'objet cible réel — ex. le tout + # premier objet créé dans un jeu, quand ce sélecteur est encore + # vide faute d'un autre objet à pointer. create_definition() + # fait un int(relation_definition_id) sans filet : le laisser + # passer plantait TOUTE la requête (500), et comme la ligne + # _definitions n'était alors jamais validée (pas de commit() + # atteint), l'objet n'était pas créé du tout — pas de champ + # "Relation" non plus, mais un panneau qui semble "ne rien + # faire" plutôt qu'un message d'erreur clair. On ignore donc ce + # champ précis (comme une ligne sans nom, ci-dessus) plutôt que + # de faire échouer la création de TOUT l'objet à cause de lui. + continue fields.append({ "name": fname.strip(), - "type": field_types[i], - "relation_definition_id": field_relations[i] if i < len(field_relations) else None, + "type": ftype, + "relation_definition_id": relation_definition_id, "required": field_required[i] == "1" if i < len(field_required) else False, "min_value": (field_min[i].strip() or None) if i < len(field_min) else None, "max_value": (field_max[i].strip() or None) if i < len(field_max) else None, diff --git a/tests/test_object_new_without_fields.py b/tests/test_object_new_without_fields.py index 944447a9..6abe07c5 100644 --- a/tests/test_object_new_without_fields.py +++ b/tests/test_object_new_without_fields.py @@ -25,3 +25,47 @@ def test_object_not_created_without_a_name(client, game): resp = client.post(f"/game/{game}/objects/new", data={"object_name": " "}, follow_redirects=False) assert resp.status_code == 302 assert db.list_definitions(game) == [] + + +def test_relation_field_without_a_chosen_target_does_not_crash_the_whole_request(client, game): + """Régression : "Relation vers un autre objet" est TOUJOURS une option + du sélecteur de type (db/constants.py), même quand il n'existe encore + AUCUN autre objet à pointer (ex. le tout premier objet créé dans un + jeu) — le sélecteur "Objet lié" est alors vide, et + create_definition() faisait int("") sans filet, plantant TOUTE la + requête (500) avant même d'atteindre son commit() : l'objet n'était + donc PAS créé du tout, pas seulement son champ "Relation" — vécu comme + "le panneau recharge la page sans créer d'objet" alors que des champs + avaient bien été renseignés. Le champ "Relation" invalide doit être + ignoré (comme une ligne sans nom), pas faire échouer tout le reste.""" + resp = client.post(f"/game/{game}/objects/new", data={ + "object_name": "Personnage", + "field_name[]": ["nom", "arme"], + "field_type[]": ["texte", "relation"], + "field_relation[]": ["", ""], + "field_required[]": ["0", "0"], + "field_min[]": ["", ""], + "field_max[]": ["", ""], + }, follow_redirects=False) + assert resp.status_code == 302 + definitions = db.list_definitions(game) + assert len(definitions) == 1 + fields = db.get_definition(game, definitions[0]["id"])["fields"] + assert [f["name"] for f in fields] == ["nom"] + + +def test_add_relation_field_without_a_chosen_target_is_ignored_not_a_crash(client, game): + """Même garde, côté "+ Ajouter un champ" du panneau d'un objet déjà + créé (object_field_add.py / add_field_to_definition.py).""" + resp = client.post(f"/game/{game}/objects/new", data={ + "object_name": "Personnage", "field_name[]": ["nom"], "field_type[]": ["texte"], + "field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""], + }, follow_redirects=False) + def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1]) + + resp = client.post(f"/game/{game}/objects/{def_id}/fields/add", data={ + "field_name": "arme", "field_type": "relation", "field_relation": "", + }, follow_redirects=False) + assert resp.status_code == 302 + fields = db.get_definition(game, def_id)["fields"] + assert [f["name"] for f in fields] == ["nom"]