From 7cf04968eba748426ce33ccdad45892af93aafde Mon Sep 17 00:00:00 2001 From: mrtoine Date: Tue, 16 Dec 2025 14:15:58 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20la=20fonctionnalit=C3=A9=20d?= =?UTF-8?q?=E2=80=99activit=C3=A9=20en=20direct=20dans=20le=20tableau=20de?= =?UTF-8?q?=20bord=20des=20statistiques=20et=20mise=20=C3=A0=20jour=20des?= =?UTF-8?q?=20templates,=20vues,=20URL,=20et=20styles=20associ=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/middleware.py | 6 +++- home/urls.py | 1 + home/views.py | 42 ++++++++++++++++++++++ static/css/app.css | 10 ++++++ templates/home/stats_dashboard.html | 56 ++++++++++++++++++++++++++++- templates/partials/_header.html | 1 + 6 files changed, 114 insertions(+), 2 deletions(-) diff --git a/core/middleware.py b/core/middleware.py index 3e76796..3167553 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -111,7 +111,11 @@ class VisitTrackingMiddleware: # Marquer la conversion si pas encore définie if visit.became_user_at is None: visit.became_user_at = now + # Mettre à jour la page courante et l'horodatage + if visit.path != path: + visit.path = path[:512] + dirty = True visit.last_seen = now dirty = True if dirty: - visit.save(update_fields=['source', 'country', 'user', 'became_user_at', 'last_seen']) + visit.save(update_fields=['source', 'country', 'user', 'became_user_at', 'path', 'last_seen']) diff --git a/home/urls.py b/home/urls.py index 1dbd960..52e2435 100644 --- a/home/urls.py +++ b/home/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ # Tableau de bord statistiques (réservé superadministrateurs) path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'), path('dashboard/stats/charts/', views.stats_charts, name='stats_charts'), + path('dashboard/stats/live-activity/', views.live_activity, name='live_activity'), ] \ No newline at end of file diff --git a/home/views.py b/home/views.py index ebd2cf2..76fba86 100644 --- a/home/views.py +++ b/home/views.py @@ -9,6 +9,7 @@ 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] @@ -293,3 +294,44 @@ def stats_charts(request): } 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}) diff --git a/static/css/app.css b/static/css/app.css index 367520c..a4ddad5 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -2330,6 +2330,16 @@ input[type="text"], input[type="email"], input[type="password"], textarea { filter: brightness(1.05); } +.btn-warning, .button-warning { + background-color: var(--warning); + border-color: var(--warning); + color: var(--warning-contrast); +} + +.btn-warning:hover, .button-warning:hover { + filter: brightness(1.05); +} + /* Tailles */ .btn-sm, .button-sm { padding: 6px 10px; font-size: 0.9rem; } .btn-lg, .button-lg { padding: 12px 20px; font-size: 1.05rem; } diff --git a/templates/home/stats_dashboard.html b/templates/home/stats_dashboard.html index 3dddab3..704ada6 100644 --- a/templates/home/stats_dashboard.html +++ b/templates/home/stats_dashboard.html @@ -20,7 +20,9 @@ {% block content %}

Tableau de bord statistiques

-

→ Voir la page de graphiques

+

+ → Voir la page de graphiques +

@@ -90,6 +92,20 @@
+
+
+

Activité en direct (5 dernières minutes)

+ + + + + + + +
TypeIdentitéPageIl y aSourcePays
Chargement…
+
+
+

Évolution quotidienne

@@ -173,5 +189,43 @@ plugins: {legend: {position: 'bottom'}} } }); + + // Live activity polling + function humanizeSeconds(s) { + if (s < 60) return s + 's'; + const m = Math.floor(s/60); const r = s % 60; + if (m < 60) return m + 'm' + (r?(' '+r+'s'):''); + const h = Math.floor(m/60); const mr = m % 60; return h + 'h' + (mr?(' '+mr+'m'):''); + } + async function fetchLive() { + try { + const res = await fetch('{% url "home:live_activity" %}', {headers: {'Accept': 'application/json'}}); + if (!res.ok) throw new Error('HTTP '+res.status); + const payload = await res.json(); + const tbody = document.querySelector('#live-activity-table tbody'); + tbody.innerHTML = ''; + const items = payload.items || []; + if (items.length === 0) { + const tr = document.createElement('tr'); + const td = document.createElement('td'); td.colSpan = 6; td.textContent = 'Aucune activité récente'; + tr.appendChild(td); tbody.appendChild(tr); return; + } + for (const it of items) { + const tr = document.createElement('tr'); + const type = document.createElement('td'); type.textContent = it.is_user ? 'Utilisateur' : 'Visiteur'; + const ident = document.createElement('td'); ident.textContent = it.is_user ? (it.username || 'Utilisateur') : it.visitor; + const page = document.createElement('td'); page.textContent = it.path || '/'; + const ago = document.createElement('td'); ago.textContent = humanizeSeconds(it.seconds_ago); + const src = document.createElement('td'); src.textContent = it.source || ''; + const country = document.createElement('td'); country.textContent = it.country || ''; + tr.append(type, ident, page, ago, src, country); + tbody.appendChild(tr); + } + } catch (e) { + // silent fail in UI + } + } + fetchLive(); + setInterval(fetchLive, 10000); {% endblock %} diff --git a/templates/partials/_header.html b/templates/partials/_header.html index ea97ebe..a6938aa 100644 --- a/templates/partials/_header.html +++ b/templates/partials/_header.html @@ -48,6 +48,7 @@ {% if user.is_authenticated and user.is_staff %}
  • Admin + Stats
  • {% endif %}