336 lines
11 KiB
Python
336 lines
11 KiB
Python
"""
|
|
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}"
|