From 04f85905eb9c76646a5dceb00d58e4d73202413a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 11:26:32 +0000 Subject: [PATCH] =?UTF-8?q?Ajouter=20architecture=20et=20aper=C3=A7u=20des?= =?UTF-8?q?=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_new/core/routes/admin.py | 87 ++++++++++++++++++- app_new/templates/admin/template_debug.html | 31 ++++++- app_new/templates/admin/template_preview.html | 45 ++++++++++ ...ntervention_location_and_template_audit.py | 7 ++ 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 app_new/templates/admin/template_preview.html diff --git a/app_new/core/routes/admin.py b/app_new/core/routes/admin.py index 855a149..2cd3993 100644 --- a/app_new/core/routes/admin.py +++ b/app_new/core/routes/admin.py @@ -327,6 +327,7 @@ def _template_inventory(): from ..models.audit import TemplateAuditMark app_root = Path(current_app.root_path).resolve() + domain_aliases = {'admin': 'core', 'auth': 'core', 'dashboard': 'core', 'setup_wizard': 'core'} files = [] for path in sorted(app_root.rglob('*.html')): relative = path.relative_to(app_root).as_posix() @@ -350,6 +351,15 @@ def _template_inventory(): 'logical_key': logical_key, 'source': source, 'hash': digest, + 'target_path': ( + f"app_new/{parts[0]}/templates/{logical_key}" + if template_index > 0 + else ( + f"app_new/templates/{logical_key}" + if logical_key in {'base.html'} or logical_key.startswith('errors/') or logical_key.startswith('components/') + else f"app_new/{domain_aliases.get(logical_key.split('/', 1)[0], logical_key.split('/', 1)[0])}/templates/{logical_key}" + ) + ), }) by_key = {} @@ -372,6 +382,34 @@ def _template_inventory(): return files, by_key +TEMPLATE_ARCHITECTURE = [ + { + 'path': 'app_new/templates/', + 'label': 'Socle partagé', + 'description': 'base.html, erreurs, composants et fragments réutilisables. Aucun écran métier ne doit être dupliqué ici.', + 'children': ['base.html', 'errors/', 'components/'], + }, + { + 'path': 'app_new//templates//', + 'label': 'Templates par domaine', + 'description': 'Un domaine métier possède ses écrans canoniques, regroupés sous son propre préfixe.', + 'children': ['core/', 'equipment/', 'buildings/', 'interventions/', 'planning/', 'lots/', 'companies/', 'training/'], + }, + { + 'path': 'app_new//templates/', + 'label': 'Zone de transition', + 'description': 'Ancienne convention actuellement présente dans le projet. À migrer progressivement vers le sous-dossier /.', + 'children': ['detail.html', 'index.html', 'form.html'], + }, + { + 'path': 'app_new/static/', + 'label': 'Ressources visuelles', + 'description': 'CSS, JavaScript et icônes partagés ; pas de logique métier dans les templates.', + 'children': ['css/', 'js/', 'icons/'], + }, +] + + @admin_bp.route('/debug/templates') @login_required @admin_required @@ -396,7 +434,54 @@ def template_debug(): groups = {} for item in visible_files: groups.setdefault(item['logical_key'], []).append(item) - return render_template('admin/template_debug.html', groups=groups, stats=stats, selected_filter=selected_filter) + return render_template('admin/template_debug.html', groups=groups, stats=stats, + selected_filter=selected_filter, template_architecture=TEMPLATE_ARCHITECTURE) + + +@admin_bp.route('/debug/templates/preview') +@login_required +@admin_required +def template_debug_preview(): + """Prévisualise un template autorisé avec un contexte volontairement neutre.""" + from flask import current_app + from jinja2 import ChoiceLoader, FileSystemLoader, TemplateNotFound, ChainableUndefined + + relative_path = (request.args.get('path') or '').strip() + files, _ = _template_inventory() + item = next((candidate for candidate in files if candidate['path'] == relative_path), None) + if not item: + flash('Template introuvable dans l’inventaire.', 'danger') + return redirect(url_for('admin.template_debug')) + + app_root = Path(current_app.root_path).resolve() + source_path = (app_root / relative_path).resolve() + source = source_path.read_text(encoding='utf-8', errors='replace') + rendered = None + render_error = None + try: + parts = Path(relative_path).parts + template_index = parts.index('templates') + inner_name = '/'.join(parts[template_index + 1:]) + if item['source'] == 'module': + module_root = app_root / parts[0] / 'templates' + loader = ChoiceLoader([FileSystemLoader(str(module_root)), current_app.jinja_loader]) + environment = current_app.jinja_env.overlay(loader=loader) + else: + environment = current_app.jinja_env.overlay() + # Un contexte de prévisualisation peut laisser les variables métier + # vides : elles doivent produire un rendu partiel plutôt qu'une 500. + environment.undefined = ChainableUndefined + template = environment.get_template(inner_name) + preview_context = {} + # Reproduire les variables globales Flask (current_user, config, + # request...) sans injecter d'objet métier réel dans la prévisualisation. + current_app.update_template_context(preview_context) + rendered = template.render(**preview_context) + except (TemplateNotFound, Exception) as exc: + render_error = f'{type(exc).__name__}: {exc}' + + return render_template('admin/template_preview.html', template=item, + source=source, rendered=rendered, render_error=render_error) @admin_bp.route('/debug/templates/mark', methods=['POST']) diff --git a/app_new/templates/admin/template_debug.html b/app_new/templates/admin/template_debug.html index a911968..ceea88e 100644 --- a/app_new/templates/admin/template_debug.html +++ b/app_new/templates/admin/template_debug.html @@ -15,6 +15,28 @@ « Utilisé probablement » reflète l’ordre des chargeurs Jinja ; vérifiez les routes avant toute suppression manuelle. +
+
+ Architecture cible des templates + Convention recommandée +
+
+

