358 lines
12 KiB
Python
358 lines
12 KiB
Python
import logging
|
|
|
|
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
|
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
|
from rest_framework import status, viewsets
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
|
|
from plugins import services
|
|
|
|
from .models import (
|
|
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
|
Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar,
|
|
Subgroep, Tijdslot,
|
|
)
|
|
from .serializers import (
|
|
ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer,
|
|
FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer,
|
|
LokaalSerializer, LokaalinstellingSerializer,
|
|
PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer,
|
|
SubgroepSerializer, TijdslotSerializer,
|
|
)
|
|
|
|
logger = logging.getLogger("roosterwijs.auth")
|
|
|
|
|
|
@api_view(["GET"])
|
|
@permission_classes([AllowAny])
|
|
def health(request):
|
|
"""Anonieme gezondheidscheck: bewust zonder inhoudelijke informatie."""
|
|
return Response({"naam": "Roosterwijs", "status": "ok"})
|
|
|
|
|
|
@api_view(["GET"])
|
|
def info(request):
|
|
"""Systeeminfo + welke modules actief zijn (alleen voor ingelogde gebruikers)."""
|
|
services.sync_states()
|
|
modules = services.describe_all()
|
|
return Response(
|
|
{
|
|
"naam": "Roosterwijs",
|
|
"fase": "1 - kerndomein (personen, groepen, subgroepen)",
|
|
"modules_totaal": len(modules),
|
|
"modules_actief": sum(1 for m in modules if m["enabled"]),
|
|
}
|
|
)
|
|
|
|
|
|
# --- Authenticatie ----------------------------------------------------------
|
|
|
|
def _user_payload(user):
|
|
return {
|
|
"authenticated": True,
|
|
"username": user.get_username(),
|
|
"is_staff": user.is_staff,
|
|
}
|
|
|
|
|
|
@api_view(["GET"])
|
|
@permission_classes([AllowAny])
|
|
@ensure_csrf_cookie
|
|
def auth_me(request):
|
|
"""Wie ben ik? Zet tegelijk de CSRF-cookie voor de frontend."""
|
|
if request.user.is_authenticated:
|
|
return Response(_user_payload(request.user))
|
|
return Response({"authenticated": False})
|
|
|
|
|
|
@api_view(["POST"])
|
|
@permission_classes([AllowAny])
|
|
def auth_login_view(request):
|
|
"""Inloggen met gebruikersnaam + wachtwoord (sessie-cookie)."""
|
|
username = str(request.data.get("username", "")).strip()
|
|
password = str(request.data.get("password", ""))
|
|
if not username or not password:
|
|
return Response(
|
|
{"detail": "Vul gebruikersnaam en wachtwoord in."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
user = authenticate(request, username=username, password=password)
|
|
if user is None:
|
|
logger.warning("Mislukte inlogpoging voor gebruiker %r", username)
|
|
return Response(
|
|
{"detail": "Onjuiste gebruikersnaam of wachtwoord."},
|
|
status=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
auth_login(request, user)
|
|
logger.info("Gebruiker %r ingelogd", username)
|
|
return Response(_user_payload(user))
|
|
|
|
|
|
@api_view(["POST"])
|
|
def auth_logout_view(request):
|
|
"""Uitloggen: sessie beëindigen."""
|
|
logger.info("Gebruiker %r uitgelogd", request.user.get_username())
|
|
auth_logout(request)
|
|
return Response({"authenticated": False})
|
|
|
|
|
|
class GroepViewSet(viewsets.ModelViewSet):
|
|
queryset = Groep.objects.all()
|
|
serializer_class = GroepSerializer
|
|
|
|
|
|
class PersoonViewSet(viewsets.ModelViewSet):
|
|
serializer_class = PersoonSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = Persoon.objects.select_related("groep", "stamgroep").prefetch_related("functies", "vakken").all()
|
|
rol = self.request.query_params.get("rol")
|
|
if rol:
|
|
qs = qs.filter(rol=rol)
|
|
return qs
|
|
|
|
|
|
class SubgroepViewSet(viewsets.ModelViewSet):
|
|
queryset = Subgroep.objects.prefetch_related("leden").all()
|
|
serializer_class = SubgroepSerializer
|
|
|
|
|
|
class FunctieViewSet(viewsets.ModelViewSet):
|
|
queryset = Functie.objects.all()
|
|
serializer_class = FunctieSerializer
|
|
|
|
|
|
# --- Fase 2 ---------------------------------------------------------------
|
|
|
|
class SchooljaarViewSet(viewsets.ModelViewSet):
|
|
queryset = Schooljaar.objects.all()
|
|
serializer_class = SchooljaarSerializer
|
|
|
|
|
|
class KalenderDagViewSet(viewsets.ModelViewSet):
|
|
serializer_class = KalenderDagSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = KalenderDag.objects.all()
|
|
schooljaar = self.request.query_params.get("schooljaar")
|
|
if schooljaar:
|
|
qs = qs.filter(schooljaar_id=schooljaar)
|
|
return qs
|
|
|
|
|
|
class TijdslotViewSet(viewsets.ModelViewSet):
|
|
queryset = Tijdslot.objects.all()
|
|
serializer_class = TijdslotSerializer
|
|
|
|
|
|
class ActiviteitViewSet(viewsets.ModelViewSet):
|
|
queryset = Activiteit.objects.all()
|
|
serializer_class = ActiviteitSerializer
|
|
|
|
|
|
class LocatieViewSet(viewsets.ModelViewSet):
|
|
queryset = Locatie.objects.all()
|
|
serializer_class = LocatieSerializer
|
|
|
|
|
|
class LokaalViewSet(viewsets.ModelViewSet):
|
|
queryset = Lokaal.objects.all()
|
|
serializer_class = LokaalSerializer
|
|
|
|
|
|
@api_view(["GET", "PUT"])
|
|
def lokaalinstelling(request):
|
|
"""Lees of wijzig de (per-school) instelling voor dubbele lokaalboekingen."""
|
|
obj = Lokaalinstelling.actueel()
|
|
if request.method == "PUT":
|
|
ser = LokaalinstellingSerializer(obj, data=request.data, partial=True)
|
|
ser.is_valid(raise_exception=True)
|
|
ser.save()
|
|
return Response(ser.data)
|
|
return Response(LokaalinstellingSerializer(obj).data)
|
|
|
|
|
|
class RoosterblokViewSet(viewsets.ModelViewSet):
|
|
serializer_class = RoosterblokSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = (
|
|
Roosterblok.objects
|
|
.select_related("tijdslot", "activiteit", "locatie", "groep", "subgroep", "leerling")
|
|
.prefetch_related("begeleiders")
|
|
.all()
|
|
)
|
|
for veld in ("schooljaar", "groep", "subgroep", "leerling"):
|
|
waarde = self.request.query_params.get(veld)
|
|
if waarde:
|
|
qs = qs.filter(**{f"{veld}_id": waarde})
|
|
return qs
|
|
|
|
|
|
class AfwezigheidViewSet(viewsets.ModelViewSet):
|
|
serializer_class = AfwezigheidSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = Afwezigheid.objects.select_related("persoon").all()
|
|
persoon = self.request.query_params.get("persoon")
|
|
if persoon:
|
|
qs = qs.filter(persoon_id=persoon)
|
|
# Overlap met een periode [van, tot]: start <= tot EN eind >= van.
|
|
van = self.request.query_params.get("van")
|
|
tot = self.request.query_params.get("tot")
|
|
if van:
|
|
qs = qs.filter(eind_datum__gte=van)
|
|
if tot:
|
|
qs = qs.filter(start_datum__lte=tot)
|
|
return qs
|
|
|
|
|
|
class BlokUitzonderingViewSet(viewsets.ModelViewSet):
|
|
serializer_class = BlokUitzonderingSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = BlokUitzondering.objects.select_related("vervanger", "roosterblok").all()
|
|
blok = self.request.query_params.get("roosterblok")
|
|
if blok:
|
|
qs = qs.filter(roosterblok_id=blok)
|
|
van = self.request.query_params.get("van")
|
|
tot = self.request.query_params.get("tot")
|
|
if van:
|
|
qs = qs.filter(datum__gte=van)
|
|
if tot:
|
|
qs = qs.filter(datum__lte=tot)
|
|
schooljaar = self.request.query_params.get("schooljaar")
|
|
if schooljaar:
|
|
qs = qs.filter(roosterblok__schooljaar_id=schooljaar)
|
|
return qs
|
|
|
|
|
|
def _leerlingen_van_blok(blok):
|
|
"""De leerlingen die een blok raakt (via groep, subgroep of individueel)."""
|
|
if blok.groep_id:
|
|
return [p.id for p in blok.groep.leerlingen.all()]
|
|
if blok.subgroep_id:
|
|
return [p.id for p in blok.subgroep.leden.all()]
|
|
if blok.leerling_id:
|
|
return [blok.leerling_id]
|
|
return []
|
|
|
|
|
|
@api_view(["GET"])
|
|
def conflicten(request):
|
|
"""Signaleer roosterconflicten binnen een schooljaar.
|
|
|
|
Per tijdslot (zelfde dag + tijd) controleren we op:
|
|
- dubbele inzet van een begeleider;
|
|
- een leerling die in twee blokken tegelijk zit;
|
|
- twee blokken in dezelfde locatie;
|
|
- onderbezetting (blok zonder begeleider) als waarschuwing.
|
|
"""
|
|
schooljaar = request.query_params.get("schooljaar")
|
|
qs = (
|
|
Roosterblok.objects
|
|
.select_related("tijdslot", "locatie", "lokaal", "groep", "subgroep", "leerling")
|
|
.prefetch_related("begeleiders", "groep__leerlingen", "subgroep__leden")
|
|
)
|
|
if schooljaar:
|
|
qs = qs.filter(schooljaar_id=schooljaar)
|
|
|
|
# Groepeer blokken per tijdslot.
|
|
per_slot = {}
|
|
for blok in qs:
|
|
per_slot.setdefault(blok.tijdslot_id, []).append(blok)
|
|
|
|
resultaat = []
|
|
|
|
def naam_van(persoon):
|
|
return persoon.volledige_naam
|
|
|
|
for slot_id, blokken in per_slot.items():
|
|
slot_label = str(blokken[0].tijdslot)
|
|
|
|
# Begeleiders dubbel ingezet.
|
|
begeleider_map = {}
|
|
for b in blokken:
|
|
for p in b.begeleiders.all():
|
|
begeleider_map.setdefault(p.id, {"naam": naam_van(p), "blokken": []})
|
|
begeleider_map[p.id]["blokken"].append(b)
|
|
for info in begeleider_map.values():
|
|
if len(info["blokken"]) > 1:
|
|
resultaat.append({
|
|
"type": "begeleider",
|
|
"ernst": "fout",
|
|
"tijdslot": slot_label,
|
|
"detail": f"{info['naam']} is op {len(info['blokken'])} plekken tegelijk ingepland",
|
|
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
|
})
|
|
|
|
# Leerlingen dubbel ingepland.
|
|
leerling_map = {}
|
|
for b in blokken:
|
|
for lid in _leerlingen_van_blok(b):
|
|
leerling_map.setdefault(lid, [])
|
|
leerling_map[lid].append(b)
|
|
if leerling_map:
|
|
namen = {p.id: p.volledige_naam for p in Persoon.objects.filter(id__in=leerling_map.keys())}
|
|
for lid, bl in leerling_map.items():
|
|
if len(bl) > 1:
|
|
resultaat.append({
|
|
"type": "leerling",
|
|
"ernst": "fout",
|
|
"tijdslot": slot_label,
|
|
"detail": f"{namen.get(lid, 'Leerling')} zit in {len(bl)} blokken tegelijk",
|
|
"blokken": [b.activiteit.naam for b in bl],
|
|
})
|
|
|
|
# Locatiebotsing.
|
|
locatie_map = {}
|
|
for b in blokken:
|
|
if b.locatie_id:
|
|
locatie_map.setdefault(b.locatie_id, {"naam": b.locatie.naam, "blokken": []})
|
|
locatie_map[b.locatie_id]["blokken"].append(b)
|
|
for info in locatie_map.values():
|
|
if len(info["blokken"]) > 1:
|
|
resultaat.append({
|
|
"type": "locatie",
|
|
"ernst": "fout",
|
|
"tijdslot": slot_label,
|
|
"detail": f"Locatie {info['naam']} is dubbel geboekt",
|
|
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
|
})
|
|
|
|
# Lokaalbotsing (dubbele boeking van een lokaal op hetzelfde tijdslot).
|
|
lokaal_map = {}
|
|
for b in blokken:
|
|
if b.lokaal_id:
|
|
lokaal_map.setdefault(b.lokaal_id, {"naam": b.lokaal.naam, "blokken": []})
|
|
lokaal_map[b.lokaal_id]["blokken"].append(b)
|
|
for info in lokaal_map.values():
|
|
if len(info["blokken"]) > 1:
|
|
resultaat.append({
|
|
"type": "lokaal",
|
|
"ernst": "fout",
|
|
"tijdslot": slot_label,
|
|
"detail": f"Lokaal {info['naam']} is dubbel geboekt",
|
|
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
|
})
|
|
|
|
# Onderbezetting (waarschuwing).
|
|
for b in blokken:
|
|
if b.begeleiders.count() == 0:
|
|
resultaat.append({
|
|
"type": "onderbezetting",
|
|
"ernst": "waarschuwing",
|
|
"tijdslot": slot_label,
|
|
"detail": f"{b.activiteit.naam} ({b.doel_type}) heeft geen begeleider",
|
|
"blokken": [b.activiteit.naam],
|
|
})
|
|
|
|
fouten = sum(1 for r in resultaat if r["ernst"] == "fout")
|
|
return Response({
|
|
"aantal": len(resultaat),
|
|
"fouten": fouten,
|
|
"waarschuwingen": len(resultaat) - fouten,
|
|
"conflicten": resultaat,
|
|
})
|