roosterwijs/backend/core/views.py

321 lines
11 KiB
Python

from rest_framework import 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, Periode, Persoon, Roosterblok, Schooljaar,
Subgroep, Tijdslot,
)
from .serializers import (
ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer,
FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer,
LokaalSerializer, LokaalinstellingSerializer, PeriodeSerializer,
PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer,
SubgroepSerializer, TijdslotSerializer,
)
@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"]),
}
)
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})
# Sjabloonblokken (Perioderooster-opzet) standaard uitsluiten uit het
# echte rooster; alleen tonen bij expliciet ?sjabloon=1.
sjabloon = self.request.query_params.get("sjabloon")
if sjabloon in ("1", "true", "True"):
qs = qs.filter(sjabloon=True)
else:
qs = qs.filter(sjabloon=False)
return qs
class PeriodeViewSet(viewsets.ModelViewSet):
serializer_class = PeriodeSerializer
def get_queryset(self):
qs = Periode.objects.select_related("schooljaar").all()
schooljaar = self.request.query_params.get("schooljaar")
if schooljaar:
qs = qs.filter(schooljaar_id=schooljaar)
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
.filter(sjabloon=False)
.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,
})