"""Automatisch rooster: stelt een conflictvrij voorstel voor (greedy). Werkt voor meerdere klassen tegelijk. Een wens (vak) kan voor meerdere klassen gelden; begeleiders en lokalen worden over álle klassen heen niet dubbel geboekt. Voorkeuren: vak liever ochtend/middag, dagen vrijhouden, max. blokken per dag. """ from rest_framework.decorators import api_view from rest_framework.response import Response from core.models import Activiteit, Groep, Lokaal, Persoon, Roosterblok, Schooljaar, Tijdslot def _is_ochtend(slot): return slot.start_tijd.hour < 12 @api_view(["POST"]) def voorstel(request): sjid = request.data.get("schooljaar") # Meerdere klassen; val terug op het oude enkele 'groep'-veld. groepen_ids = request.data.get("groepen") if not groepen_ids and request.data.get("groep"): groepen_ids = [request.data.get("groep")] try: groepen_ids = [int(g) for g in (groepen_ids or [])] except (TypeError, ValueError): groepen_ids = [] wensen = request.data.get("wensen") or [] respecteer = request.data.get("respecteer_bestaand", True) vrije_dagen = {int(d) for d in (request.data.get("vrije_dagen") or [])} mpd = request.data.get("max_per_dag") try: max_per_dag = int(mpd) if mpd not in (None, "", 0, "0") else None except (TypeError, ValueError): max_per_dag = None if not sjid or not groepen_ids: return Response({"detail": "Geef een schooljaar en minstens één klas op."}, status=400) try: sj = Schooljaar.objects.get(pk=sjid) except Schooljaar.DoesNotExist: return Response({"detail": "Schooljaar niet gevonden."}, status=404) groepen = {g.id: g for g in Groep.objects.filter(id__in=groepen_ids)} ordered_gids = [gid for gid in groepen_ids if gid in groepen] if not ordered_gids: return Response({"detail": "Geen geldige klassen."}, status=404) alle_sloten = list(Tijdslot.objects.filter(actief=True).order_by("dag", "volgorde", "start_tijd")) slot_dag = {s.id: s.dag for s in alle_sloten} def sloten_voor(gid): return [s for s in alle_sloten if s.groep_id in (None, gid) and s.dag not in vrije_dagen] # Gedeelde bezetting over alle klassen heen. groep_bezet = {gid: set() for gid in ordered_gids} per_dag = {} # (gid, weekdag) -> aantal beg_bezet = {} # tijdslot -> set(begeleider-id) (globaal) lok_bezet = {} # tijdslot -> set(lokaal-id) (globaal) if respecteer: for b in Roosterblok.objects.filter(schooljaar=sj).prefetch_related("begeleiders"): for p in b.begeleiders.all(): beg_bezet.setdefault(b.tijdslot_id, set()).add(p.id) if b.lokaal_id: lok_bezet.setdefault(b.tijdslot_id, set()).add(b.lokaal_id) if b.groep_id in groep_bezet: groep_bezet[b.groep_id].add(b.tijdslot_id) wd = slot_dag.get(b.tijdslot_id) if wd is not None: per_dag[(b.groep_id, wd)] = per_dag.get((b.groep_id, wd), 0) + 1 act_naam = {a.id: a.naam for a in Activiteit.objects.all()} pers_naam = {p.id: p.volledige_naam for p in Persoon.objects.all()} lok_naam = {l.id: l.naam for l in Lokaal.objects.all()} slot_info = {s.id: {"label": f"{s.get_dag_display()} {s.start_tijd:%H:%M} {s.naam}", "dag": s.dag, "dag_label": s.get_dag_display()} for s in alle_sloten} voorstel_lijst = [] niet = [] for w in wensen: act_id = w.get("activiteit") aantal = int(w.get("aantal") or 0) beg = w.get("begeleider") or None lok = w.get("lokaal") or None dagdeel = (w.get("dagdeel") or "").strip() if not act_id or aantal <= 0: continue beg = int(beg) if beg else None lok = int(lok) if lok else None # Doelklassen van deze wens (leeg = alle gekozen klassen). try: doel = [int(g) for g in (w.get("groepen") or [])] except (TypeError, ValueError): doel = [] doel = [gid for gid in doel if gid in groepen] or list(ordered_gids) for gid in doel: sloten = sloten_voor(gid) if dagdeel == "ochtend": kandidaten = [s for s in sloten if _is_ochtend(s)] + [s for s in sloten if not _is_ochtend(s)] elif dagdeel == "middag": kandidaten = [s for s in sloten if not _is_ochtend(s)] + [s for s in sloten if _is_ochtend(s)] else: kandidaten = sloten geplaatst = 0 vak_dagen = set() for pas in (1, 2): for s in kandidaten: if geplaatst >= aantal: break if s.id in groep_bezet[gid]: continue if pas == 1 and s.dag in vak_dagen: continue if max_per_dag is not None and per_dag.get((gid, s.dag), 0) >= max_per_dag: continue if beg and beg in beg_bezet.get(s.id, set()): continue if lok and lok in lok_bezet.get(s.id, set()): continue voorstel_lijst.append({ "tijdslot": s.id, "tijdslot_label": slot_info[s.id]["label"], "dag": slot_info[s.id]["dag"], "dag_label": slot_info[s.id]["dag_label"], "dagdeel": "ochtend" if _is_ochtend(s) else "middag", "activiteit": act_id, "activiteit_naam": act_naam.get(act_id, "?"), "groep": gid, "groep_naam": groepen[gid].naam, "begeleider": beg, "begeleider_naam": pers_naam.get(beg) if beg else None, "lokaal": lok, "lokaal_naam": lok_naam.get(lok) if lok else None, }) groep_bezet[gid].add(s.id) vak_dagen.add(s.dag) per_dag[(gid, s.dag)] = per_dag.get((gid, s.dag), 0) + 1 if beg: beg_bezet.setdefault(s.id, set()).add(beg) if lok: lok_bezet.setdefault(s.id, set()).add(lok) geplaatst += 1 if geplaatst >= aantal: break if geplaatst < aantal: niet.append({"activiteit_naam": act_naam.get(act_id, "?"), "groep_naam": groepen[gid].naam, "tekort": aantal - geplaatst}) voorstel_lijst.sort(key=lambda x: (x["groep_naam"], x["dag"], x["tijdslot_label"])) return Response({"voorstel": voorstel_lijst, "niet_geplaatst": niet})