You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
4.5 KiB
136 lines
4.5 KiB
import json
|
|
|
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
from django.db.models import Count, Max, Min
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import render, HttpResponse, redirect
|
|
from django.utils import timezone
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from datetime import timedelta
|
|
|
|
from .models import UsageEvent
|
|
from .utils import parse_user_agent
|
|
|
|
# Create your views here.
|
|
def index(request):
|
|
return render(request, "app/base.html")
|
|
|
|
def install(request):
|
|
return render(request, "app/install.html")
|
|
|
|
def home(request):
|
|
return redirect("/app/v263")
|
|
|
|
def login(request):
|
|
return render(request, "app/login.html")
|
|
|
|
def v1321(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1322(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v133(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1332(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1333(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1334(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1335(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v1340(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v260(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v261(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v262(request):
|
|
return redirect("/app/v263")
|
|
|
|
def v263(request):
|
|
return render(request, "app/home.html")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Anonyme Nutzungsstatistik fürs Admin-Dashboard
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@csrf_exempt
|
|
@require_POST
|
|
def track_usage(request):
|
|
"""Nimmt ein anonymes Nutzungssignal entgegen (ein Aufruf pro Gerät und Tag,
|
|
siehe home01.js: pingUsageIfNeeded()). Kein CSRF-Schutz nötig, da rein additiv
|
|
und ohne jeden Personenbezug - device_id kommt aus dem localStorage des Geräts."""
|
|
try:
|
|
data = json.loads(request.body or "{}")
|
|
except json.JSONDecodeError:
|
|
data = {}
|
|
|
|
device_id = str(data.get("device_id", "")).strip()[:64]
|
|
if not device_id:
|
|
return JsonResponse({"ok": False, "error": "device_id fehlt"}, status=400)
|
|
|
|
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
platform, browser = parse_user_agent(user_agent)
|
|
|
|
UsageEvent.objects.create(
|
|
device_id=device_id,
|
|
app_version=str(data.get("app_version", ""))[:20],
|
|
user_agent=user_agent[:1000],
|
|
platform=platform,
|
|
browser=browser,
|
|
is_standalone=bool(data.get("standalone", False)),
|
|
)
|
|
return JsonResponse({"ok": True})
|
|
|
|
|
|
@staff_member_required
|
|
def usage_dashboard(request):
|
|
"""Nutzungs-Dashboard für Admins - nur für angemeldete Staff-Nutzer (Django-Admin-Login)."""
|
|
now = timezone.now()
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
events = UsageEvent.objects.all()
|
|
|
|
# "Nutzer" = Gerät hat PioMint mindestens einmal als installierte PWA
|
|
# (Standalone) geöffnet - nur das ist echte Nutzung. Ein Gerät, das die
|
|
# Seite nur im Browser-Tab geöffnet hat, ist ein "Aufrufer", kein Nutzer.
|
|
user_device_ids = events.filter(is_standalone=True).values_list("device_id", flat=True).distinct()
|
|
users = events.filter(device_id__in=user_device_ids)
|
|
visitors_total = events.exclude(device_id__in=user_device_ids).values("device_id").distinct().count()
|
|
|
|
context = {
|
|
"total_connections": events.count(),
|
|
"visitors_total": visitors_total,
|
|
"users_total": user_device_ids.count(),
|
|
"users_today": users.filter(created_at__gte=today_start).values("device_id").distinct().count(),
|
|
"users_7d": users.filter(created_at__gte=now - timedelta(days=7)).values("device_id").distinct().count(),
|
|
"users_30d": users.filter(created_at__gte=now - timedelta(days=30)).values("device_id").distinct().count(),
|
|
"by_platform": users.values("platform").annotate(count=Count("device_id", distinct=True)).order_by("-count"),
|
|
"by_browser": users.values("browser").annotate(count=Count("device_id", distinct=True)).order_by("-count"),
|
|
"device_summary": (
|
|
events.values("device_id")
|
|
.annotate(
|
|
ping_count=Count("id"),
|
|
first_seen=Min("created_at"),
|
|
last_seen=Max("created_at"),
|
|
standalone_any=Max("is_standalone"),
|
|
)
|
|
.order_by("-last_seen")
|
|
),
|
|
"recent_events": events[:30],
|
|
}
|
|
return render(request, "app/dashboard.html", context)
|