Ajout de la fonctionnalité d’activité en direct dans le tableau de bord des statistiques et mise à jour des templates, vues, URL, et styles associés.
This commit is contained in:
parent
a7b51e3a82
commit
7cf04968eb
6 changed files with 114 additions and 2 deletions
|
|
@ -111,7 +111,11 @@ class VisitTrackingMiddleware:
|
||||||
# Marquer la conversion si pas encore définie
|
# Marquer la conversion si pas encore définie
|
||||||
if visit.became_user_at is None:
|
if visit.became_user_at is None:
|
||||||
visit.became_user_at = now
|
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
|
visit.last_seen = now
|
||||||
dirty = True
|
dirty = True
|
||||||
if dirty:
|
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'])
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ urlpatterns = [
|
||||||
# Tableau de bord statistiques (réservé superadministrateurs)
|
# Tableau de bord statistiques (réservé superadministrateurs)
|
||||||
path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'),
|
path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'),
|
||||||
path('dashboard/stats/charts/', views.stats_charts, name='stats_charts'),
|
path('dashboard/stats/charts/', views.stats_charts, name='stats_charts'),
|
||||||
|
path('dashboard/stats/live-activity/', views.live_activity, name='live_activity'),
|
||||||
]
|
]
|
||||||
|
|
@ -9,6 +9,7 @@ from courses.models import Course, Lesson
|
||||||
from blog.models import Post
|
from blog.models import Post
|
||||||
from progression.models import Progression
|
from progression.models import Progression
|
||||||
import json
|
import json
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
courses = Course.objects.order_by('-created_at')[:6]
|
courses = Course.objects.order_by('-created_at')[:6]
|
||||||
|
|
@ -293,3 +294,44 @@ def stats_charts(request):
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'home/stats_charts.html', context)
|
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})
|
||||||
|
|
|
||||||
|
|
@ -2330,6 +2330,16 @@ input[type="text"], input[type="email"], input[type="password"], textarea {
|
||||||
filter: brightness(1.05);
|
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 */
|
/* Tailles */
|
||||||
.btn-sm, .button-sm { padding: 6px 10px; font-size: 0.9rem; }
|
.btn-sm, .button-sm { padding: 6px 10px; font-size: 0.9rem; }
|
||||||
.btn-lg, .button-lg { padding: 12px 20px; font-size: 1.05rem; }
|
.btn-lg, .button-lg { padding: 12px 20px; font-size: 1.05rem; }
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Tableau de bord statistiques</h1>
|
<h1>Tableau de bord statistiques</h1>
|
||||||
<p style="margin:8px 0"><a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a></p>
|
<p style="margin:8px 0">
|
||||||
|
<a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<form method="get" class="toolbar">
|
<form method="get" class="toolbar">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -90,6 +92,20 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="tables" id="live-activity">
|
||||||
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
|
<h3>Activité en direct (5 dernières minutes)</h3>
|
||||||
|
<table id="live-activity-table" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Type</th><th>Identité</th><th>Page</th><th>Il y a</th><th>Source</th><th>Pays</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr id="live-empty"><td colspan="6">Chargement…</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="charts">
|
<section class="charts">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Évolution quotidienne</h3>
|
<h3>Évolution quotidienne</h3>
|
||||||
|
|
@ -173,5 +189,43 @@
|
||||||
plugins: {legend: {position: 'bottom'}}
|
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);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
{% if user.is_authenticated and user.is_staff %}
|
{% if user.is_authenticated and user.is_staff %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'admin:index' %}" class="button button-danger" title="Administration">Admin</a>
|
<a href="{% url 'admin:index' %}" class="button button-danger" title="Administration">Admin</a>
|
||||||
|
<a href="{% url 'home:stats_dashboard' %}" class="button button-warning" title="Stats">Stats</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue