Files
williamandClaude Sonnet 5 c57420c8c9 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>
2026-09-15 16:06:15 +02:00

217 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Any
from flask import abort, g, render_template, request, url_for
import auth
import db
import screens
def render_scene_edit(slug: str, screen_id: int, game: dict[str, Any]) -> str:
"""Vue de l'éditeur de scène 2D — appelée par routes/screens/
screen_edit.py (le dispatcher, selon screen["kind"]), PAS une route
Flask elle-même : même URL/endpoint "screen_edit" pour les deux types
d'écran. Réutilise TEL QUEL tout ce qui est déjà générique côté logique
de flow/animation/variables/événements (voir screens/screens_repo/
ensure_schema.py — le type d'écran est une propriété PAR ÉCRAN) —
seuls les objets de scène et la palette de nœuds proposée diffèrent de
l'éditeur document."""
screen = screens.get_screen(slug, screen_id)
if not screen:
abort(404)
objects = screens.list_scene_objects(slug, screen_id)
for o in objects:
o["rendered_html"] = screens.render_scene_object(o)
# "Plein écran" (demande explicite : la boîte à quiz doit occuper
# tout le canevas de CET éditeur, pas seulement au jeu, dès que ce
# réglage est coché) — calculé pour TOUS les objets (pas
# seulement le sélectionné) : plusieurs quiz plein écran sur le
# même écran resteraient rares/déconseillés, mais rien n'empêche
# techniquement d'en poser plusieurs.
o["quiz_fullscreen"] = o["kind"] == "quiz_box" and screens.resolve_quiz_box_config(o)["fullscreen"]
# La "notion de caméra disparaît" (demande explicite) : le cadre "🎥
# Champ de la caméra" n'a plus aucun sens dès qu'un quiz plein écran
# est posé sur cet écran, caméra ou pas — masqué dans ce cas (voir
# templates/scene_edit.html).
any_quiz_box_fullscreen = any(o["quiz_fullscreen"] for o in objects)
# "Monde" (voir screens.resolve_scene_world_size, réutilisé aussi par
# la miniature de "Mes écrans", routes/games/game_dashboard.py) : un
# "fond" posé à sa taille RÉELLE (souvent plus grande que la scène,
# voir add_scene_object.py) rend visible/joignable, dans CET éditeur,
# tout ce qui dépasse la scène nominale plutôt que de le rogner (le
# glisser-déposer le permet déjà depuis le fix précédent — sans
# agrandir le canevas ici, cette partie restait invisible/injoignable
# à la souris). Le rectangle "🎥 Champ de la caméra" (voir
# templates/scene_edit.html) marque ensuite où s'arrête la scène
# nominale À L'INTÉRIEUR de ce monde plus grand.
world_width, world_height = screens.resolve_scene_world_size(screen, objects)
selected_id = request.args.get("selected", type=int)
personnage_data = None
personnage_commands = None
personnage_role = None
collision_settings = None
dialogue_box_style = None
quiz_box_config = None
selected_obj = next((o for o in objects if o["id"] == selected_id), None) if selected_id else None
_UI_WIDGET_KINDS = ("dialogue_box", "quiz_box", "score_widget")
if selected_obj:
if selected_obj["kind"] not in _UI_WIDGET_KINDS:
collision_settings = screens.resolve_collision_settings(selected_obj)
if selected_obj["kind"] == "personnage":
personnage_data = screens.resolve_personnage_data(selected_obj)
personnage_commands = screens.resolve_personnage_commands(selected_obj)
personnage_role = screens.resolve_personnage_role(selected_obj)
elif selected_obj["kind"] in _UI_WIDGET_KINDS:
dialogue_box_style = screens.resolve_dialogue_box_style(selected_obj)
if selected_obj["kind"] == "quiz_box":
# Plein écran/minuteur/modèle (demande explicite : quiz RH
# autonome) — voir screens/rendering/quiz_box_config.py,
# PANNEAU distinct de "🎨 Style" ci-dessus (couleurs/police,
# partagé avec dialogue_box/score_widget).
quiz_box_config = screens.resolve_quiz_box_config(selected_obj)
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
object_labels = {o["id"]: (o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor")) for o in objects}
element_animations_map = {
o["id"]: screens.resolve_personnage_animations(o) for o in objects if o["kind"] == "personnage"
}
flow_blocks = screens.list_flow_blocks(slug, screen_id)
flow_nodes = screens.list_flow_nodes(slug, screen_id)
flow_edges = screens.list_flow_edges(slug, screen_id)
animation_clips = screens.list_animation_clips(slug, screen_id)
global_variables = db.list_global_variables(slug)
custom_events = db.list_custom_events(slug)
custom_event_usages = screens.list_custom_event_usages(slug)
custom_events_map = {e["id"]: {"name": e["name"]} for e in custom_events}
all_screens = screens.list_screens(slug)
screens_map = {s["id"]: s["name"] for s in all_screens}
blocks_view = screens.blocks_view(flow_blocks, flow_nodes, object_labels)
# Sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) : réservés au
# rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan
# "Bibliothèque de sprites animaux CraftPix").
sprite_library = screens.SPRITE_LIBRARY if g.current_user["role"] == "admin" else screens.PUBLIC_SPRITE_LIBRARY
sprite_library_families = screens.sprite_gallery_families(sprite_library)
# Images de fond CraftPix (screens.BACKGROUND_LIBRARY) : même règle
# d'accès que les sprites animaux — réservées au rôle admin.
background_library = screens.BACKGROUND_LIBRARY if g.current_user["role"] == "admin" else {}
# "Mes assets" (voir auth/user_assets_dir.py) : par COMPTE, jamais
# filtré par rôle contrairement à sprite_library/background_library
# ci-dessus (rien à voir avec le catalogue Forge réservé à l'admin).
all_user_assets = auth.list_user_assets(g.current_user["id"])
# Sélecteur "asset_url" des actions "son"/"vidéo" (voir static/js/
# scenes/collision-rules-editor.js) — TOUS les assets (pas seulement
# les images) : url_for calculée ici (pas possible côté JS), le reste
# (id, nom) sert juste à l'affichage.
for a in all_user_assets:
a["url"] = url_for("serve_user_asset", user_id=a["user_id"], filename=a["filename"])
a["kind"] = auth.user_asset_kind(a["filename"])
user_assets_options = [
{"id": a["id"], "url": a["url"], "name": a["original_name"] or a["filename"], "kind": a["kind"]}
for a in all_user_assets
]
# "Mes assets" : sous-sections Fonds/Décors/Sons/Vidéos (demande
# explicite : "voir, utiliser ou supprimer des son, image et vidéo",
# PUIS "un bouton d'import séparé pour les images de fond et les
# objets" — sans ça, un clic posait TOUJOURS l'image en kind="decor",
# jamais en "fond", voir screens/scenes/add_scene_object.py). Une
# image importée AVANT ce réglage (scene_kind NULL) retombe sur
# "decor", comportement inchangé. Les sons/vidéos ne posent rien sur
# la scène (ils s'utilisent depuis le sélecteur de fichier des
# actions "son"/"vidéo", voir user_assets_options ci-dessus) — les
# quatre listent néanmoins un bouton "Supprimer" (voir
# deleteUserAsset(), static/js/scenes/scene-editor.js).
user_assets_fond = [a for a in all_user_assets if a["kind"] == "image" and a["scene_kind"] == "fond"]
user_assets_decor = [a for a in all_user_assets if a["kind"] == "image" and a["scene_kind"] != "fond"]
user_assets_audio = [a for a in all_user_assets if a["kind"] == "audio"]
user_assets_video = [a for a in all_user_assets if a["kind"] == "video"]
return render_template(
"scene_edit.html",
game=game,
screen=screen,
all_screens=all_screens,
world_width=world_width,
world_height=world_height,
any_quiz_box_fullscreen=any_quiz_box_fullscreen,
objects=objects,
selected_id=selected_id,
personnage_data=personnage_data,
personnage_commands=personnage_commands or screens.DEFAULT_PERSONNAGE_COMMANDS,
personnage_role=personnage_role or screens.DEFAULT_PERSONNAGE_ROLE,
personnage_role_choices=screens.PERSONNAGE_ROLES,
personnage_role_labels=screens.PERSONNAGE_ROLE_LABELS,
collision_settings=collision_settings or screens.DEFAULT_COLLISION_SETTINGS,
dialogue_box_style=dialogue_box_style or screens.DEFAULT_DIALOGUE_BOX_STYLE,
dialogue_box_font_weight_choices=screens.FONT_WEIGHT_CHOICES,
quiz_box_config=quiz_box_config or screens.DEFAULT_QUIZ_BOX_CONFIG,
quiz_box_timer_modes=screens.TIMER_MODES,
quiz_box_dialog_templates=screens.QUIZ_BOX_DIALOG_TEMPLATES,
quiz_box_dialog_template_labels=screens.QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
quiz_box_page_templates=screens.QUIZ_BOX_PAGE_TEMPLATES,
quiz_box_page_template_labels=screens.QUIZ_BOX_PAGE_TEMPLATE_LABELS,
sprite_library=sprite_library,
sprite_library_families=sprite_library_families,
sprite_library_json=db.json_for_script(sprite_library),
background_library=background_library,
user_assets_fond=user_assets_fond,
user_assets_decor=user_assets_decor,
user_assets_audio=user_assets_audio,
user_assets_video=user_assets_video,
element_animations_map_json=db.json_for_script(element_animations_map),
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
element_orientation_labels_json=db.json_for_script(screens.ELEMENT_ORIENTATION_LABELS),
element_visibility_values=screens.ELEMENT_VISIBILITY_VALUES,
element_visibility_labels_json=db.json_for_script(screens.ELEMENT_VISIBILITY_LABELS),
trigger_events=screens.TRIGGER_EVENTS_2D,
action_type_labels=screens.ACTION_TYPE_LABELS_2D,
object_action_properties=screens.OBJECT_ACTION_PROPERTIES,
element_action_property_labels_json=db.json_for_script(dict(screens.OBJECT_ACTION_PROPERTIES)),
condition_operators=screens.CONDITION_OPERATORS,
data_operations=screens.DATA_OPERATIONS,
data_operation_labels=screens.DATA_OPERATION_LABELS,
score_status_choices=db.SCORE_STATUS_CHOICES,
score_status_labels_json=db.json_for_script(db.SCORE_STATUS_LABELS),
data_operation_labels_json=db.json_for_script(screens.DATA_OPERATION_LABELS),
condition_operator_labels_json=db.json_for_script(screens.CONDITION_OPERATOR_LABELS),
action_type_labels_json=db.json_for_script(screens.ACTION_TYPE_LABELS_2D),
elements_labels_json=db.json_for_script(object_labels),
screens_map_json=db.json_for_script(screens_map),
flow_nodes=flow_nodes,
flow_edges=flow_edges,
flow_nodes_json=db.json_for_script(flow_nodes),
flow_edges_json=db.json_for_script(flow_edges),
flow_blocks_json=db.json_for_script(flow_blocks),
blocks_view=blocks_view,
animation_clips=animation_clips,
animation_clips_json=db.json_for_script(animation_clips),
animate_css_catalog=screens.ANIMATE_CSS_CATALOG,
animate_css_labels_json=db.json_for_script(screens.ANIMATE_CSS_LABELS),
global_variables=global_variables,
global_variable_types=db.GLOBAL_VARIABLE_TYPES,
custom_events=custom_events,
custom_event_usages=custom_event_usages,
custom_events_map_json=db.json_for_script(custom_events_map),
definitions=db.list_definitions(slug),
definitions_json=db.json_for_script(
{str(d["id"]): screens.data_definition_options(slug, d["id"]) for d in db.list_definitions(slug)}
),
definitions_names_json=db.json_for_script({d["id"]: d["name"] for d in db.list_definitions(slug)}),
# "qui parle" dans un dialogue de déclencheur (voir "ℹ️
# Informations", static/js/triggers/trigger-editor.js) : les noms
# donnés à N'IMPORTE QUEL objet de scène (personnage, décor,
# fond), toutes scènes du jeu confondues.
scene_object_names_json=db.json_for_script(screens.list_named_scene_object_names(slug)),
# Nouveaux déclencheurs/actions (voir screens/rendering/
# collision_rules.py, screens/rendering/screen_triggers.py) :
# vocabulaire des nouvelles actions feuilles + assets disponibles
# pour "son"/"vidéo", posés en JS comme le reste ci-dessus.
surbrillance_labels_json=db.json_for_script(screens.SURBRILLANCE_LABELS),
video_mode_labels_json=db.json_for_script(screens.VIDEO_MODE_LABELS),
user_assets_options_json=db.json_for_script(user_assets_options),
screen_triggers=screens.resolve_screen_triggers(screen),
)