Fonctionnalité mise de côté depuis le tout début de ce chantier
("jouable en toute autonomie"). Nouveau bouton "📦 Publier" dans la barre
de navigation du jeu (après "▶️ Jouer") : construit un zip contenant un
Python portable + Flask embarqués, une copie figée du moteur de rendu
(screens/db/filters + le strict minimum de core/) et des données du jeu
(game.db + uploads/), lancé en double-cliquant sur run.bat — aucune
installation requise, 100% hors-ligne (voir le commit précédent qui a
auto-hébergé polices/animate.css, dernière dépendance CDN de l'app).
Exploration préalable a confirmé que screens/ et db/ sont déjà
totalement découplés de auth/routes/core (seul lien : un try/except
optionnel dans db/connection.py) et que templates/play.html n'appelle
que 4 endpoints "purs" (aucune logique d'auth mélangée dedans) — ce qui
a permis de réutiliser ces routes quasiment telles quelles dans un
mini-serveur Flask séparé plutôt que de les réécrire.
- db/constants.py : PROJECTS_DIR devient surchargeable via
FORGE_PROJECTS_DIR (même schéma que auth/connection.py) — le serveur
joueur autonome pointe ainsi vers son propre dossier "projects/"
embarqué.
- publish/vendor_runtime.py : télécharge (une fois par poste, mis en
cache sous data/publish_vendor/ — déjà ignoré par git) le ZIP Python
embeddable officiel (python.org) et vendore Flask via pip --target ;
fonctions séparées et mockables pour ne jamais déclencher de vrai
téléchargement dans les tests.
- publish/player_app_template.py : mini-Flask autonome, réutilise
core/flask_app.py et core/jinja_filters.py tels quels (SLUG figé en
dur au moment de la publication, csrf_token() factice puisqu'aucune
session n'existe dans cet export). sys.path doit être complété
manuellement au démarrage : le python311._pth de la distribution
embeddable ne référence que le dossier de python.exe lui-même, jamais
celui du script lancé.
- publish/build_package.py : assemble le zip dans un dossier temporaire
(jamais les vrais fichiers de l'app), copié/nettoyé après envoi de la
réponse HTTP (routes/publish/publish_game.py, déjà protégée par la
garde d'accès existante — aucune vérification supplémentaire).
- templates/base.html : bouton + modale (avancement séquentiel, jamais
de suivi serveur réel — le build est rapide) qui déclenche le
téléchargement du zip via un blob, comme la modale des codes de
récupération déjà en place (posée À L'INTÉRIEUR de <main> pour que
pjax.js la remplace et rejoue son script à chaque navigation).
Vérifié pour de vrai (pas seulement via les tests) : zip construit,
extrait, lancé avec le Python embeddable réel — /, /game/test/play,
/game/test/runtime-payload et /static/style.css répondent tous 200.
tests/test_publish.py (nouveau, vendor mocké) : structure du zip,
SLUG correctement substitué, route protégée par la même isolation par
projet que le reste de l'app. 203 tests au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44 lines
2.1 KiB
Python
44 lines
2.1 KiB
Python
"""Constantes partagées de la couche données."""
|
|
|
|
import os
|
|
|
|
# Surchargeable via FORGE_PROJECTS_DIR (même schéma que
|
|
# auth/connection.py::FORGE_USERS_DB_PATH) — utilisé par le serveur
|
|
# joueur autonome (publish/player_app_template.py), qui pose cette
|
|
# variable AVANT d'importer db/ pour pointer vers son propre dossier
|
|
# "projects/" embarqué plutôt que celui, réel, du poste de développement.
|
|
# Résolu ici, à l'IMPORT (pas par un appel de fonction) : la variable
|
|
# d'environnement doit donc déjà être posée avant le tout premier
|
|
# `import db`.
|
|
PROJECTS_DIR = os.environ.get("FORGE_PROJECTS_DIR") or 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"},
|
|
}
|
|
|
|
# Types disponibles pour une variable globale (voir db/global_vars/) — un
|
|
# sous-ensemble de FIELD_TYPES : ni "relation" (une variable globale n'a pas
|
|
# d'objet à pointer) ni "texte_long"/"date" (pas gérés par
|
|
# apply_variable_action.py, qui ne coerce que ces 4 types). "objet"/
|
|
# "tableau" sont, eux, propres aux variables (pas de colonne SQL réelle,
|
|
# contrairement à un champ d'objet) : la valeur est stockée telle quelle
|
|
# (colonne TEXT) sous forme de JSON, lue via un chemin ".champ"/"[index]"
|
|
# (voir _resolve_variable_path dans screens/rendering/filter_repeater_rows.py).
|
|
GLOBAL_VARIABLE_TYPES = {
|
|
"texte": FIELD_TYPES["texte"],
|
|
"nombre_entier": FIELD_TYPES["nombre_entier"],
|
|
"nombre_decimal": FIELD_TYPES["nombre_decimal"],
|
|
"booleen": FIELD_TYPES["booleen"],
|
|
"objet": {"label": "Objet (JSON)"},
|
|
"tableau": {"label": "Tableau (JSON)"},
|
|
}
|