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, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) from .serializers import ( ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer, FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer, 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 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", "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"]], }) # 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, })