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, })