from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import user_passes_test from django.utils import timezone from django.db.models import Count from core.models import Visit 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 from django.http import JsonResponse def home(request): courses = Course.objects.order_by('-created_at')[:6] return render(request, 'home.html', {'courses': courses}) 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 # Visites / Trafic period_start_date = start_dt.date() period_end_date = now.date() period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date) unique_visitors = period_visits.values('visitor_id').distinct().count() earlier_visitors_qs = Visit.objects.filter(date__lt=period_start_date).values('visitor_id').distinct() returning_visitors = period_visits.filter(visitor_id__in=earlier_visitors_qs).values('visitor_id').distinct().count() converted_visitors = ( period_visits .filter(became_user_at__isnull=False, became_user_at__date__gte=period_start_date, became_user_at__date__lte=period_end_date) .values('visitor_id').distinct().count() ) top_sources_qs = ( period_visits .values('source') .annotate(c=Count('visitor_id', distinct=True)) .order_by('-c') ) top_countries_qs = ( period_visits .exclude(country='') .values('country') .annotate(c=Count('visitor_id', distinct=True)) .order_by('-c') ) top_sources_table = [(row['source'] or 'Direct/Unknown', row['c']) for row in top_sources_qs[:10]] top_countries_table = [(row['country'], row['c']) for row in top_countries_qs[:10]] # 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, 'unique_visitors': unique_visitors, 'returning_visitors': returning_visitors, 'converted_visitors': converted_visitors, '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, 'top_sources_table': top_sources_table, 'top_countries_table': top_countries_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) @user_passes_test(lambda u: u.is_superuser) @cache_page(60 * 15) def stats_charts(request): """Page dédiée aux graphiques (réservée superadmins).""" # 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) period_start_date = start_dt.date() period_end_date = now.date() # Trafic par jour (visiteurs uniques) visits_qs = ( Visit.objects .filter(date__gte=period_start_date, date__lte=period_end_date) .values('date') .annotate(c=Count('visitor_id', distinct=True)) .order_by('date') ) # Conversions par jour (visiteurs devenus utilisateurs) conversions_qs = ( Visit.objects .filter( became_user_at__isnull=False, became_user_at__date__gte=period_start_date, became_user_at__date__lte=period_end_date, ) .extra(select={'day': "date(became_user_at)"}) .values('day') .annotate(c=Count('visitor_id', distinct=True)) .order_by('day') ) def build_series_dict(qs, date_key='date', count_key='c'): counts = {str(item[date_key]): item[count_key] for item in qs} days = [] values = [] d = period_start_date while d <= period_end_date: key = str(d) days.append(key) values.append(counts.get(key, 0)) d += timezone.timedelta(days=1) return days, values labels, visitors_series = build_series_dict(visits_qs, date_key='date') _, conversions_series = build_series_dict(conversions_qs, date_key='day') # Sources & Pays (sur la période) period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date) top_sources_qs = ( period_visits .values('source') .annotate(c=Count('visitor_id', distinct=True)) .order_by('-c')[:10] ) top_countries_qs = ( period_visits .exclude(country='') .values('country') .annotate(c=Count('visitor_id', distinct=True)) .order_by('-c')[:10] ) sources_labels = [(row['source'] or 'Direct/Unknown') for row in top_sources_qs] sources_values = [row['c'] for row in top_sources_qs] countries_labels = [row['country'] for row in top_countries_qs] countries_values = [row['c'] for row in top_countries_qs] context = { 'period_options': period_options, 'p': p, 'start_date': period_start_date, 'end_date': period_end_date, 'labels_json': json.dumps(labels), 'visitors_series_json': json.dumps(visitors_series), 'conversions_series_json': json.dumps(conversions_series), 'sources_labels_json': json.dumps(sources_labels), 'sources_values_json': json.dumps(sources_values), 'countries_labels_json': json.dumps(countries_labels), 'countries_values_json': json.dumps(countries_values), } return render(request, 'home/stats_charts.html', context) @user_passes_test(lambda u: u.is_superuser) def live_activity(request): """Retourne en JSON l'activité récente (5 dernières minutes): visiteurs et utilisateurs et leur page actuelle. """ now = timezone.now() since = now - timezone.timedelta(minutes=5) qs = ( Visit.objects .filter(last_seen__gte=since) .order_by('-last_seen') ) data = [] for v in qs[:200]: username = None is_user = False if v.user_id: is_user = True # safe access if user deleted try: username = v.user.username except Exception: username = 'Utilisateur' visitor_label = v.visitor_id[:8] seconds_ago = int((now - v.last_seen).total_seconds()) data.append({ 'visitor': visitor_label, 'is_user': is_user, 'username': username, 'path': v.path, 'last_seen': v.last_seen.isoformat(), 'seconds_ago': seconds_ago, 'date': str(v.date), 'country': v.country, 'source': v.source, }) return JsonResponse({'now': now.isoformat(), 'items': data})