350 lines
12 KiB
Python
350 lines
12 KiB
Python
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 .audit import AuditedModelViewSet, audit_log
|
|
from .models import (
|
|
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
|
Locatie, Lokaal, Lokaalinstelling, Periode, Persoon, Roosterblok, Schooljaar,
|
|
Subgroep, Tijdslot,
|
|
)
|
|
from .permissions import CoreDataPermission, restrict_core_queryset
|
|
from .serializers import (
|
|
ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer,
|
|
FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer,
|
|
LokaalSerializer, LokaalinstellingSerializer, PeriodeSerializer,
|
|
PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer,
|
|
SubgroepSerializer, TijdslotSerializer,
|
|
)
|
|
|
|
SCHEDULE_WRITE_ROLES = ("roostermaker", "directie")
|
|
ORGANIZATION_WRITE_ROLES = ("directie", "zorgcoordinator")
|
|
FACILITY_WRITE_ROLES = ("roostermaker", "directie")
|
|
|
|
|
|
|
|
@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 CoreModelViewSet(AuditedModelViewSet):
|
|
permission_classes = [CoreDataPermission]
|
|
|
|
def get_queryset(self):
|
|
return restrict_core_queryset(self.request, super().get_queryset())
|
|
|
|
|
|
class GroepViewSet(CoreModelViewSet):
|
|
queryset = Groep.objects.all()
|
|
serializer_class = GroepSerializer
|
|
write_roles = ORGANIZATION_WRITE_ROLES
|
|
|
|
|
|
class PersoonViewSet(CoreModelViewSet):
|
|
serializer_class = PersoonSerializer
|
|
write_roles = ORGANIZATION_WRITE_ROLES
|
|
|
|
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 restrict_core_queryset(self.request, qs)
|
|
|
|
|
|
class SubgroepViewSet(CoreModelViewSet):
|
|
queryset = Subgroep.objects.prefetch_related("leden").all()
|
|
serializer_class = SubgroepSerializer
|
|
write_roles = ORGANIZATION_WRITE_ROLES
|
|
|
|
|
|
class FunctieViewSet(CoreModelViewSet):
|
|
queryset = Functie.objects.all()
|
|
serializer_class = FunctieSerializer
|
|
write_roles = ORGANIZATION_WRITE_ROLES
|
|
|
|
|
|
# --- Fase 2 ---------------------------------------------------------------
|
|
|
|
class SchooljaarViewSet(CoreModelViewSet):
|
|
queryset = Schooljaar.objects.all()
|
|
serializer_class = SchooljaarSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
|
|
class KalenderDagViewSet(CoreModelViewSet):
|
|
serializer_class = KalenderDagSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
def get_queryset(self):
|
|
qs = KalenderDag.objects.all()
|
|
schooljaar = self.request.query_params.get("schooljaar")
|
|
if schooljaar:
|
|
qs = qs.filter(schooljaar_id=schooljaar)
|
|
return restrict_core_queryset(self.request, qs)
|
|
|
|
|
|
class TijdslotViewSet(CoreModelViewSet):
|
|
queryset = Tijdslot.objects.all()
|
|
serializer_class = TijdslotSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
|
|
class ActiviteitViewSet(CoreModelViewSet):
|
|
queryset = Activiteit.objects.all()
|
|
serializer_class = ActiviteitSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
|
|
class LocatieViewSet(CoreModelViewSet):
|
|
queryset = Locatie.objects.all()
|
|
serializer_class = LocatieSerializer
|
|
write_roles = FACILITY_WRITE_ROLES
|
|
|
|
|
|
class LokaalViewSet(CoreModelViewSet):
|
|
queryset = Lokaal.objects.all()
|
|
serializer_class = LokaalSerializer
|
|
write_roles = FACILITY_WRITE_ROLES
|
|
|
|
|
|
@api_view(["GET", "PUT"])
|
|
@permission_classes([CoreDataPermission])
|
|
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()
|
|
audit_log(request, "update", "Lokaalinstelling:1")
|
|
return Response(ser.data)
|
|
return Response(LokaalinstellingSerializer(obj).data)
|
|
|
|
|
|
class RoosterblokViewSet(CoreModelViewSet):
|
|
serializer_class = RoosterblokSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
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 restrict_core_queryset(self.request, qs)
|
|
|
|
|
|
class PeriodeViewSet(CoreModelViewSet):
|
|
serializer_class = PeriodeSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
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 restrict_core_queryset(self.request, qs)
|
|
|
|
|
|
class AfwezigheidViewSet(CoreModelViewSet):
|
|
serializer_class = AfwezigheidSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES + ORGANIZATION_WRITE_ROLES
|
|
|
|
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 restrict_core_queryset(self.request, qs)
|
|
|
|
|
|
class BlokUitzonderingViewSet(CoreModelViewSet):
|
|
serializer_class = BlokUitzonderingSerializer
|
|
write_roles = SCHEDULE_WRITE_ROLES
|
|
|
|
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 restrict_core_queryset(self.request, 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"])
|
|
@permission_classes([CoreDataPermission])
|
|
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,
|
|
})
|