first commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
db — toute la logique base de données du moteur Forge, organisée en un
|
||||
sous-module par responsabilité (un fichier = une fonction, un dossier = un
|
||||
domaine) plutôt qu'un seul gros fichier. Ce fichier réexporte l'intégralité
|
||||
de l'API publique historique : le reste du moteur continue simplement
|
||||
d'écrire `import db` puis `db.ma_fonction(...)`, sans rien savoir de cette
|
||||
organisation interne.
|
||||
|
||||
Chaque JEU créé a SA PROPRE base SQLite (fichier .db dans son dossier de
|
||||
projet). Cette base contient :
|
||||
- des tables "méta" (_definitions, _fields) qui décrivent les objets que
|
||||
l'utilisateur a définis via l'interface (l'équivalent d'un schéma de
|
||||
base de données, mais construit à la souris plutôt qu'en SQL) ;
|
||||
- une VRAIE table SQL par objet défini (obj_<nom>), avec des colonnes du
|
||||
bon type SQLite selon le type choisi pour chaque champ, y compris des
|
||||
colonnes de clé étrangère pour les champs de type "relation".
|
||||
|
||||
Rien n'est une simulation : créer une "définition d'objet" avec des champs
|
||||
exécute un vrai CREATE TABLE, et remplir le formulaire généré exécute un
|
||||
vrai INSERT dans cette table.
|
||||
"""
|
||||
|
||||
from .constants import PROJECTS_DIR, FIELD_TYPES
|
||||
from .slugify import slugify
|
||||
from .table_name_for import table_name_for
|
||||
from .game_dir import game_dir
|
||||
from .db_path import db_path
|
||||
from .connection import connect
|
||||
|
||||
from .games.list_games import list_games
|
||||
from .games.game_meta import game_meta
|
||||
from .games.create_game import create_game
|
||||
from .games.update_game_name import update_game_name
|
||||
from .games.delete_game import delete_game
|
||||
|
||||
from .definitions.list_definitions import list_definitions
|
||||
from .definitions.get_definition import get_definition
|
||||
from .definitions.create_definition import create_definition
|
||||
from .definitions.rename_definition import rename_definition
|
||||
from .definitions.definitions_referencing import definitions_referencing
|
||||
from .definitions.add_field_to_definition import add_field_to_definition
|
||||
from .definitions.update_field import update_field
|
||||
from .definitions.delete_field import delete_field
|
||||
from .definitions.delete_definition import delete_definition
|
||||
|
||||
from .rows.list_rows import list_rows
|
||||
from .rows.relation_options import relation_options
|
||||
from .rows.insert_row import insert_row
|
||||
from .rows.get_row import get_row
|
||||
from .rows.update_row import update_row
|
||||
from .rows.update_row_field import update_row_field
|
||||
from .rows.delete_row import delete_row
|
||||
from .rows.rows_referencing import rows_referencing
|
||||
|
||||
__all__ = [
|
||||
"PROJECTS_DIR", "FIELD_TYPES",
|
||||
"slugify", "table_name_for", "game_dir", "db_path", "connect",
|
||||
"list_games", "game_meta", "create_game", "update_game_name", "delete_game",
|
||||
"list_definitions", "get_definition", "create_definition", "rename_definition",
|
||||
"definitions_referencing", "add_field_to_definition", "update_field", "delete_field",
|
||||
"delete_definition",
|
||||
"list_rows", "relation_options", "insert_row", "get_row", "update_row",
|
||||
"update_row_field", "delete_row", "rows_referencing",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
import sqlite3
|
||||
|
||||
from .db_path import db_path
|
||||
|
||||
|
||||
def connect(slug):
|
||||
# timeout=10 : si une autre connexion tient un verrou d'écriture au même
|
||||
# instant (deux requêtes qui arrivent presque en même temps, ex. deux
|
||||
# onglets, ou le navigateur qui recharge plusieurs ressources), sqlite3
|
||||
# réessaie pendant 10 secondes au lieu de lever immédiatement "database
|
||||
# is locked". Le mode WAL (Write-Ahead Logging) va plus loin : il permet
|
||||
# à des lectures de se faire PENDANT qu'une écriture est en cours
|
||||
# ailleurs, ce qui est la cause la plus fréquente de ce blocage avec le
|
||||
# serveur de développement Flask.
|
||||
conn = sqlite3.connect(db_path(slug), timeout=10)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.execute("PRAGMA journal_mode = WAL")
|
||||
conn.execute("PRAGMA busy_timeout = 8000")
|
||||
return conn
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Constantes partagées de la couche données."""
|
||||
|
||||
import os
|
||||
|
||||
PROJECTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "projects")
|
||||
|
||||
# Types de champ exposés dans l'interface -> type de colonne SQLite réel.
|
||||
FIELD_TYPES = {
|
||||
"texte": {"label": "Texte court", "sql": "TEXT"},
|
||||
"texte_long": {"label": "Texte long", "sql": "TEXT"},
|
||||
"nombre_entier": {"label": "Nombre entier", "sql": "INTEGER"},
|
||||
"nombre_decimal": {"label": "Nombre décimal", "sql": "REAL"},
|
||||
"booleen": {"label": "Oui / Non", "sql": "INTEGER"},
|
||||
"date": {"label": "Date", "sql": "TEXT"},
|
||||
"relation": {"label": "Relation vers un autre objet", "sql": "INTEGER"},
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from .game_dir import game_dir
|
||||
|
||||
|
||||
def db_path(slug):
|
||||
return os.path.join(game_dir(slug), "game.db")
|
||||
@@ -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()
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from .constants import PROJECTS_DIR
|
||||
|
||||
|
||||
def game_dir(slug):
|
||||
return os.path.join(PROJECTS_DIR, slug)
|
||||
@@ -0,0 +1 @@
|
||||
"""Cycle de vie d'un jeu (création, méta, suppression)."""
|
||||
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
|
||||
from ..connection import connect
|
||||
from ..game_dir import game_dir
|
||||
from ..slugify import slugify
|
||||
|
||||
|
||||
def create_game(name):
|
||||
"""Feature 1 : crée le dossier du jeu, ses fichiers index.html/css/js
|
||||
reliés entre eux, et sa base de données dédiée (nom du jeu en méta)."""
|
||||
slug = slugify(name)
|
||||
base = slug
|
||||
n = 2
|
||||
while os.path.isdir(game_dir(base)):
|
||||
base = f"{slug}-{n}"
|
||||
n += 1
|
||||
slug = base
|
||||
|
||||
os.makedirs(game_dir(slug), exist_ok=True)
|
||||
|
||||
index_html = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{name}</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>{name}</h1>
|
||||
<p>Squelette généré par le moteur Forge — à remplir au fur et à mesure
|
||||
des fonctionnalités du jeu.</p>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
index_css = """body {
|
||||
font-family: system-ui, sans-serif;
|
||||
margin: 40px;
|
||||
color: #222;
|
||||
}
|
||||
"""
|
||||
index_js = """// index.js — point d'entrée du jeu, généré par le moteur Forge.
|
||||
console.log("Jeu chargé.");
|
||||
"""
|
||||
with open(os.path.join(game_dir(slug), "index.html"), "w", encoding="utf-8") as f:
|
||||
f.write(index_html)
|
||||
with open(os.path.join(game_dir(slug), "index.css"), "w", encoding="utf-8") as f:
|
||||
f.write(index_css)
|
||||
with open(os.path.join(game_dir(slug), "index.js"), "w", encoding="utf-8") as f:
|
||||
f.write(index_js)
|
||||
|
||||
conn = connect(slug)
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE _meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
CREATE TABLE _definitions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
table_name TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE _fields (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
definition_id INTEGER NOT NULL REFERENCES _definitions(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
relation_definition_id INTEGER REFERENCES _definitions(id),
|
||||
required INTEGER NOT NULL DEFAULT 0,
|
||||
position INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute("INSERT INTO _meta (key, value) VALUES ('name', ?)", (name,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return slug
|
||||
@@ -0,0 +1,7 @@
|
||||
import shutil
|
||||
|
||||
from ..game_dir import game_dir
|
||||
|
||||
|
||||
def delete_game(slug):
|
||||
shutil.rmtree(game_dir(slug))
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def game_meta(slug):
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'name'").fetchone()
|
||||
conn.close()
|
||||
return {"slug": slug, "name": row["value"] if row else slug}
|
||||
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
from ..constants import PROJECTS_DIR
|
||||
from ..connection import connect
|
||||
from ..db_path import db_path
|
||||
|
||||
|
||||
def list_games():
|
||||
"""Scanne le dossier projects/ : chaque sous-dossier avec un game.db est un jeu."""
|
||||
games = []
|
||||
if not os.path.isdir(PROJECTS_DIR):
|
||||
return games
|
||||
for slug in sorted(os.listdir(PROJECTS_DIR)):
|
||||
path = db_path(slug)
|
||||
if os.path.isfile(path):
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'name'").fetchone()
|
||||
conn.close()
|
||||
games.append({"slug": slug, "name": row["value"] if row else slug})
|
||||
return games
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def update_game_name(slug, new_name):
|
||||
conn = connect(slug)
|
||||
conn.execute("UPDATE _meta SET value = ? WHERE key = 'name'", (new_name,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1 @@
|
||||
"""Lignes de données (CRUD) pour les objets définis dans un jeu."""
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def delete_row(slug, definition, row_id):
|
||||
conn = connect(slug)
|
||||
conn.execute(f"DELETE FROM {definition['table_name']} WHERE id = ?", (row_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,10 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def get_row(slug, definition, row_id):
|
||||
conn = connect(slug)
|
||||
row = conn.execute(
|
||||
f"SELECT * FROM {definition['table_name']} WHERE id = ?", (row_id,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
@@ -0,0 +1,14 @@
|
||||
from ..connection import connect
|
||||
from .row_columns_and_values import row_columns_and_values
|
||||
|
||||
|
||||
def insert_row(slug, definition, form_data):
|
||||
"""Feature 3 : enregistre en base les données saisies dans le formulaire
|
||||
généré à partir de la définition d'objet."""
|
||||
cols, values = row_columns_and_values(definition, form_data)
|
||||
conn = connect(slug)
|
||||
placeholders = ["?"] * len(cols)
|
||||
sql = f"INSERT INTO {definition['table_name']} ({', '.join(cols)}) VALUES ({', '.join(placeholders)})"
|
||||
conn.execute(sql, values)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,8 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def list_rows(slug, definition):
|
||||
conn = connect(slug)
|
||||
rows = conn.execute(f"SELECT * FROM {definition['table_name']} ORDER BY id DESC").fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
@@ -0,0 +1,19 @@
|
||||
from ..connection import connect
|
||||
from ..slugify import slugify
|
||||
from ..definitions.get_definition import get_definition
|
||||
|
||||
|
||||
def relation_options(slug, related_definition_id):
|
||||
"""Lignes existantes d'un objet lié, pour peupler un <select> de relation."""
|
||||
related = get_definition(slug, related_definition_id)
|
||||
conn = connect(slug)
|
||||
rows = conn.execute(f"SELECT * FROM {related['table_name']} ORDER BY id").fetchall()
|
||||
conn.close()
|
||||
display_field = related["fields"][0]["name"] if related["fields"] else "id"
|
||||
display_col = slugify(display_field).replace("-", "_")
|
||||
options = []
|
||||
for r in rows:
|
||||
r = dict(r)
|
||||
label = r.get(display_col, f"#{r['id']}")
|
||||
options.append({"id": r["id"], "label": f"{label} (#{r['id']})"})
|
||||
return options
|
||||
@@ -0,0 +1,26 @@
|
||||
from ..slugify import slugify
|
||||
|
||||
|
||||
def row_columns_and_values(definition, form_data):
|
||||
"""Traduit les données d'un formulaire généré en (colonnes, valeurs)
|
||||
prêtes pour un INSERT ou un UPDATE — factorisé pour que les deux
|
||||
partagent exactement la même logique de conversion de type."""
|
||||
cols, values = [], []
|
||||
for f in definition["fields"]:
|
||||
fname = slugify(f["name"]).replace("-", "_")
|
||||
if f["type"] == "relation":
|
||||
col = f"{fname}_id"
|
||||
raw = form_data.get(col, "")
|
||||
values.append(int(raw) if raw else None)
|
||||
elif f["type"] == "booleen":
|
||||
values.append(1 if form_data.get(fname) == "on" else 0)
|
||||
col = fname
|
||||
elif f["type"] in ("nombre_entier", "nombre_decimal"):
|
||||
raw = form_data.get(fname, "")
|
||||
values.append((int(raw) if f["type"] == "nombre_entier" else float(raw)) if raw != "" else None)
|
||||
col = fname
|
||||
else:
|
||||
values.append(form_data.get(fname, "") or None)
|
||||
col = fname
|
||||
cols.append(col)
|
||||
return cols, values
|
||||
@@ -0,0 +1,26 @@
|
||||
from ..connection import connect
|
||||
from ..slugify import slugify
|
||||
from ..definitions.definitions_referencing import definitions_referencing
|
||||
from ..definitions.get_definition import get_definition
|
||||
|
||||
|
||||
def rows_referencing(slug, definition_id, row_id):
|
||||
"""Pour l'écran de suppression d'une ligne : liste, pour chaque objet
|
||||
qui a une relation vers l'objet de cette ligne, combien de ses lignes
|
||||
pointent vers CETTE ligne précise — pour avertir avant une suppression
|
||||
qui laisserait des relations orphelines (aucune suppression en cascade
|
||||
n'est faite automatiquement, par sécurité)."""
|
||||
refs = []
|
||||
for d in definitions_referencing(slug, definition_id):
|
||||
full = get_definition(slug, d["id"])
|
||||
for f in full["fields"]:
|
||||
if f["type"] == "relation" and f["relation_definition_id"] == definition_id:
|
||||
col = slugify(f["name"]).replace("-", "_") + "_id"
|
||||
conn = connect(slug)
|
||||
count = conn.execute(
|
||||
f"SELECT COUNT(*) AS c FROM {full['table_name']} WHERE {col} = ?", (row_id,)
|
||||
).fetchone()["c"]
|
||||
conn.close()
|
||||
if count:
|
||||
refs.append({"definition_name": full["name"], "field_name": f["name"], "count": count})
|
||||
return refs
|
||||
@@ -0,0 +1,16 @@
|
||||
from ..connection import connect
|
||||
from .row_columns_and_values import row_columns_and_values
|
||||
|
||||
|
||||
def update_row(slug, definition, row_id, form_data):
|
||||
"""CRUD — Update : ré-enregistre une ligne existante avec les nouvelles
|
||||
valeurs saisies dans le même formulaire généré que pour la création."""
|
||||
cols, values = row_columns_and_values(definition, form_data)
|
||||
conn = connect(slug)
|
||||
set_clause = ", ".join(f"{c} = ?" for c in cols)
|
||||
conn.execute(
|
||||
f"UPDATE {definition['table_name']} SET {set_clause} WHERE id = ?",
|
||||
values + [row_id],
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,15 @@
|
||||
from ..connection import connect
|
||||
from ..slugify import slugify
|
||||
|
||||
|
||||
def update_row_field(slug, definition, row_id, field_def, new_value):
|
||||
"""Met à jour UNE SEULE colonne d'une ligne existante — utilisé par les
|
||||
actions "Modifier une donnée" du mode jouable, qui ne doivent jamais
|
||||
écraser les autres champs de la ligne (contrairement à update_row, qui
|
||||
attend un formulaire complet)."""
|
||||
fname = slugify(field_def["name"]).replace("-", "_")
|
||||
col = f"{fname}_id" if field_def["type"] == "relation" else fname
|
||||
conn = connect(slug)
|
||||
conn.execute(f"UPDATE {definition['table_name']} SET {col} = ? WHERE id = ?", (new_value, row_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,8 @@
|
||||
import re
|
||||
|
||||
|
||||
def slugify(name):
|
||||
s = name.strip().lower()
|
||||
s = re.sub(r"[^a-z0-9]+", "-", s)
|
||||
s = re.sub(r"-+", "-", s).strip("-")
|
||||
return s or "jeu"
|
||||
@@ -0,0 +1,6 @@
|
||||
from .slugify import slugify
|
||||
|
||||
|
||||
def table_name_for(object_name):
|
||||
"""Nom de la vraie table SQL pour un objet défini par l'utilisateur."""
|
||||
return "obj_" + slugify(object_name).replace("-", "_")
|
||||
Reference in New Issue
Block a user