Mise à jour de la version applicative dans VERSION.txt.

This commit is contained in:
mrtoine 2025-12-16 15:03:46 +01:00
parent d20302be0e
commit 18b807bf5a
3 changed files with 65 additions and 85 deletions

View file

@ -11,6 +11,49 @@ from progression.models import Progression
import json
from django.http import JsonResponse
# --------------------
# Helpers Stats Module
# --------------------
def _parse_period(request, default=30, options=None):
"""Parse period parameter 'p' from request and compute date range.
Returns (p, now_dt, start_dt, start_date, end_date)
- now_dt is timezone-aware now
- start_dt is datetime at start of range (inclusive)
- start_date/end_date are date objects for convenient filtering
"""
if options is None:
options = [7, 30, 90, 180]
try:
p = int(request.GET.get('p', default))
except (TypeError, ValueError):
p = default
if p not in options:
p = default
now_dt = timezone.now()
start_dt = now_dt - timezone.timedelta(days=p - 1)
return p, now_dt, start_dt, start_dt.date(), now_dt.date()
def _build_series_for_range(start_date, end_date, qs, date_key='day', count_key='c'):
"""Build a continuous daily series from an aggregated queryset.
qs must yield dicts with date_key (date as string or date) and count_key.
Returns (labels_days, values_counts)
"""
counts = {str(item[date_key]): item[count_key] for item in qs}
days = []
values = []
d = start_date
while d <= end_date:
key = str(d)
days.append(key)
values.append(counts.get(key, 0))
d += timezone.timedelta(days=1)
return days, values
def home(request):
courses = Course.objects.order_by('-created_at')[:6]
return render(request, 'home.html', {'courses': courses})
@ -30,19 +73,13 @@ def stats_dashboard(request):
# 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
p, now, start_dt, period_start_date, period_end_date = _parse_period(
request, default=30, options=period_options
)
# 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())
new_users_qs = User.objects.filter(date_joined__date__gte=period_start_date, date_joined__date__lte=period_end_date)
# Séries quotidiennes nouveaux utilisateurs
new_users_by_day = (
new_users_qs
@ -53,7 +90,7 @@ def stats_dashboard(request):
# 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())
Progression.objects.filter(updated_at__date__gte=period_start_date, updated_at__date__lte=period_end_date)
.values('user').distinct()
)
active_users_count = active_users_qs.count()
@ -62,7 +99,7 @@ def stats_dashboard(request):
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())
Course.objects.filter(created_at__date__gte=period_start_date, created_at__date__lte=period_end_date)
.extra(select={'day': "date(created_at)"})
.values('day').annotate(c=Count('id'))
)
@ -83,7 +120,7 @@ def stats_dashboard(request):
# 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())
Progression.objects.filter(updated_at__date__gte=period_start_date, updated_at__date__lte=period_end_date)
.extra(select={'day': "date(updated_at)"})
.values('day').annotate(c=Count('id'))
)
@ -91,7 +128,7 @@ def stats_dashboard(request):
# 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())
Post.objects.filter(created_at__date__gte=period_start_date, created_at__date__lte=period_end_date)
.extra(select={'day': "date(created_at)"})
.values('day').annotate(c=Count('id'))
)
@ -101,8 +138,6 @@ def stats_dashboard(request):
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()
@ -135,16 +170,8 @@ def stats_dashboard(request):
# 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
# Wrapper conservant l'API locale mais utilisant le helper commun
return _build_series_for_range(period_start_date, period_end_date, qs, date_key=date_key, count_key=count_key)
days_users, values_new_users = build_series_dict(new_users_by_day)
days_courses, values_new_courses = build_series_dict(new_courses_by_day)
@ -158,8 +185,8 @@ def stats_dashboard(request):
context = {
'period_options': period_options,
'p': p,
'start_date': start_dt.date(),
'end_date': now.date(),
'start_date': period_start_date,
'end_date': period_end_date,
# KPI
'kpi': {
@ -206,19 +233,11 @@ def stats_dashboard(request):
@cache_page(60 * 15)
def stats_charts(request):
"""Page dédiée aux graphiques (réservée superadmins)."""
# Période
# Période (utilise les mêmes helpers que le dashboard pour harmonisation)
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()
p, _now, _start_dt, period_start_date, period_end_date = _parse_period(
request, default=30, options=period_options
)
# Trafic par jour (visiteurs uniques)
visits_qs = (
@ -255,8 +274,8 @@ def stats_charts(request):
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')
labels, visitors_series = _build_series_for_range(period_start_date, period_end_date, visits_qs, date_key='date')
_, conversions_series = _build_series_for_range(period_start_date, period_end_date, 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)

View file

@ -4,32 +4,14 @@
{% block title %}- Stats · Graphiques{% endblock %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
.card{background:var(--bg-200);border-radius:12px;padding:16px;border:1px solid var(--bg-300)}
.grid{display:grid;grid-template-columns:1fr;gap:24px;margin:16px 0}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:24px}
.toolbar{display:flex;gap:8px;align-items:center;justify-content:space-between;margin:8px 0}
@media (max-width: 960px){.grid-2{grid-template-columns:1fr}}
</style>
{% include "partials/_stats_head.html" %}
{% endblock %}
{% block content %}
<div class="container">
<h1>Graphiques statistiques</h1>
<form method="get" class="toolbar">
<div>
<label for="p">Période:&nbsp;</label>
<select id="p" name="p" onchange="this.form.submit()">
{% for opt in period_options %}
<option value="{{ opt }}" {% if p == opt %}selected{% endif %}>{{ opt }} jours</option>
{% endfor %}
</select>
<span style="margin-left:8px;color:var(--fg-300)">Du {{ start_date }} au {{ end_date }}</span>
</div>
<div style="color:var(--fg-300)">Mise en cache 15 minutes</div>
</form>
{% include "partials/_stats_toolbar.html" %}
<div class="grid">
<div class="card">

View file

@ -4,17 +4,7 @@
{% block title %}- Tableau de bord{% endblock %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin:16px 0}
.card{background:var(--bg-200);border-radius:12px;padding:16px;border:1px solid var(--bg-300)}
.card h3{margin:0 0 8px 0;font-size:16px;color:var(--fg-300)}
.kpi{font-size:28px;font-weight:700}
.toolbar{display:flex;gap:8px;align-items:center;justify-content:space-between;margin:8px 0}
.charts{display:grid;grid-template-columns:1fr;gap:24px;margin:24px 0}
.tables{display:grid;grid-template-columns:1fr 1fr;gap:24px}
@media (max-width: 960px){.stats-grid{grid-template-columns:repeat(2,1fr)}.tables{grid-template-columns:1fr}}
</style>
{% include "partials/_stats_head.html" %}
{% endblock %}
{% block content %}
@ -24,18 +14,7 @@
<a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a>
</p>
<form method="get" class="toolbar">
<div>
<label for="p">Période:&nbsp;</label>
<select id="p" name="p" onchange="this.form.submit()">
{% for opt in period_options %}
<option value="{{ opt }}" {% if p == opt %}selected{% endif %}>{{ opt }} jours</option>
{% endfor %}
</select>
<span style="margin-left:8px;color:var(--fg-300)">Du {{ start_date }} au {{ end_date }}</span>
</div>
<div style="color:var(--fg-300)">Mise en cache 15 minutes</div>
</form>
{% include "partials/_stats_toolbar.html" %}
<section class="stats-grid">
<div class="card">