136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
"""Automatisch rooster: stelt een conflictvrij voorstel voor (greedy).
|
|
|
|
Houdt rekening met voorkeuren: een vak liever in de ochtend of middag, bepaalde
|
|
weekdagen vrijhouden, en een maximum aantal blokken per dag per groep.
|
|
"""
|
|
from django.db.models import Q
|
|
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")
|
|
groep_id = request.data.get("groep")
|
|
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 groep_id:
|
|
return Response({"detail": "Geef een schooljaar en een groep op."}, status=400)
|
|
try:
|
|
sj = Schooljaar.objects.get(pk=sjid)
|
|
groep = Groep.objects.get(pk=groep_id)
|
|
except (Schooljaar.DoesNotExist, Groep.DoesNotExist):
|
|
return Response({"detail": "Schooljaar of groep niet gevonden."}, status=404)
|
|
|
|
# Kandidaat-tijdsloten: groep-specifiek of algemeen, vrije dagen uitgesloten.
|
|
sloten = [
|
|
s for s in Tijdslot.objects.filter(actief=True)
|
|
.filter(Q(groep=groep) | Q(groep__isnull=True))
|
|
.order_by("dag", "volgorde", "start_tijd")
|
|
if s.dag not in vrije_dagen
|
|
]
|
|
if not sloten:
|
|
return Response({"voorstel": [], "niet_geplaatst": [], "detail": "Geen bruikbare tijdsloten (let op vrije dagen)."})
|
|
|
|
alle_slot_dag = {t.id: t.dag for t in Tijdslot.objects.all()}
|
|
|
|
# Bezetting bijhouden (eventueel inclusief bestaande blokken).
|
|
groep_bezet = set()
|
|
beg_bezet = {}
|
|
lok_bezet = {}
|
|
per_dag = {} # weekdag -> aantal blokken voor deze groep
|
|
if respecteer:
|
|
for b in Roosterblok.objects.filter(schooljaar=sj).prefetch_related("begeleiders"):
|
|
if b.groep_id == groep.id:
|
|
groep_bezet.add(b.tijdslot_id)
|
|
wd = alle_slot_dag.get(b.tijdslot_id)
|
|
if wd is not None:
|
|
per_dag[wd] = per_dag.get(wd, 0) + 1
|
|
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)
|
|
|
|
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 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
|
|
|
|
# Voorkeur dagdeel: zet de voorkeurssloten vooraan (zachte voorkeur).
|
|
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): # pas 1 = spreiden (max 1 per dag), pas 2 = opvullen
|
|
for s in kandidaten:
|
|
if geplaatst >= aantal:
|
|
break
|
|
if s.id in groep_bezet:
|
|
continue
|
|
if pas == 1 and s.dag in vak_dagen:
|
|
continue
|
|
if max_per_dag is not None and per_dag.get(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": groep.id,
|
|
"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.add(s.id)
|
|
vak_dagen.add(s.dag)
|
|
per_dag[s.dag] = per_dag.get(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, "?"), "tekort": aantal - geplaatst})
|
|
|
|
voorstel_lijst.sort(key=lambda x: (x["dag"], x["tijdslot_label"]))
|
|
return Response({"voorstel": voorstel_lijst, "niet_geplaatst": niet})
|