roosterwijs/backend/core/models.py

398 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Kerndomein van Roosterwijs.
Fase 1: Persoon, Groep, Subgroep, Functie (catalogus).
Fase 2: Schooljaar + KalenderDag (jaaroverzicht), Tijdslot, Activiteit,
Locatie en Roosterblok (de spil van het rooster).
Veel flexibiliteit voor het speciaal onderwijs:
- één Persoon-tabel met een rol (leerling of medewerker-rol);
- medewerkers kunnen meerdere FUNCTIES hebben uit een beheerbare catalogus;
- roosterblokken zijn toewijsbaar aan een groep, subgroep OF een individuele
leerling, met één of meer begeleiders.
"""
from django.core.exceptions import ValidationError
from django.db import models
class Rol(models.TextChoices):
LEERLING = "leerling", "Leerling"
LEERKRACHT = "leerkracht", "Leerkracht"
PRAKTIJKONDERSTEUNER = "praktijkondersteuner", "Praktijkondersteuner"
KLASSENONDERSTEUNER = "klassenondersteuner", "Klassenondersteuner"
class Functie(models.Model):
"""Een beheerbare functie/rol voor medewerkers (bv. Directie, Logopedist).
Dit is een vrije catalogus: een beheerder maakt functies aan en verwijdert
ze. Een medewerker kan meerdere functies hebben (zie Persoon.functies).
"""
naam = models.CharField("naam", max_length=80, unique=True)
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
actief = models.BooleanField("actief", default=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "functie"
verbose_name_plural = "functies"
ordering = ["naam"]
def __str__(self) -> str:
return self.naam
class Groep(models.Model):
"""Een klas/groep waar leerlingen bij horen."""
naam = models.CharField("naam", max_length=100, unique=True)
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
actief = models.BooleanField("actief", default=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "groep"
verbose_name_plural = "groepen"
ordering = ["naam"]
def __str__(self) -> str:
return self.naam
class Persoon(models.Model):
"""Leerling of medewerker - een tabel met een rol.
Leerlingen horen bij een groep. Medewerkers (niet-leerlingen) kunnen
meerdere functies hebben uit de Functie-catalogus.
"""
voornaam = models.CharField("voornaam", max_length=80)
achternaam = models.CharField("achternaam", max_length=120)
rol = models.CharField("rol", max_length=32, choices=Rol.choices)
groep = models.ForeignKey(
Groep,
verbose_name="groep",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="leerlingen",
help_text="Alleen van toepassing op leerlingen.",
)
functies = models.ManyToManyField(
Functie,
verbose_name="functies",
related_name="personen",
blank=True,
help_text="Alleen van toepassing op medewerkers.",
)
actief = models.BooleanField("actief", default=True)
opmerkingen = models.TextField("opmerkingen", blank=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "persoon"
verbose_name_plural = "personen"
ordering = ["achternaam", "voornaam"]
def __str__(self) -> str:
return f"{self.volledige_naam} ({self.get_rol_display()})"
@property
def volledige_naam(self) -> str:
return f"{self.voornaam} {self.achternaam}".strip()
@property
def is_leerling(self) -> bool:
return self.rol == Rol.LEERLING
def save(self, *args, **kwargs):
# Een groepskoppeling hoort alleen bij leerlingen.
if self.rol != Rol.LEERLING:
self.groep = None
super().save(*args, **kwargs)
if self.pk:
# Wie geen leerling (meer) is, hoort niet in subgroepen thuis.
# (limit_choices_to geldt alleen voor formulieren, niet voor de API.)
if self.rol != Rol.LEERLING:
self.subgroepen.clear()
else:
# Een leerling heeft geen medewerkersfuncties.
self.functies.clear()
class Subgroep(models.Model):
"""Een vrije, groep-overstijgende verzameling leerlingen.
Bijvoorbeeld een niveaugroepje rekenen met leerlingen uit meerdere groepen.
"""
naam = models.CharField("naam", max_length=100)
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
leden = models.ManyToManyField(
Persoon,
verbose_name="leden",
related_name="subgroepen",
blank=True,
limit_choices_to={"rol": Rol.LEERLING},
)
actief = models.BooleanField("actief", default=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "subgroep"
verbose_name_plural = "subgroepen"
ordering = ["naam"]
def __str__(self) -> str:
return self.naam
# --------------------------------------------------------------------------
# Fase 2: schooljaar/kalender en roosterblokken
# --------------------------------------------------------------------------
class Schooljaar(models.Model):
"""Een schooljaar met begin- en einddatum; bevat de kalender."""
naam = models.CharField("naam", max_length=40, unique=True, help_text="bv. 2026-2027")
start_datum = models.DateField("startdatum")
eind_datum = models.DateField("einddatum")
actief = models.BooleanField("actief", default=True)
class Meta:
verbose_name = "schooljaar"
verbose_name_plural = "schooljaren"
ordering = ["-start_datum"]
def __str__(self) -> str:
return self.naam
class DagType(models.TextChoices):
STUDIEDAG = "studiedag", "Studiedag"
VRIJE_DAG = "vrije_dag", "Vrije dag"
VAKANTIE = "vakantie", "Vakantie"
class KalenderDag(models.Model):
"""Een bijzondere dag in het schooljaar: studiedag, vrije dag of vakantie.
Op zulke dagen vervallen de roosterblokken (geen les).
"""
schooljaar = models.ForeignKey(
Schooljaar, on_delete=models.CASCADE, related_name="kalenderdagen"
)
datum = models.DateField("datum")
type = models.CharField("type", max_length=16, choices=DagType.choices)
omschrijving = models.CharField("omschrijving", max_length=120, blank=True)
class Meta:
verbose_name = "kalenderdag"
verbose_name_plural = "kalenderdagen"
ordering = ["datum"]
constraints = [
models.UniqueConstraint(
fields=["schooljaar", "datum"], name="uniek_schooljaar_datum"
)
]
def __str__(self) -> str:
return f"{self.datum} ({self.get_type_display()})"
@property
def is_lesdag(self) -> bool:
# Alle drie de types betekenen 'geen les'.
return False
class Weekdag(models.IntegerChoices):
MAANDAG = 0, "Maandag"
DINSDAG = 1, "Dinsdag"
WOENSDAG = 2, "Woensdag"
DONDERDAG = 3, "Donderdag"
VRIJDAG = 4, "Vrijdag"
class Tijdslot(models.Model):
"""Een vast tijdblok op een vaste schooldag (bv. 'Blok 1' ma 08:30-10:00)."""
naam = models.CharField("naam", max_length=60)
dag = models.IntegerField("weekdag", choices=Weekdag.choices)
start_tijd = models.TimeField("starttijd")
eind_tijd = models.TimeField("eindtijd")
volgorde = models.PositiveSmallIntegerField("volgorde", default=0)
actief = models.BooleanField("actief", default=True)
class Meta:
verbose_name = "tijdslot"
verbose_name_plural = "tijdsloten"
ordering = ["dag", "volgorde", "start_tijd"]
def __str__(self) -> str:
return f"{self.get_dag_display()} {self.start_tijd:%H:%M}-{self.eind_tijd:%H:%M} {self.naam}"
class Activiteit(models.Model):
"""Wat er gedaan wordt: rekenen, gym, therapie, pauze ..."""
naam = models.CharField("naam", max_length=80, unique=True)
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
actief = models.BooleanField("actief", default=True)
class Meta:
verbose_name = "activiteit"
verbose_name_plural = "activiteiten"
ordering = ["naam"]
def __str__(self) -> str:
return self.naam
class Locatie(models.Model):
"""Een lokaal/ruimte (handig voor conflictdetectie in fase 4)."""
naam = models.CharField("naam", max_length=80, unique=True)
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
actief = models.BooleanField("actief", default=True)
class Meta:
verbose_name = "locatie"
verbose_name_plural = "locaties"
ordering = ["naam"]
def __str__(self) -> str:
return self.naam
class Roosterblok(models.Model):
"""De spil: wie doet wat, wanneer, waar, met wie.
De doelgroep is precies één van: groep, subgroep of individuele leerling.
Zo kun je een groepsrooster maken én er individueel van afwijken.
"""
schooljaar = models.ForeignKey(
Schooljaar, on_delete=models.CASCADE, related_name="roosterblokken"
)
tijdslot = models.ForeignKey(
Tijdslot, on_delete=models.PROTECT, related_name="roosterblokken"
)
activiteit = models.ForeignKey(
Activiteit, on_delete=models.PROTECT, related_name="roosterblokken"
)
locatie = models.ForeignKey(
Locatie, on_delete=models.SET_NULL, null=True, blank=True,
related_name="roosterblokken",
)
# Doelgroep: precies één van de drie is gevuld.
groep = models.ForeignKey(
Groep, on_delete=models.CASCADE, null=True, blank=True,
related_name="roosterblokken",
)
subgroep = models.ForeignKey(
Subgroep, on_delete=models.CASCADE, null=True, blank=True,
related_name="roosterblokken",
)
leerling = models.ForeignKey(
Persoon, on_delete=models.CASCADE, null=True, blank=True,
related_name="leerling_roosterblokken",
limit_choices_to={"rol": Rol.LEERLING},
)
begeleiders = models.ManyToManyField(
Persoon, related_name="begeleider_roosterblokken", blank=True,
)
opmerkingen = models.CharField("opmerkingen", max_length=255, blank=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "roosterblok"
verbose_name_plural = "roosterblokken"
ordering = ["tijdslot__dag", "tijdslot__volgorde"]
def clean(self):
doelen = [self.groep_id, self.subgroep_id, self.leerling_id]
gevuld = [d for d in doelen if d]
if len(gevuld) != 1:
raise ValidationError(
"Kies precies één doelgroep: groep, subgroep of leerling."
)
@property
def doel_type(self) -> str:
if self.groep_id:
return "groep"
if self.subgroep_id:
return "subgroep"
if self.leerling_id:
return "leerling"
return "onbekend"
def __str__(self) -> str:
return f"{self.activiteit} @ {self.tijdslot}"
# --------------------------------------------------------------------------
# Fase 3: flexibiliteit en uitzonderingen
# --------------------------------------------------------------------------
class Afwezigheid(models.Model):
"""Een persoon (leerling of medewerker) is afwezig in een periode."""
persoon = models.ForeignKey(
Persoon, on_delete=models.CASCADE, related_name="afwezigheden"
)
start_datum = models.DateField("van")
eind_datum = models.DateField("tot en met")
reden = models.CharField("reden", max_length=160, blank=True)
aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "afwezigheid"
verbose_name_plural = "afwezigheden"
ordering = ["-start_datum"]
def clean(self):
if self.eind_datum and self.start_datum and self.eind_datum < self.start_datum:
raise ValidationError("De einddatum ligt vóór de startdatum.")
def __str__(self) -> str:
return f"{self.persoon} afwezig {self.start_datum}{self.eind_datum}"
class BlokUitzondering(models.Model):
"""Een eenmalige afwijking op een roosterblok voor één specifieke datum.
- vervalt=True -> het blok gaat die dag niet door;
- vervanger -> een vervangende begeleider voor die dag;
- opmerking -> toelichting (bv. 'in ander lokaal').
"""
roosterblok = models.ForeignKey(
Roosterblok, on_delete=models.CASCADE, related_name="uitzonderingen"
)
datum = models.DateField("datum")
vervalt = models.BooleanField("vervalt", default=True)
vervanger = models.ForeignKey(
Persoon, on_delete=models.SET_NULL, null=True, blank=True,
related_name="vervangingen",
)
opmerking = models.CharField("opmerking", max_length=200, blank=True)
class Meta:
verbose_name = "blok-uitzondering"
verbose_name_plural = "blok-uitzonderingen"
ordering = ["datum"]
constraints = [
models.UniqueConstraint(
fields=["roosterblok", "datum"], name="uniek_blok_datum"
)
]
def __str__(self) -> str:
wat = "vervalt" if self.vervalt else "wijziging"
return f"{self.roosterblok} op {self.datum} ({wat})"