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"]