Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

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.

  1. Indexation des routes : parcourt urls.py et enregistre toutes les routes nommées (DetectedUrl).
  2. Scan des templates : cherche les {% url 'xxx' %} / {% url "xxx" %}, vérifie si xxx est une route valide, stocke chaque usage (TemplateTagUsage).
  3. Scan des vues Python : repère les template_name='...' et render(request, '...'), enregistre les couples vue ↔ template (ViewTemplateUsage).
  4. Reporting SQL : tout est rattaché à un ScanReport unique 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

Arborescence projet
your_project/
  analyzer/                         # ou tools/, etc.
    management/
      __init__.py
      commands/
        __init__.py
        analyze_urls.py             # <= notre script
Ne 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.

analyze_urls.py
#! /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

Terminal
# Audit global (FULL PROJECT)
python manage.py analyze_urls

2. Audit ciblé sur une application

Terminal
# Audit uniquement de l'app CV
python manage.py analyze_urls --app CV

3. Audit ciblé sur un dossier spécifique

Terminal
# Audit sur un dossier précis (templates + views Python)
python manage.py analyze_urls \
  --path /opt/ideo-lab/src/apps/CV

4. Exemple Cron (audit récurrent)

Crontab
# 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

analyzer/models.py (extrait)
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)

analyzer/admin.py (extrait)
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=False pour 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.py ou 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 NoReverseMatch en prod.
  • Utiliser les rapports pour nettoyer les routes / templates obsolètes.