Django URL & Template Auditor – analyze_urls.py
Objectif : auditer la cohérence de ton projet Django : URLs, templates, vues Python et stocker tout ça en SQL pour analyse dans l’admin.
- Indexation des routes : parcourt
urls.pyet enregistre toutes les routes nommées (DetectedUrl). - Scan des templates : cherche les
{% url 'xxx' %}/{% url "xxx" %}, vérifie sixxxest une route valide, stocke chaque usage (TemplateTagUsage). - Scan des vues Python : repère les
template_name='...'etrender(request, '...'), enregistre les couples vue ↔ template (ViewTemplateUsage). - Reporting SQL : tout est rattaché à un
ScanReportunique avec stats, erreurs, durée.
Cible : détecter les
NoReverseMatch potentiels, les routes obsolètes, les templates orphelins, et cartographier ton graphe URLs <=> Templates <=> Vues.Étape 1 – Installation du management command
On installe analyze_urls.py comme management command dans une app existante (par exemple analyzer, tools ou similaire).
1. Arborescence Django
your_project/
analyzer/ # ou tools/, etc.
management/
__init__.py
commands/
__init__.py
analyze_urls.py # <= notre scriptNe pas oublier les fichiers
__init__.py dans management/ et commands/, sinon Django ne verra pas la commande.2. Contenu de analyze_urls.py
Voici la version actuelle de ton outil (DJANGO AUDITOR V4.0 – SQL EDITION), adaptée pour être appelée comme python manage.py analyze_urls.
#! /usr/bin/python3.1
# -*- coding: utf-8 -*-
import os
import re
import sys
import time
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from django.urls import get_resolver, URLPattern, URLResolver
from django.apps import apps
from django.db import transaction
# Import des modèles (Ajustez 'analyzer' selon le nom de votre app)
from analyzer.models import ScanReport, DetectedUrl, AnalyzedTemplate, TemplateTagUsage, ViewTemplateUsage
class Command(BaseCommand):
help = 'Audit complet : URLs, Templates, Vues et stockage SQL.'
def add_arguments(self, parser):
parser.add_argument('--app', type=str, help='Cibler une application')
parser.add_argument('--path', type=str, help='Cibler un dossier')
def print_progress(self, iteration, total, prefix='', suffix='', decimals=1, length=30, fill='█'):
if total == 0: return
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filled_length = int(length * iteration // total)
bar = fill * filled_length + '-' * (length - filled_length)
sys.stdout.write(f'\r{prefix} |{self.style.HTTP_INFO(bar)}| {percent}% {suffix}')
sys.stdout.flush()
@transaction.atomic
def handle(self, *args, **options):
start_time = time.time()
self.stdout.write(self.style.MIGRATE_HEADING("\n📊 DJANGO AUDITOR V4.0 (SQL EDITION)"))
# 1. CRÉATION DU RAPPORT
scope = options['app'] or options['path'] or "FULL PROJECT"
report = ScanReport.objects.create(scope_path=scope)
self.stdout.write(f"📝 Rapport ID #{report.id} initialisé.")
# --- PHASE 2 : INDEXATION DES URLs ---
self.stdout.write("1️⃣ Indexation des routes urls.py...")
valid_url_names = set()
def extract(patterns, ns=None):
for p in patterns:
if isinstance(p, URLPattern) and p.name:
full_name = f"{ns}:{p.name}" if ns else p.name
pattern_desc = str(p.pattern)
# Sauvegarde en base
DetectedUrl.objects.create(
scan=report,
name=full_name,
pattern=pattern_desc,
namespace=ns
)
valid_url_names.add(full_name)
elif isinstance(p, URLResolver):
new_ns = f"{ns}:{p.namespace}" if ns and p.namespace else (p.namespace or ns)
extract(p.url_patterns, new_ns)
extract(get_resolver().url_patterns)
report.total_urls_found = len(valid_url_names)
self.stdout.write(self.style.SUCCESS(f" -> {len(valid_url_names)} routes archivées."))
# --- PHASE 3 : DÉTECTION DES FICHIERS ---
target_dirs = set()
if options['app']:
target_dirs.add(apps.get_app_config(options['app']).path)
elif options['path']:
target_dirs.add(options['path'])
else:
base = str(settings.BASE_DIR)
for tpl in settings.TEMPLATES:
for d in tpl.get('DIRS', []): target_dirs.add(str(d))
for conf in apps.get_app_configs():
if conf.path.startswith(base) and 'site-packages' not in conf.path:
target_dirs.add(conf.path)
tpl_files = []
py_files = []
for d in target_dirs:
for root, dirs, files in os.walk(d):
dirs[:] = [d for d in dirs if d not in ['__pycache__', 'migrations', 'venv', '.git', 'node_modules']]
for f in files:
if f.endswith('.html'): tpl_files.append(os.path.join(root, f))
if f.endswith('.py'): py_files.append(os.path.join(root, f))
# --- PHASE 4 : SCAN TEMPLATES (Cross-Reference) ---
self.stdout.write(f"2️⃣ Analyse des Templates ({len(tpl_files)} fichiers)...")
url_regex = re.compile(r"{%\s*url\s+(?:'([^']+)'|\"([^\"]+)\")")
for i, fpath in enumerate(tpl_files):
rel_path = os.path.relpath(fpath, settings.BASE_DIR)
self.print_progress(i, len(tpl_files), prefix='Scan Tpl:', suffix=rel_path[:20])
# Création objet Template en BDD
tpl_obj = AnalyzedTemplate.objects.create(scan=report, file_path=rel_path)
link_count = 0
try:
with open(fpath, 'r', encoding='utf-8') as f:
for l_idx, line in enumerate(f):
for m in url_regex.findall(line):
u_name = m[0] or m[1]
# Ignorer variables dynamiques
if " " in u_name or "{{" in u_name: continue
is_valid = u_name in valid_url_names
err_msg = None if is_valid else "NoReverseMatch: URL introuvable"
if not is_valid:
report.total_errors += 1
# Affichage console erreur
sys.stdout.write('\r' + ' '*80 + '\r')
self.stdout.write(self.style.ERROR(f" ❌ {rel_path}:{l_idx+1} > {u_name}"))
# Sauvegarde du Tag
TemplateTagUsage.objects.create(
template=tpl_obj,
line_number=l_idx + 1,
url_name_used=u_name,
is_valid=is_valid,
error_message=err_msg
)
link_count += 1
except Exception: pass
tpl_obj.total_links = link_count
tpl_obj.save()
# --- PHASE 5 : SCAN VUES (Qui appelle quel template ?) ---
self.stdout.write(f"\n3️⃣ Analyse des Vues Python ({len(py_files)} fichiers)...")
# Regex pour trouver: template_name = 'xxx.html' OU render(request, 'xxx.html')
view_regex = re.compile(r"(?:template_name\s*=\s*|render\s*\(\s*request\s*,\s*)(?:'([^']+)'|\"([^\"]+)\")")
for i, fpath in enumerate(py_files):
rel_path = os.path.relpath(fpath, settings.BASE_DIR)
self.print_progress(i, len(py_files), prefix='Scan Py:', suffix=rel_path[:20])
try:
with open(fpath, 'r', encoding='utf-8') as f:
for l_idx, line in enumerate(f):
for m in view_regex.findall(line):
tpl_name = m[0] or m[1]
if tpl_name.endswith('.html'):
ViewTemplateUsage.objects.create(
scan=report,
view_file=rel_path,
view_line=l_idx + 1,
template_name=tpl_name
)
except: pass
# --- FINALISATION ---
report.duration_seconds = time.time() - start_time
report.total_templates_scanned = len(tpl_files)
report.total_views_scanned = len(py_files)
report.save()
print("\n" + "="*40)
self.stdout.write(self.style.SUCCESS(f"✅ Audit terminé en {report.duration_seconds:.2f}s"))
self.stdout.write(f"📊 Données sauvegardées dans le Scan #{report.id}")
self.stdout.write(f" - URLs indexées : {report.total_urls_found}")
self.stdout.write(f" - Templates liés : {report.total_templates_scanned}")
self.stdout.write(f" - Erreurs détectées : {report.total_errors}")Étape 2 – Utilisation & scénarios types
1. Audit complet du projet
# Audit global (FULL PROJECT)
python manage.py analyze_urls2. Audit ciblé sur une application
# Audit uniquement de l'app CV
python manage.py analyze_urls --app CV3. Audit ciblé sur un dossier spécifique
# Audit sur un dossier précis (templates + views Python)
python manage.py analyze_urls \
--path /opt/ideo-lab/src/apps/CV4. Exemple Cron (audit récurrent)
# Tous les jours à 3h du matin, audit global
0 3 * * * /opt/ideo-lab/venv/bin/python \
/opt/ideo-lab/manage.py analyze_urls \
>> /var/log/ideo-lab/analyze_urls.log 2>&1Étape 3 – Data Model : app analyzer
Le cron analyze_urls s’appuie sur un sous-ensemble des modèles de l’app analyzer pour stocker ses résultats : ScanReport, DetectedUrl, AnalyzedTemplate, TemplateTagUsage, ViewTemplateUsage. :contentReference[oaicite:1]{index=1}
1. Modèles utilisés par analyze_urls
from django.db import models
class ScanReport(models.Model):
"""Rapport global d'une exécution de scan"""
created_at = models.DateTimeField(auto_now_add=True)
duration_seconds = models.FloatField(default=0.0)
scope_path = models.CharField(max_length=255, help_text="Dossier ou App scannée")
# Stats
total_urls_found = models.IntegerField(default=0)
total_templates_scanned = models.IntegerField(default=0)
total_views_scanned = models.IntegerField(default=0)
total_errors = models.IntegerField(default=0)
class Meta:
verbose_name = "ANALYZER - Scan Report **"
def __str__(self):
return f"Scan #{self.id} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
class DetectedUrl(models.Model):
"""Une URL valide trouvée dans urls.py"""
scan = models.ForeignKey(ScanReport, on_delete=models.CASCADE, related_name='urls')
name = models.CharField(max_length=150, db_index=True) # ex: 'blog:detail'
pattern = models.CharField(max_length=250) # ex: 'articles/<int:pk>/'
namespace = models.CharField(max_length=100, blank=True, null=True)
class Meta:
verbose_name = "ANALYZER - Urls Detected **"
class AnalyzedTemplate(models.Model):
"""Un fichier HTML scanné"""
scan = models.ForeignKey(ScanReport, on_delete=models.CASCADE, related_name='templates')
file_path = models.CharField(max_length=150) # Chemin relatif
total_links = models.IntegerField(default=0)
class Meta:
verbose_name = "ANALYZER - Analyzed Templates **"
class TemplateTagUsage(models.Model):
"""Une utilisation de {% url %} dans un template"""
template = models.ForeignKey(AnalyzedTemplate, on_delete=models.CASCADE, related_name='tags')
line_number = models.IntegerField()
url_name_used = models.CharField(max_length=150)
is_valid = models.BooleanField(default=False)
error_message = models.CharField(max_length=150, blank=True, null=True)
class Meta:
verbose_name = "ANALYZER - Template Urls Found **"
class ViewTemplateUsage(models.Model):
"""Lien détecté entre une Vue Python et un Template"""
scan = models.ForeignKey(ScanReport, on_delete=models.CASCADE, related_name='view_usages')
view_file = models.CharField(max_length=150) # ex: core/views.py
view_line = models.IntegerField()
template_name = models.CharField(max_length=150) # ex: blog/detail.html
class Meta:
verbose_name = "ANALYZER - Template-View referenced **"Ces modèles cohabitent avec les autres modèles Analyzer existants (
AnalyzerRun, AnalyzerIssue, etc.) sans contraintes uniques additionnelles : on peut lancer autant de scans qu’on veut, chaque exécution crée un nouveau ScanReport.Étape 4 – Django Admin & exploitation
L’admin de l’app analyzer permet de naviguer dans :
- Les ScanReport : chaque exécution du cron
analyze_urls. - Les DetectedUrl : toutes les routes indexées.
- Les AnalyzedTemplate : templates scannés avec nombre de liens.
- Les TemplateTagUsage : chaque usage de
{% url %}(OK ou erreur). - Les ViewTemplateUsage : liens Vue Python ↔ Template HTML.
1. Contenu de analyzer/admin.py (extrait URL Auditor)
from django.contrib import admin
from analyzer.models import (
ScanReport,
DetectedUrl,
AnalyzedTemplate,
TemplateTagUsage,
ViewTemplateUsage,
)
@admin.register(ScanReport)
class ScanReportAdmin(admin.ModelAdmin):
list_display = ("id", "created_at", "scope_path",
"total_urls_found", "total_templates_scanned",
"total_views_scanned", "total_errors")
list_filter = ("created_at",)
search_fields = ("scope_path",)
date_hierarchy = "created_at"
ordering = ("-created_at",)
@admin.register(DetectedUrl)
class DetectedUrlAdmin(admin.ModelAdmin):
list_display = ("name", "pattern", "namespace", "scan")
list_filter = ("namespace", "scan__created_at")
search_fields = ("name", "pattern", "namespace")
@admin.register(AnalyzedTemplate)
class AnalyzedTemplateAdmin(admin.ModelAdmin):
list_display = ("file_path", "scan", "total_links")
list_filter = ("scan__created_at",)
search_fields = ("file_path",)
@admin.register(TemplateTagUsage)
class TemplateTagUsageAdmin(admin.ModelAdmin):
list_display = ("template", "line_number", "url_name_used", "is_valid")
list_filter = ("is_valid", "template__scan__created_at")
search_fields = ("url_name_used", "template__file_path")
@admin.register(ViewTemplateUsage)
class ViewTemplateUsageAdmin(admin.ModelAdmin):
list_display = ("scan", "view_file", "view_line", "template_name")
list_filter = ("scan__created_at",)
search_fields = ("view_file", "template_name")2. Utilisation dans l’admin
- Filtrer les ScanReport par date / scope pour comparer les audits.
- Lister les TemplateTagUsage avec
is_valid=Falsepour corriger les URLs cassées. - Sur un AnalyzedTemplate, voir tous les tags {% url %} associés.
- Depuis ViewTemplateUsage, voir quelles vues consomment un template donné.
Étape 5 – Exploitation & tuning
1. Ce qui est stocké en SQL (rappel)
- ScanReport : un rapport par exécution (scope, durée, nb d’URLs, nb de templates, nb d’erreurs).
- DetectedUrl : toutes les routes indexées (nom complet, pattern, namespace).
- AnalyzedTemplate : chaque template scanné (chemin relatif, nb de liens).
- TemplateTagUsage : chaque usage de
{% url %}(ligne, url_name, is_valid, message d’erreur). - ViewTemplateUsage : chaque lien vue Python ↔ template HTML.
Admin Django : une fois l’app
analyzer déclarée dans INSTALLED_APPS et les modèles enregistrés dans l’admin, tu disposes d’un vrai “Google Analytics des URLs / Templates” sur Ideo-Lab.2. Bonnes pratiques d’exploitation
- Lancer l’audit après de gros refactors de
urls.pyou de renommage de templates. - Mettre un audit quotidien en Cron sur un environnement de recette/staging.
- Surveiller les TemplateTagUsage.is_valid = False pour anticiper les
NoReverseMatchen prod. - Utiliser les rapports pour nettoyer les routes / templates obsolètes.
