""" 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.", ) stamgroep = models.ForeignKey( Groep, verbose_name="stamgroep", on_delete=models.SET_NULL, null=True, blank=True, related_name="medewerkers", help_text="Stamgroep/plein van een medewerker.", ) vakken = models.ManyToManyField( "Activiteit", verbose_name="vakken", related_name="docenten", blank=True, help_text="Vakken die een medewerker kan geven.", ) geboortedatum = models.DateField("geboortedatum", null=True, blank=True) 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): # 'groep' is leerling-only; 'stamgroep' is medewerker-only. if self.rol != Rol.LEERLING: self.groep = None else: self.stamgroep = 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 of -vakken. self.functies.clear() self.vakken.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) groep = models.ForeignKey( Groep, verbose_name="groep", on_delete=models.SET_NULL, null=True, blank=True, related_name="subgroepen", help_text="De groep waar deze subgroep onder valt.", ) leden = models.ManyToManyField( Persoon, verbose_name="leden", related_name="subgroepen", blank=True, limit_choices_to={"rol": Rol.LEERLING}, ) begeleiders = models.ManyToManyField( Persoon, verbose_name="begeleiders", related_name="begeleide_subgroepen", blank=True, help_text="Medewerkers die aan deze subgroep gekoppeld zijn.", ) 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) groep = models.ForeignKey( Groep, verbose_name="groep", on_delete=models.CASCADE, null=True, blank=True, related_name="tijdsloten", help_text="Leeg = algemene (vaste) tijden; gevuld = afwijkende tijden voor die groep.", ) actief = models.BooleanField("actief", default=True) class Meta: verbose_name = "tijdslot" verbose_name_plural = "tijdsloten" ordering = ["dag", "volgorde", "start_tijd"] def __str__(self) -> str: suffix = f" ({self.groep.naam})" if self.groep_id else "" return f"{self.get_dag_display()} {self.start_tijd:%H:%M}-{self.eind_tijd:%H:%M} {self.naam}{suffix}" 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) theorievak = models.BooleanField( "theorievak", default=False, help_text="Theorievakken worden standaard aan nieuwe leerkrachten gekoppeld.", ) 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})"