""" Commande Django pour agréger les statistiques de pages vues et purger les données détaillées de plus d'un mois. Workflow: 1. Pour chaque jour ayant des PageView de plus de 30 jours, calculer les agrégats et créer/mettre à jour un DailyPageViewSummary. 2. Supprimer les PageView et UserSession de plus de 30 jours. Usage: python manage.py cleanup_analytics python manage.py cleanup_analytics --days 60 python manage.py cleanup_analytics --dry-run """ from datetime import timedelta from django.core.management.base import BaseCommand from django.db.models import Count, Avg, Max, Q from django.db.models.functions import TruncDate from django.utils import timezone from analytics.models import PageView, UserSession, DailyPageViewSummary class Command(BaseCommand): help = ( "Agrège les statistiques quotidiennes de pages vues dans " "DailyPageViewSummary puis purge les enregistrements détaillés " "de PageView et UserSession de plus de N jours (défaut: 30)." ) def add_arguments(self, parser): parser.add_argument( "--days", type=int, default=30, help="Nombre de jours de rétention des données détaillées (défaut: 30).", ) parser.add_argument( "--dry-run", action="store_true", help="Affiche ce qui serait fait sans modifier la base de données.", ) def handle(self, *args, **options): retention_days = options["days"] dry_run = options["dry_run"] cutoff = timezone.now() - timedelta(days=retention_days) cutoff_date = cutoff.date() if dry_run: self.stdout.write(self.style.WARNING("[DRY-RUN] Aucune modification ne sera effectuée.\n")) self.stdout.write( self.style.WARNING( f"\n=== Nettoyage analytics — rétention {retention_days} jours " f"(avant le {cutoff_date}) ===\n" ) ) # ── 1. Agrégation ───────────────────────────────────────────── old_page_views = PageView.objects.filter(timestamp__lt=cutoff) total_old = old_page_views.count() if total_old == 0: self.stdout.write("Aucune PageView à agréger.\n") else: self.stdout.write(f"{total_old} PageView(s) à agréger et purger.\n") self._aggregate_page_views(old_page_views, dry_run) # ── 2. Purge des PageView ───────────────────────────────────── if not dry_run and total_old > 0: deleted_pv, _ = old_page_views.delete() self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_pv} PageView(s) supprimée(s).\n")) # ── 3. Purge des UserSession inactives ──────────────────────── old_sessions = UserSession.objects.filter( last_activity__lt=cutoff, is_active=False, ) total_sessions = old_sessions.count() if total_sessions == 0: self.stdout.write("Aucune UserSession inactive à purger.\n") else: if dry_run: self.stdout.write(f" [DRY-RUN] {total_sessions} UserSession(s) inactives seraient supprimées.\n") else: deleted_us, _ = old_sessions.delete() self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_us} UserSession(s) inactives supprimées.\n")) self.stdout.write(self.style.SUCCESS("\n=== Nettoyage terminé ===\n")) # ────────────────────────────────────────────────────────────────── # Méthodes internes # ────────────────────────────────────────────────────────────────── def _aggregate_page_views(self, queryset, dry_run): """ Calcule les résumés quotidiens à partir du queryset fourni et crée / met à jour les DailyPageViewSummary correspondants. """ days = ( queryset .annotate(day=TruncDate("timestamp")) .values("day") .annotate(count=Count("id")) .order_by("day") ) for entry in days: day = entry["day"] day_qs = queryset.filter(timestamp__date=day) summary_data = self._compute_daily_summary(day, day_qs) if dry_run: self.stdout.write( f" [DRY-RUN] {day} : {summary_data['total_page_views']} vues, " f"{summary_data['unique_users']} utilisateurs uniques" ) else: DailyPageViewSummary.objects.update_or_create( date=day, defaults=summary_data, ) self.stdout.write( self.style.SUCCESS( f" ✓ Résumé {day} : {summary_data['total_page_views']} vues, " f"{summary_data['unique_users']} utilisateurs uniques" ) ) def _compute_daily_summary(self, day, day_qs): """Calcule les métriques agrégées pour un jour donné.""" # Compteurs de base total_page_views = day_qs.count() unique_users = ( day_qs.filter(user__isnull=False) .values("user") .distinct() .count() ) unique_sessions = ( day_qs.exclude(session_key="") .values("session_key") .distinct() .count() ) unique_ips = ( day_qs.filter(ip_address__isnull=False) .values("ip_address") .distinct() .count() ) # Temps de réponse response_stats = day_qs.filter( response_time_ms__isnull=False ).aggregate( avg=Avg("response_time_ms"), mx=Max("response_time_ms"), ) avg_response = response_stats["avg"] max_response = response_stats["mx"] # Top 20 pages top_paths = dict( day_qs.values_list("path") .annotate(cnt=Count("id")) .order_by("-cnt")[:20] ) # Top 20 utilisateurs top_users = dict( day_qs.filter(user__isnull=False) .values_list("user__username") .annotate(cnt=Count("id")) .order_by("-cnt")[:20] ) # Répartition par méthode HTTP method_counts = dict( day_qs.values_list("method") .annotate(cnt=Count("id")) .order_by("-cnt") ) # Sessions démarrées ce jour-là total_sessions_started = UserSession.objects.filter( login_time__date=day ).count() return { "total_page_views": total_page_views, "unique_users": unique_users, "unique_sessions": unique_sessions, "unique_ips": unique_ips, "avg_response_time_ms": round(avg_response, 2) if avg_response else None, "max_response_time_ms": max_response, "top_paths": top_paths, "top_users": top_users, "method_counts": method_counts, "total_sessions_started": total_sessions_started, }