Ajout du tableau de bord statistiques pour les superadministrateurs, avec cache et graphiques JSON, et mise à jour des routes pour inclure la vue correspondante.
This commit is contained in:
parent
82c2e234e3
commit
e44e31bfb9
2 changed files with 154 additions and 1 deletions
|
|
@ -5,4 +5,6 @@ app_name = 'home'
|
|||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('premium/<int:course_id>', views.premium, name='premium'),
|
||||
# Tableau de bord statistiques (réservé superadministrateurs)
|
||||
path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'),
|
||||
]
|
||||
153
home/views.py
153
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue