first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Définitions d'objet (le "schéma" créé à la souris) et leurs champs."""
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user