Le domaine métier est propriétaire de ses écrans. Le dossier global reste réservé au socle partagé ; une seule copie canonique doit être conservée par écran.

+
+ {% for node in template_architecture %} +
+
+ {{ node.path }} + {{ node.label }} +

{{ node.description }}

+
{% for child in node.children %}{{ child }}{% endfor %}
+
+
+ {% endfor %} +
+
+
+
{% for label, value, color in [('Templates', stats.total, 'primary'), ('Groupes doublons', stats.duplicate_groups, 'warning'), ('Identiques', stats.identical_groups, 'success'), ('Divergents', stats.divergent_groups, 'danger'), ('Marqués obsolètes', stats.obsolete, 'secondary')] %}
{{ label }}
{{ value }}
@@ -31,18 +53,22 @@ {% for logical_key, templates in groups|dictsort %}
- {{ logical_key }} +
+ {{ logical_key }} + Cible : {{ templates[0].target_path }} +
{% if templates[0].duplicate_count > 1 %} {{ templates[0].duplicate_count }} copies — {{ templates[0].comparison }} {% else %}unique{% endif %}
- + {% for template in templates %} + +
FichierSourceÉtat estiméEmpreinteDécision
FichierArchitecture cibleSourceÉtat estiméEmpreinteActionsDécision
{{ template.path }}{{ template.target_path }} {{ template.source }} {% if template.mark and template.mark.status == 'obsolete' %}Obsolète @@ -50,6 +76,7 @@ {% else %}Masqué probablement{% endif %} {{ template.hash[:10] }} Prévisualiser
diff --git a/app_new/templates/admin/template_preview.html b/app_new/templates/admin/template_preview.html new file mode 100644 index 0000000..2b899b0 --- /dev/null +++ b/app_new/templates/admin/template_preview.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Prévisualisation {{ template.path }} — Administration{% endblock %} +{% block content %} +
+
+
+

Prévisualisation du template

+ {{ template.path }} +
+ Retour à l’audit +
+ + {% if render_error %} +
+ Rendu incomplet +

Ce template attend probablement un contexte métier (objet, identifiant ou endpoint). Le fichier reste consultable ci-dessous.

+ {{ render_error }} +
+ {% endif %} + +
+
+
+
+ Rendu visuel + sandboxé +
+
+ {% if rendered is not none %} + + {% else %} +
Aucun rendu exploitable sans contexte métier.
+ {% endif %} +
+
+
+
+
+
Source
+
{{ source }}
+
+
+
+
+{% endblock %} diff --git a/tests/integration/test_intervention_location_and_template_audit.py b/tests/integration/test_intervention_location_and_template_audit.py index 71f5b60..c398f77 100644 --- a/tests/integration/test_intervention_location_and_template_audit.py +++ b/tests/integration/test_intervention_location_and_template_audit.py @@ -5,6 +5,13 @@ def test_template_audit_can_mark_and_restore_without_deleting(authenticated_clie response = authenticated_client.get('/admin/debug/templates?filter=all') assert response.status_code == 200 assert 'Audit des templates' in response.get_data(as_text=True) + assert 'Architecture cible des templates' in response.get_data(as_text=True) + assert 'app_new/chatbot/templates/chatbot/index.html' in response.get_data(as_text=True) + + response = authenticated_client.get('/admin/debug/templates/preview?path=templates%2Fadmin%2Ftemplate_debug.html') + assert response.status_code == 200 + assert 'Prévisualisation du template' in response.get_data(as_text=True) + assert 'sandbox' in response.get_data(as_text=True) response = authenticated_client.post('/admin/debug/templates/mark', data={ 'path': template_path,