Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -6,7 +6,7 @@ from ..slugify import slugify
|
||||
from .project_slug import build_slug
|
||||
|
||||
|
||||
def create_game(name, owner_folder=None, project_slug_override=None):
|
||||
def create_game(name: str, owner_folder: str | None = None, project_slug_override: str | None = None) -> str:
|
||||
"""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).
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from ..game_dir import game_dir
|
||||
from .project_slug import split_slug
|
||||
|
||||
|
||||
def delete_game(slug):
|
||||
def delete_game(slug: str) -> None:
|
||||
shutil.rmtree(game_dir(slug))
|
||||
# Structure de dossiers par utilisateur (voir project_slug.py) :
|
||||
# nettoie aussi le dossier propriétaire s'il ne contient plus aucun
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from typing import Any
|
||||
|
||||
from ..connection import connect
|
||||
from .game_type_catalog import get_onboarding_type
|
||||
|
||||
|
||||
def game_meta(slug):
|
||||
def game_meta(slug: str) -> dict[str, Any]:
|
||||
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,
|
||||
"slug": slug,
|
||||
"name": row["value"] if row else slug,
|
||||
# Onboarding guidé (voir game_type_catalog.py) : utilisé par
|
||||
# templates/base.html pour cacher le lien "Tableau de bord" à un
|
||||
# compte "restreint" (quiz/embranchement/rpg).
|
||||
|
||||
@@ -13,11 +13,14 @@ _meta['onboarding_type'] (une ligne _meta par projet, même convention que
|
||||
game_type) retient CE choix — jamais lu par le
|
||||
rendu jouable, seulement par le routage (routes/games/game_dashboard.py,
|
||||
routes/screens/screens_new.py)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import db
|
||||
|
||||
DEFAULT_ONBOARDING_TYPE = "rpg"
|
||||
|
||||
ONBOARDING_TYPES = {
|
||||
ONBOARDING_TYPES: dict[str, dict[str, Any]] = {
|
||||
"rpg": {
|
||||
"label": "Créer un jeu 2D ludique",
|
||||
"tagline": "Ton monde, ton héros, ton scénario.",
|
||||
@@ -57,7 +60,7 @@ ONBOARDING_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
def get_onboarding_type_raw(slug):
|
||||
def get_onboarding_type_raw(slug: str) -> str | None:
|
||||
"""None si _meta['onboarding_type'] est absent — distingue un projet
|
||||
JAMAIS passé par l'onboarding guidé (créé avant son existence, ou par
|
||||
l'admin via "+ Nouveau jeu") d'un projet explicitement "custom"."""
|
||||
@@ -67,11 +70,11 @@ def get_onboarding_type_raw(slug):
|
||||
return row["value"] if row else None
|
||||
|
||||
|
||||
def get_onboarding_type(slug):
|
||||
def get_onboarding_type(slug: str) -> str:
|
||||
return get_onboarding_type_raw(slug) or DEFAULT_ONBOARDING_TYPE
|
||||
|
||||
|
||||
def set_onboarding_type(slug, onboarding_type):
|
||||
def set_onboarding_type(slug: str, onboarding_type: str) -> None:
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('onboarding_type', ?)",
|
||||
@@ -81,15 +84,14 @@ def set_onboarding_type(slug, onboarding_type):
|
||||
conn.close()
|
||||
|
||||
|
||||
def is_restricted(onboarding_type_or_slug):
|
||||
def is_restricted(onboarding_type_or_slug: str) -> bool:
|
||||
"""Accepte directement une clé de ONBOARDING_TYPES, ou un slug de
|
||||
projet (résout alors son onboarding_type d'abord) — pratique aussi
|
||||
bien pour core/auth_guard.py (a le slug) que pour un test unitaire (a
|
||||
déjà la clé)."""
|
||||
onboarding_type = (
|
||||
onboarding_type_or_slug if onboarding_type_or_slug in ONBOARDING_TYPES
|
||||
onboarding_type_or_slug
|
||||
if onboarding_type_or_slug in ONBOARDING_TYPES
|
||||
else get_onboarding_type(onboarding_type_or_slug)
|
||||
)
|
||||
return ONBOARDING_TYPES.get(onboarding_type, ONBOARDING_TYPES[DEFAULT_ONBOARDING_TYPE])["restricted"]
|
||||
|
||||
|
||||
return bool(ONBOARDING_TYPES.get(onboarding_type, ONBOARDING_TYPES[DEFAULT_ONBOARDING_TYPE])["restricted"])
|
||||
|
||||
@@ -3,7 +3,7 @@ from ..connection import connect
|
||||
DEFAULT_GAME_TYPE = "jeu_2d"
|
||||
|
||||
|
||||
def get_game_type(slug):
|
||||
def get_game_type(slug: str) -> str:
|
||||
"""Type de jeu choisi à la création (voir create_game.py) : "document"
|
||||
(éditeur générique actuel — quiz/formulaires/contenus, écrans =
|
||||
éléments HTML positionnés en %) ou "jeu_2d" (éditeur de scène dédié —
|
||||
|
||||
@@ -4,7 +4,7 @@ DEFAULT_SCORM_VERSION = "1.2"
|
||||
VALID_SCORM_VERSIONS = ("1.2", "2004")
|
||||
|
||||
|
||||
def get_scorm_version(slug):
|
||||
def get_scorm_version(slug: str) -> str:
|
||||
"""Version SCORM exportée pour ce jeu (voir publish/build_scorm_package.py,
|
||||
publish/scorm_manifest.py) — '1.2' par défaut (compatibilité la plus
|
||||
large) ou '2004' (3rd/4th edition, sépare completion_status/
|
||||
@@ -16,4 +16,4 @@ def get_scorm_version(slug):
|
||||
conn.close()
|
||||
if row is None or row["value"] not in VALID_SCORM_VERSIONS:
|
||||
return DEFAULT_SCORM_VERSION
|
||||
return row["value"]
|
||||
return str(row["value"])
|
||||
|
||||
@@ -3,16 +3,14 @@ from ..connection import connect
|
||||
DEFAULT_SUCCESS_THRESHOLD_PERCENT = 70
|
||||
|
||||
|
||||
def get_success_threshold(slug):
|
||||
def get_success_threshold(slug: str) -> int:
|
||||
"""Seuil de réussite (% de bonnes réponses au quiz) de CE jeu — stocké
|
||||
dans _meta (clé 'success_threshold_percent'), même convention que
|
||||
get_xapi_settings.py. Détermine le statut SCORM/xAPI de fin de partie
|
||||
(reussi/echoue, voir static/js/play/dialogue-box-controller.js::
|
||||
forgeSyncAllQuestsCompletionToScorm) : 70% par défaut si jamais réglé."""
|
||||
conn = connect(slug)
|
||||
row = conn.execute(
|
||||
"SELECT value FROM _meta WHERE key = 'success_threshold_percent'"
|
||||
).fetchone()
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'success_threshold_percent'").fetchone()
|
||||
conn.close()
|
||||
if row is None or row["value"] in (None, ""):
|
||||
return DEFAULT_SUCCESS_THRESHOLD_PERCENT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def get_xapi_settings(slug):
|
||||
def get_xapi_settings(slug: str) -> dict[str, str]:
|
||||
"""Réglages xAPI de CE jeu (voir set_xapi_settings.py — stockés dans
|
||||
_meta, même convention que 'name'/'onboarding_type', voir
|
||||
db/games/game_type_catalog.py) : URL du LRS (Learning Record Store)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from ..constants import PROJECTS_DIR
|
||||
from ..connection import connect
|
||||
from ..constants import PROJECTS_DIR
|
||||
from ..db_path import db_path
|
||||
from .project_slug import build_slug
|
||||
|
||||
|
||||
def list_games(owner_folder):
|
||||
def list_games(owner_folder: str) -> list[dict[str, Any]]:
|
||||
"""Scanne UNIQUEMENT projects/<owner_folder>/ (voir db/games/
|
||||
project_slug.py — owner_folder est l'id du compte, voir
|
||||
db/games/create_game.py) : jamais les autres comptes — voir
|
||||
@@ -17,7 +18,7 @@ def list_games(owner_folder):
|
||||
fonction ne lit plus jamais l'arborescence project/ en entier (avant
|
||||
ce correctif, elle listait TOUS les comptes sans distinction — faille
|
||||
corrigée, voir routes/games/index.py)."""
|
||||
games = []
|
||||
games: list[dict[str, Any]] = []
|
||||
entry_path = os.path.join(PROJECTS_DIR, owner_folder)
|
||||
if not os.path.isdir(entry_path):
|
||||
return games
|
||||
@@ -28,7 +29,7 @@ def list_games(owner_folder):
|
||||
return games
|
||||
|
||||
|
||||
def _append_game(games, slug):
|
||||
def _append_game(games: list[dict[str, Any]], slug: str) -> None:
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'name'").fetchone()
|
||||
conn.close()
|
||||
|
||||
@@ -5,7 +5,7 @@ from ..game_dir import game_dir
|
||||
from .project_slug import build_slug, split_slug
|
||||
|
||||
|
||||
def move_game(old_slug, new_owner_folder):
|
||||
def move_game(old_slug: str, new_owner_folder: str) -> str:
|
||||
"""Renomme le dossier PROPRIÉTAIRE d'un compte — owner_folder est
|
||||
l'id du compte (voir db/games/project_slug.py, routes/games/
|
||||
games_new.py) : ne change donc plus jamais après coup en usage normal,
|
||||
|
||||
@@ -17,11 +17,11 @@ move_game, list_games) savent que le slug encode ce chemin composé — si
|
||||
la convention change un jour, ce fichier est le seul à modifier."""
|
||||
|
||||
|
||||
def build_slug(owner_folder, project_part):
|
||||
def build_slug(owner_folder: str, project_part: str) -> str:
|
||||
return f"{owner_folder}_{project_part}"
|
||||
|
||||
|
||||
def split_slug(slug):
|
||||
def split_slug(slug: str) -> tuple[str, str | None]:
|
||||
"""(owner_folder, project_part) si `slug` est bien composé, sinon
|
||||
(slug, None) — un slug "plat" (créé avant cette convention, pas encore
|
||||
migré par scripts/migrate_flat_project_slugs.py) reste lisible tel
|
||||
|
||||
@@ -2,7 +2,7 @@ from ..connection import connect
|
||||
from .get_scorm_version import VALID_SCORM_VERSIONS
|
||||
|
||||
|
||||
def set_scorm_version(slug, version):
|
||||
def set_scorm_version(slug: str, version: str) -> None:
|
||||
"""Enregistre la version SCORM exportée — voir get_scorm_version.py."""
|
||||
if version not in VALID_SCORM_VERSIONS:
|
||||
raise ValueError("version SCORM invalide : {!r}".format(version))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def set_success_threshold(slug, percent):
|
||||
def set_success_threshold(slug: str, percent: int) -> int:
|
||||
"""Enregistre le seuil de réussite (0-100) — voir get_success_threshold.py."""
|
||||
percent = max(0, min(100, int(percent)))
|
||||
conn = connect(slug)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def set_xapi_settings(slug, endpoint, login, password=None):
|
||||
def set_xapi_settings(slug: str, endpoint: str, login: str, password: str | None = None) -> None:
|
||||
"""Enregistre les réglages xAPI de CE jeu (voir get_xapi_settings.py).
|
||||
`password=None` (champ laissé vide côté formulaire, voir
|
||||
routes/publish/xapi_settings.py) laisse le mot de passe déjà
|
||||
@@ -10,15 +10,9 @@ def set_xapi_settings(slug, endpoint, login, password=None):
|
||||
que la route GET puisse ne jamais renvoyer sa valeur au navigateur
|
||||
(juste un booléen "déjà configuré")."""
|
||||
conn = connect(slug)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_endpoint', ?)", (endpoint or "",)
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_login', ?)", (login or "",)
|
||||
)
|
||||
conn.execute("INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_endpoint', ?)", (endpoint or "",))
|
||||
conn.execute("INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_login', ?)", (login or "",))
|
||||
if password is not None:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_password', ?)", (password,)
|
||||
)
|
||||
conn.execute("INSERT OR REPLACE INTO _meta (key, value) VALUES ('xapi_lrs_password', ?)", (password,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def update_game_name(slug, new_name):
|
||||
def update_game_name(slug: str, new_name: str) -> None:
|
||||
conn = connect(slug)
|
||||
conn.execute("UPDATE _meta SET value = ? WHERE key = 'name'", (new_name,))
|
||||
conn.commit()
|
||||
|
||||
Reference in New Issue
Block a user