Dev #3

Merged
w-vandal merged 7 commits from dev into main 2026-08-27 17:46:02 +00:00
2 changed files with 52 additions and 10 deletions
Showing only changes of commit 169b720620 - Show all commits
+10 -10
View File
@@ -453,8 +453,8 @@
{% set rows = rows_by_definition[d.id] %}
{% if rows %}
{% for r in rows %}
<form method="post" action="{{ url_for('data_edit', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataEditForm{{ r.id }}"></form>
<form method="post" action="{{ url_for('data_delete', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataDeleteForm{{ r.id }}"
<form method="post" action="{{ url_for('data_edit', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataEditForm{{ d.id }}-{{ r.id }}"></form>
<form method="post" action="{{ url_for('data_delete', slug=game.slug, definition_id=d.id, row_id=r.id) }}" id="dataDeleteForm{{ d.id }}-{{ r.id }}"
onsubmit="return confirm('Supprimer définitivement l\'entrée #{{ r.id }} ?');"></form>
{% endfor %}
<div class="fieldsTableWrap">
@@ -470,18 +470,18 @@
{% set col = f.name|colname %}
<td data-col="field-{{ f.id }}" data-field-label="{{ f.name }}">
{% if f.type == 'texte' %}
<input class="input is-small" type="text" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
<input class="input is-small" type="text" form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
{% elif f.type == 'texte_long' %}
<textarea class="textarea is-small" form="dataEditForm{{ r.id }}" name="{{ col }}" rows="1">{{ r[col] or '' }}</textarea>
<textarea class="textarea is-small" form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}" rows="1">{{ r[col] or '' }}</textarea>
{% elif f.type in ('nombre_entier', 'nombre_decimal') %}
<input class="input is-small" type="number" step="{{ '1' if f.type == 'nombre_entier' else 'any' }}" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] if r[col] is not none else '' }}">
<input class="input is-small" type="number" step="{{ '1' if f.type == 'nombre_entier' else 'any' }}" form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}" value="{{ r[col] if r[col] is not none else '' }}">
{% elif f.type == 'booleen' %}
<label class="checkbox"><input type="checkbox" form="dataEditForm{{ r.id }}" name="{{ col }}" {{ 'checked' if r[col] else '' }}></label>
<label class="checkbox"><input type="checkbox" form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}" {{ 'checked' if r[col] else '' }}></label>
{% elif f.type == 'date' %}
<input class="input is-small" type="date" form="dataEditForm{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
<input class="input is-small" type="date" form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}" value="{{ r[col] or '' }}">
{% elif f.type == 'relation' %}
<div class="select is-small">
<select form="dataEditForm{{ r.id }}" name="{{ col }}_id">
<select form="dataEditForm{{ d.id }}-{{ r.id }}" name="{{ col }}_id">
<option value="">—</option>
{% for opt in relation_options_by_definition[d.id].get(f.id, []) %}
<option value="{{ opt.id }}" {{ 'selected' if r[col ~ '_id'] == opt.id else '' }}>{{ opt.label }}</option>
@@ -493,8 +493,8 @@
{% endfor %}
<td class="fieldsTableActions">
<button type="button" class="button is-small" title="Voir cette entrée en détail" onclick="openRowDetailPanel({{ d.id }}, {{ r.id }})">👁️</button>
<button type="submit" form="dataEditForm{{ r.id }}" class="button is-small">Enregistrer</button>
<button type="submit" form="dataDeleteForm{{ r.id }}" class="button is-small is-danger">🗑️</button>
<button type="submit" form="dataEditForm{{ d.id }}-{{ r.id }}" class="button is-small">Enregistrer</button>
<button type="submit" form="dataDeleteForm{{ d.id }}-{{ r.id }}" class="button is-small is-danger">🗑️</button>
</td>
</tr>
{% endfor %}
+42
View File
@@ -0,0 +1,42 @@
"""Régression : chaque objet a sa propre table SQLite, donc ses ids de
ligne repartent de 1 (db/rows/insert_row.py) — les <form id="dataEditForm
{{r.id}}"> de l'onglet Données (game_dashboard.html) doivent donc être
scopés par objet (dataEditForm{{d.id}}-{{r.id}}), sinon deux objets ayant
chacun une ligne #1 partagent le même id de formulaire dans le DOM et le
navigateur soumet vers le premier objet trouvé, pas celui qu'on modifie
réellement."""
import re
def _create_object(client, slug, name, field_name, field_type="texte"):
resp = client.post(f"/game/{slug}/objects/new", data={
"object_name": name, "field_name[]": [field_name], "field_type[]": [field_type],
"field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""],
}, follow_redirects=False)
return int(resp.headers["Location"].rstrip("/").split("/")[-1])
def test_data_edit_form_ids_are_scoped_per_object(client, game):
def_a = _create_object(client, game, "Jauge", "valeur", "nombre_entier")
def_b = _create_object(client, game, "Autre", "nom", "texte")
# Chaque objet a sa PROPRE table -> sa première ligne a id=1 dans les
# deux cas, exactement le scénario qui provoquait la collision.
client.post(f"/game/{game}/objects/{def_a}/data/new", data={"valeur": "10"})
client.post(f"/game/{game}/objects/{def_b}/data/new", data={"nom": "Bob"})
html = client.get(f"/game/{game}?tab=objects").get_data(as_text=True)
assert f'id="dataEditForm{def_a}-1"' in html
assert f'id="dataEditForm{def_b}-1"' in html
# Les deux formulaires doivent avoir des ids DISTINCTS malgré le même row id.
assert html.count('id="dataEditForm') == len(set(re.findall(r'id="(dataEditForm[^"]+)"', html)))
# Modifier la donnée du DEUXIÈME objet doit bien mettre à jour def_b,
# sans toucher def_a.
client.post(f"/game/{game}/objects/{def_b}/data/1/edit", data={"nom": "Alice"})
import db
row_a = db.list_rows(game, db.get_definition(game, def_a))[0]
row_b = db.list_rows(game, db.get_definition(game, def_b))[0]
assert row_a["valeur"] == 10
assert row_b["nom"] == "Alice"