first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s

This commit is contained in:
william
2026-08-21 16:23:49 +02:00
committed by william
commit 3f4ebc4527
248 changed files with 10180 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Définitions d'objet (le "schéma" créé à la souris) et leurs champs."""
+43
View File
@@ -0,0 +1,43 @@
from ..connection import connect
from ..constants import FIELD_TYPES
from ..slugify import slugify
from .get_definition import get_definition
def add_field_to_definition(slug, definition_id, field):
"""CRUD — Update d'une définition : ajoute un champ à un objet déjà
créé. Exécute un vrai ALTER TABLE ... ADD COLUMN sur la table SQL
existante (les lignes déjà enregistrées reçoivent NULL pour ce
nouveau champ, comportement standard d'un ALTER TABLE)."""
definition = get_definition(slug, definition_id)
conn = connect(slug)
fname = slugify(field["name"]).replace("-", "_")
ftype = field["type"]
required = 1 if field.get("required") else 0
relation_definition_id = None
position = (max((f["position"] for f in definition["fields"]), default=-1)) + 1
if ftype == "relation":
related = get_definition(slug, int(field["relation_definition_id"]))
col = f"{fname}_id"
# ALTER TABLE ADD COLUMN de SQLite n'accepte pas de contrainte
# REFERENCES portant sur une colonne ajoutée après coup avec la
# même simplicité qu'à la création : on ajoute la colonne simple —
# c'est la table _fields qui reste la source de vérité utilisée par
# le moteur pour savoir que cette colonne est une relation.
conn.execute(f"ALTER TABLE {definition['table_name']} ADD COLUMN {col} INTEGER")
relation_definition_id = related["id"]
else:
sql_type = FIELD_TYPES[ftype]["sql"]
conn.execute(f"ALTER TABLE {definition['table_name']} ADD COLUMN {fname} {sql_type}")
min_value = field.get("min_value") if ftype in ("nombre_entier", "nombre_decimal") else None
max_value = field.get("max_value") if ftype in ("nombre_entier", "nombre_decimal") else None
conn.execute(
"""INSERT INTO _fields
(definition_id, name, type, relation_definition_id, required, position, min_value, max_value)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(definition_id, field["name"], ftype, relation_definition_id, required, position, min_value, max_value),
)
conn.commit()
conn.close()
+57
View File
@@ -0,0 +1,57 @@
from ..connection import connect
from ..constants import FIELD_TYPES
from ..slugify import slugify
from ..table_name_for import table_name_for
from .get_definition import get_definition
from .ensure_field_bounds_schema import ensure_field_bounds_schema
def create_definition(slug, name, fields):
"""Feature 2 : crée une définition d'objet (comme une table de BDD) et
exécute le vrai CREATE TABLE correspondant, avec les bons types de
colonne, y compris les colonnes de clé étrangère pour les relations
vers d'autres définitions déjà créées dans CE jeu (une définition peut
donc "en contenir" une autre, au sens relationnel)."""
ensure_field_bounds_schema(slug)
conn = connect(slug)
tname = table_name_for(name)
conn.execute(
"INSERT INTO _definitions (name, table_name) VALUES (?, ?)", (name, tname)
)
definition_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
columns_sql = ["id INTEGER PRIMARY KEY AUTOINCREMENT"]
for pos, f in enumerate(fields):
fname = slugify(f["name"]).replace("-", "_")
ftype = f["type"]
required = 1 if f.get("required") else 0
relation_definition_id = None
if ftype == "relation":
related = get_definition(slug, int(f["relation_definition_id"]))
col = f"{fname}_id"
columns_sql.append(
f"{col} INTEGER REFERENCES {related['table_name']}(id)"
)
relation_definition_id = related["id"]
else:
sql_type = FIELD_TYPES[ftype]["sql"]
not_null = " NOT NULL" if required else ""
columns_sql.append(f"{fname} {sql_type}{not_null}")
min_value = f.get("min_value") if ftype in ("nombre_entier", "nombre_decimal") else None
max_value = f.get("max_value") if ftype in ("nombre_entier", "nombre_decimal") else None
conn.execute(
"""INSERT INTO _fields
(definition_id, name, type, relation_definition_id, required, position, min_value, max_value)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(definition_id, f["name"], ftype, relation_definition_id, required, pos, min_value, max_value),
)
columns_sql.append("created_at TEXT DEFAULT CURRENT_TIMESTAMP")
create_sql = f"CREATE TABLE {tname} ({', '.join(columns_sql)})"
conn.execute(create_sql)
conn.commit()
conn.close()
return definition_id
+16
View File
@@ -0,0 +1,16 @@
from ..connection import connect
def definitions_referencing(slug, definition_id):
"""Autres définitions de CE jeu qui ont un champ de type relation
pointant vers cette définition — utilisé pour bloquer une suppression
qui casserait ces relations."""
conn = connect(slug)
rows = conn.execute(
"""SELECT DISTINCT d.id, d.name FROM _fields f
JOIN _definitions d ON d.id = f.definition_id
WHERE f.relation_definition_id = ?""",
(definition_id,),
).fetchall()
conn.close()
return [dict(r) for r in rows]
+12
View File
@@ -0,0 +1,12 @@
from ..connection import connect
from .get_definition import get_definition
def delete_definition(slug, definition_id):
definition = get_definition(slug, definition_id)
conn = connect(slug)
conn.execute(f"DROP TABLE IF EXISTS {definition['table_name']}")
conn.execute("DELETE FROM _fields WHERE definition_id = ?", (definition_id,))
conn.execute("DELETE FROM _definitions WHERE id = ?", (definition_id,))
conn.commit()
conn.close()
+28
View File
@@ -0,0 +1,28 @@
import sqlite3
from ..connection import connect
from ..slugify import slugify
from .get_definition import get_definition
def delete_field(slug, definition_id, field_id):
"""CRUD — Update d'une définition : retire un champ. Exécute un vrai
ALTER TABLE ... DROP COLUMN (SQLite ≥ 3.35). Sur une version de SQLite
trop ancienne pour DROP COLUMN, le champ est retiré de la définition
(le moteur ne le proposera plus dans les formulaires) mais la colonne
SQL peut subsister sans casser quoi que ce soit d'autre."""
definition = get_definition(slug, definition_id)
field = next((f for f in definition["fields"] if f["id"] == field_id), None)
if not field:
return
col = slugify(field["name"]).replace("-", "_")
if field["type"] == "relation":
col += "_id"
conn = connect(slug)
try:
conn.execute(f"ALTER TABLE {definition['table_name']} DROP COLUMN {col}")
except sqlite3.OperationalError:
pass
conn.execute("DELETE FROM _fields WHERE id = ?", (field_id,))
conn.commit()
conn.close()
@@ -0,0 +1,16 @@
from ..connection import connect
def ensure_field_bounds_schema(slug):
"""Migration légère (voir screens/screens_repo/ensure_schema.py pour le
même principe) : ajoute les colonnes min_value/max_value à _fields pour
les jeux créés avant le bornage automatique (2.2) — aucune migration
manuelle nécessaire, la prochaine lecture d'une définition suffit."""
conn = connect(slug)
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_fields)").fetchall()}
if "min_value" not in cols:
conn.execute("ALTER TABLE _fields ADD COLUMN min_value REAL")
if "max_value" not in cols:
conn.execute("ALTER TABLE _fields ADD COLUMN max_value REAL")
conn.commit()
conn.close()
+18
View File
@@ -0,0 +1,18 @@
from ..connection import connect
from .ensure_field_bounds_schema import ensure_field_bounds_schema
def get_definition(slug, definition_id):
ensure_field_bounds_schema(slug)
conn = connect(slug)
d = conn.execute("SELECT * FROM _definitions WHERE id = ?", (definition_id,)).fetchone()
if not d:
conn.close()
return None
fields = conn.execute(
"SELECT * FROM _fields WHERE definition_id = ? ORDER BY position, id", (definition_id,)
).fetchall()
conn.close()
d = dict(d)
d["fields"] = [dict(f) for f in fields]
return d
+8
View File
@@ -0,0 +1,8 @@
from ..connection import connect
def list_definitions(slug):
conn = connect(slug)
rows = conn.execute("SELECT * FROM _definitions ORDER BY id").fetchall()
conn.close()
return [dict(r) for r in rows]
+8
View File
@@ -0,0 +1,8 @@
from ..connection import connect
def rename_definition(slug, definition_id, new_name):
conn = connect(slug)
conn.execute("UPDATE _definitions SET name = ? WHERE id = ?", (new_name, definition_id))
conn.commit()
conn.close()
+48
View File
@@ -0,0 +1,48 @@
import sqlite3
from ..connection import connect
from ..slugify import slugify
from .get_definition import get_definition
def update_field(slug, definition_id, field_id, new_name, required, relation_definition_id=None, min_value=None, max_value=None):
"""CRUD — Update d'une définition : modifie un champ déjà créé (nom,
obligatoire, objet lié si c'est une relation, et bornes mini/maxi si
c'est un champ numérique — voir 2.2, bornage automatique). Si le nom
change, la vraie colonne SQL est renommée (ALTER TABLE ... RENAME
COLUMN, SQLite ≥ 3.25) pour que la colonne réelle continue de
correspondre exactement au nom du champ tel qu'affiché — pas de dérive
entre la définition et la table. Le TYPE d'un champ existant ne se
change pas ici (une vraie conversion de type SQLite demanderait de
reconstruire la table et de convertir les données déjà enregistrées,
hors scope de cette version)."""
definition = get_definition(slug, definition_id)
field = next((f for f in definition["fields"] if f["id"] == field_id), None)
if not field:
return
old_col = slugify(field["name"]).replace("-", "_")
new_col = slugify(new_name).replace("-", "_")
if field["type"] == "relation":
old_col += "_id"
new_col += "_id"
conn = connect(slug)
if old_col != new_col:
try:
conn.execute(
f"ALTER TABLE {definition['table_name']} RENAME COLUMN {old_col} TO {new_col}"
)
except sqlite3.OperationalError:
pass # SQLite trop ancien pour RENAME COLUMN : la colonne SQL garde son ancien nom
rel_id = int(relation_definition_id) if (field["type"] == "relation" and relation_definition_id) else field["relation_definition_id"]
is_numeric = field["type"] in ("nombre_entier", "nombre_decimal")
min_v = (min_value if min_value not in (None, "") else None) if is_numeric else None
max_v = (max_value if max_value not in (None, "") else None) if is_numeric else None
conn.execute(
"UPDATE _fields SET name = ?, required = ?, relation_definition_id = ?, min_value = ?, max_value = ? WHERE id = ?",
(new_name, 1 if required else 0, rel_id, min_v, max_v, field_id),
)
conn.commit()
conn.close()