From e44e31bfb95f47fbe1442717b2df9ec97c6fda29 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 22:54:27 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20tableau=20de=20bord=20statistiques?= =?UTF-8?q?=20pour=20les=20superadministrateurs,=20avec=20cache=20et=20gra?= =?UTF-8?q?phiques=20JSON,=20et=20mise=20=C3=A0=20jour=20des=20routes=20po?= =?UTF-8?q?ur=20inclure=20la=20vue=20correspondante.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- home/urls.py | 2 + home/views.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/home/urls.py b/home/urls.py index a1ea3c1..80f5069 100644 --- a/home/urls.py +++ b/home/urls.py @@ -5,4 +5,6 @@ app_name = 'home' urlpatterns = [ path('', views.home, name='home'), path('premium/', views.premium, name='premium'), + # Tableau de bord statistiques (réservé superadministrateurs) + path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'), ] \ No newline at end of file diff --git a/home/views.py b/home/views.py index 5bd3838..9702e82 100644 --- a/home/views.py +++ b/home/views.py @@ -1,5 +1,13 @@ from django.shortcuts import render, get_object_or_404 -from courses.models import Course +from django.contrib.auth.decorators import user_passes_test +from django.utils import timezone +from django.db.models import Count +from django.views.decorators.cache import cache_page +from django.contrib.auth.models import User +from courses.models import Course, Lesson +from blog.models import Post +from progression.models import Progression +import json def home(request): courses = Course.objects.order_by('-created_at')[:6] @@ -9,3 +17,146 @@ def premium(request, course_id): """Landing page présentant les avantages du Premium.""" course = get_object_or_404(Course, pk=course_id) return render(request, 'premium.html', {'course': course}) + + +# 15 minutes de cache sur la page complète (sauf si décochée plus tard pour des blocs en direct) +@user_passes_test(lambda u: u.is_superuser) +@cache_page(60 * 15) +def stats_dashboard(request): + """ Tableau de bord statistiques réservé aux superadministrateurs. + Périodes supportées: 7, 30, 90, 180 jours (GET param 'p'). """ + + # Période + period_options = [7, 30, 90, 180] + try: + p = int(request.GET.get('p', 30)) + except ValueError: + p = 30 + if p not in period_options: + p = 30 + + now = timezone.now() + start_dt = now - timezone.timedelta(days=p-1) # inclut aujourd'hui + + # Utilisateurs + total_users = User.objects.count() + new_users_qs = User.objects.filter(date_joined__date__gte=start_dt.date(), date_joined__date__lte=now.date()) + # Séries quotidiennes nouveaux utilisateurs + new_users_by_day = ( + new_users_qs + .extra(select={'day': "date(date_joined)"}) + .values('day') + .annotate(c=Count('id')) + ) + + # Activité approximée via Progression mise à jour + active_users_qs = ( + Progression.objects.filter(updated_at__date__gte=start_dt.date(), updated_at__date__lte=now.date()) + .values('user').distinct() + ) + active_users_count = active_users_qs.count() + + # Cours + total_courses = Course.objects.count() + total_courses_enabled = Course.objects.filter(enable=True).count() + new_courses_by_day = ( + Course.objects.filter(created_at__date__gte=start_dt.date(), created_at__date__lte=now.date()) + .extra(select={'day': "date(created_at)"}) + .values('day').annotate(c=Count('id')) + ) + + # Leçons + total_lessons = Lesson.objects.count() + + # Achèvements de leçons (via table de liaison M2M) + through = Progression.completed_lessons.through + lesson_completions_by_day = ( + through.objects.filter( + progression__updated_at__date__gte=start_dt.date(), + progression__updated_at__date__lte=now.date(), + ) + .extra(select={'day': "date(progression_updated_at)"}) if 'progression_updated_at' in [f.name for f in through._meta.fields] + else through.objects.extra(select={'day': "date(created_at)"}) # fallback si champs créé n'existe pas + ) + # Si la table M2M n'a pas de timestamps, on utilisera updated_at de Progression pour l'activité par jour + # donc on refait une série quotidienne d'activité progression + progress_activity_by_day = ( + Progression.objects.filter(updated_at__date__gte=start_dt.date(), updated_at__date__lte=now.date()) + .extra(select={'day': "date(updated_at)"}) + .values('day').annotate(c=Count('id')) + ) + + # Blog + total_posts = Post.objects.count() + new_posts_by_day = ( + Post.objects.filter(created_at__date__gte=start_dt.date(), created_at__date__lte=now.date()) + .extra(select={'day': "date(created_at)"}) + .values('day').annotate(c=Count('id')) + ) + + # Revenus/Paiements & Technique (placeholders faute de sources) + revenus_disponibles = False + technique_disponible = False + + # Helper pour avoir toutes les dates de la période et remplir les trous + def build_series_dict(qs, date_key='day', count_key='c'): + counts = {str(item[date_key]): item[count_key] for item in qs} + days = [] + values = [] + d = start_dt.date() + while d <= now.date(): + key = str(d) + days.append(key) + values.append(counts.get(key, 0)) + d += timezone.timedelta(days=1) + return days, values + + days_users, values_new_users = build_series_dict(new_users_by_day) + days_courses, values_new_courses = build_series_dict(new_courses_by_day) + days_posts, values_new_posts = build_series_dict(new_posts_by_day) + days_activity, values_activity = build_series_dict(progress_activity_by_day) + + # Tables simples (jour, valeur) + new_users_table = list(zip(days_users, values_new_users)) + new_courses_table = list(zip(days_courses, values_new_courses)) + + context = { + 'period_options': period_options, + 'p': p, + 'start_date': start_dt.date(), + 'end_date': now.date(), + + # KPI + 'kpi': { + 'total_users': total_users, + 'new_users_period': sum(values_new_users), + 'active_users_period': active_users_count, + 'total_courses': total_courses, + 'courses_enabled': total_courses_enabled, + 'total_lessons': total_lessons, + 'total_posts': total_posts, + }, + + # Séries pour graphiques + 'series': { + 'days': days_users, # mêmes intervalles pour tous + 'new_users': values_new_users, + 'new_courses': values_new_courses, + 'new_posts': values_new_posts, + 'activity_progress': values_activity, + }, + + # Disponibilité des sections + 'revenus_disponibles': revenus_disponibles, + 'technique_disponible': technique_disponible, + + # Tables + 'new_users_table': new_users_table, + 'new_courses_table': new_courses_table, + } + + # Sérialisation JSON pour Chart.js + context['series_json'] = json.dumps(context['series']) + context['labels_json'] = json.dumps(context['series']['days']) + + return render(request, 'home/stats_dashboard.html', context)