Mise à jour de la version applicative dans VERSION.txt.
This commit is contained in:
parent
d20302be0e
commit
18b807bf5a
3 changed files with 65 additions and 85 deletions
103
home/views.py
103
home/views.py
|
|
@ -11,6 +11,49 @@ from progression.models import Progression
|
||||||
import json
|
import json
|
||||||
from django.http import JsonResponse
|
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):
|
def home(request):
|
||||||
courses = Course.objects.order_by('-created_at')[:6]
|
courses = Course.objects.order_by('-created_at')[:6]
|
||||||
return render(request, 'home.html', {'courses': courses})
|
return render(request, 'home.html', {'courses': courses})
|
||||||
|
|
@ -30,19 +73,13 @@ def stats_dashboard(request):
|
||||||
|
|
||||||
# Période
|
# Période
|
||||||
period_options = [7, 30, 90, 180]
|
period_options = [7, 30, 90, 180]
|
||||||
try:
|
p, now, start_dt, period_start_date, period_end_date = _parse_period(
|
||||||
p = int(request.GET.get('p', 30))
|
request, default=30, options=period_options
|
||||||
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
|
# Utilisateurs
|
||||||
total_users = User.objects.count()
|
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
|
# Séries quotidiennes nouveaux utilisateurs
|
||||||
new_users_by_day = (
|
new_users_by_day = (
|
||||||
new_users_qs
|
new_users_qs
|
||||||
|
|
@ -53,7 +90,7 @@ def stats_dashboard(request):
|
||||||
|
|
||||||
# Activité approximée via Progression mise à jour
|
# Activité approximée via Progression mise à jour
|
||||||
active_users_qs = (
|
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()
|
.values('user').distinct()
|
||||||
)
|
)
|
||||||
active_users_count = active_users_qs.count()
|
active_users_count = active_users_qs.count()
|
||||||
|
|
@ -62,7 +99,7 @@ def stats_dashboard(request):
|
||||||
total_courses = Course.objects.count()
|
total_courses = Course.objects.count()
|
||||||
total_courses_enabled = Course.objects.filter(enable=True).count()
|
total_courses_enabled = Course.objects.filter(enable=True).count()
|
||||||
new_courses_by_day = (
|
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)"})
|
.extra(select={'day': "date(created_at)"})
|
||||||
.values('day').annotate(c=Count('id'))
|
.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
|
# 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
|
# donc on refait une série quotidienne d'activité progression
|
||||||
progress_activity_by_day = (
|
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)"})
|
.extra(select={'day': "date(updated_at)"})
|
||||||
.values('day').annotate(c=Count('id'))
|
.values('day').annotate(c=Count('id'))
|
||||||
)
|
)
|
||||||
|
|
@ -91,7 +128,7 @@ def stats_dashboard(request):
|
||||||
# Blog
|
# Blog
|
||||||
total_posts = Post.objects.count()
|
total_posts = Post.objects.count()
|
||||||
new_posts_by_day = (
|
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)"})
|
.extra(select={'day': "date(created_at)"})
|
||||||
.values('day').annotate(c=Count('id'))
|
.values('day').annotate(c=Count('id'))
|
||||||
)
|
)
|
||||||
|
|
@ -101,8 +138,6 @@ def stats_dashboard(request):
|
||||||
technique_disponible = False
|
technique_disponible = False
|
||||||
|
|
||||||
# Visites / Trafic
|
# 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)
|
period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date)
|
||||||
|
|
||||||
unique_visitors = period_visits.values('visitor_id').distinct().count()
|
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
|
# 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'):
|
def build_series_dict(qs, date_key='day', count_key='c'):
|
||||||
counts = {str(item[date_key]): item[count_key] for item in qs}
|
# Wrapper conservant l'API locale mais utilisant le helper commun
|
||||||
days = []
|
return _build_series_for_range(period_start_date, period_end_date, qs, date_key=date_key, count_key=count_key)
|
||||||
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_users, values_new_users = build_series_dict(new_users_by_day)
|
||||||
days_courses, values_new_courses = build_series_dict(new_courses_by_day)
|
days_courses, values_new_courses = build_series_dict(new_courses_by_day)
|
||||||
|
|
@ -158,8 +185,8 @@ def stats_dashboard(request):
|
||||||
context = {
|
context = {
|
||||||
'period_options': period_options,
|
'period_options': period_options,
|
||||||
'p': p,
|
'p': p,
|
||||||
'start_date': start_dt.date(),
|
'start_date': period_start_date,
|
||||||
'end_date': now.date(),
|
'end_date': period_end_date,
|
||||||
|
|
||||||
# KPI
|
# KPI
|
||||||
'kpi': {
|
'kpi': {
|
||||||
|
|
@ -206,19 +233,11 @@ def stats_dashboard(request):
|
||||||
@cache_page(60 * 15)
|
@cache_page(60 * 15)
|
||||||
def stats_charts(request):
|
def stats_charts(request):
|
||||||
"""Page dédiée aux graphiques (réservée superadmins)."""
|
"""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]
|
period_options = [7, 30, 90, 180]
|
||||||
try:
|
p, _now, _start_dt, period_start_date, period_end_date = _parse_period(
|
||||||
p = int(request.GET.get('p', 30))
|
request, default=30, options=period_options
|
||||||
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)
|
# Trafic par jour (visiteurs uniques)
|
||||||
visits_qs = (
|
visits_qs = (
|
||||||
|
|
@ -255,8 +274,8 @@ def stats_charts(request):
|
||||||
d += timezone.timedelta(days=1)
|
d += timezone.timedelta(days=1)
|
||||||
return days, values
|
return days, values
|
||||||
|
|
||||||
labels, visitors_series = build_series_dict(visits_qs, date_key='date')
|
labels, visitors_series = _build_series_for_range(period_start_date, period_end_date, visits_qs, date_key='date')
|
||||||
_, conversions_series = build_series_dict(conversions_qs, date_key='day')
|
_, conversions_series = _build_series_for_range(period_start_date, period_end_date, conversions_qs, date_key='day')
|
||||||
|
|
||||||
# Sources & Pays (sur la période)
|
# Sources & Pays (sur la période)
|
||||||
period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date)
|
period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date)
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,14 @@
|
||||||
{% block title %}- Stats · Graphiques{% endblock %}
|
{% block title %}- Stats · Graphiques{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
{% include "partials/_stats_head.html" %}
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Graphiques statistiques</h1>
|
<h1>Graphiques statistiques</h1>
|
||||||
|
|
||||||
<form method="get" class="toolbar">
|
{% include "partials/_stats_toolbar.html" %}
|
||||||
<div>
|
|
||||||
<label for="p">Période: </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>
|
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,7 @@
|
||||||
{% block title %}- Tableau de bord{% endblock %}
|
{% block title %}- Tableau de bord{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
{% include "partials/_stats_head.html" %}
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
@ -24,18 +14,7 @@
|
||||||
<a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a>
|
<a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="get" class="toolbar">
|
{% include "partials/_stats_toolbar.html" %}
|
||||||
<div>
|
|
||||||
<label for="p">Période: </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>
|
|
||||||
|
|
||||||
<section class="stats-grid">
|
<section class="stats-grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue