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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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: </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">
|
||||
|
|
|
|||
|
|
@ -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: </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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue