Skip to content
Snippets Groups Projects
models.py 30.1 KiB
Newer Older
Tom Teichler's avatar
Tom Teichler committed
# flake8: noqa: DJ01

Tom Teichler's avatar
Tom Teichler committed
from datetime import date, datetime, time, timedelta
from typing import Dict, Optional, Tuple, Union
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Max, Min, Q
from django.db.models.functions import Coalesce
from django.forms import Media
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import classproperty
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
Tom Teichler's avatar
Tom Teichler committed
from calendarweek.django import CalendarWeek, i18n_day_abbrs_lazy, i18n_day_names_lazy
Tom Teichler's avatar
Tom Teichler committed
from colorfield.fields import ColorField
from django_global_request.middleware import get_request
Tom Teichler's avatar
Tom Teichler committed
from aleksis.apps.chronos.managers import (
    AbsenceQuerySet,
Hangzhi Yu's avatar
Hangzhi Yu committed
    CurrentSiteManager,
Tom Teichler's avatar
Tom Teichler committed
    EventQuerySet,
    ExtraLessonQuerySet,
    GroupPropertiesMixin,
    HolidayQuerySet,
    LessonPeriodManager,
    LessonPeriodQuerySet,
    LessonSubstitutionManager,
    LessonSubstitutionQuerySet,
    SupervisionQuerySet,
    TeacherPropertiesMixin,
    ValidityRangeQuerySet,
from aleksis.apps.chronos.mixins import ValidityRangeRelatedExtensibleModel
from aleksis.apps.chronos.util.format import format_m2m
from aleksis.core.managers import CurrentSiteManagerWithoutMigrations
from aleksis.core.mixins import ExtensibleModel, SchoolTermRelatedExtensibleModel
from aleksis.core.models import DashboardWidget, SchoolTerm
Tom Teichler's avatar
Tom Teichler committed
from aleksis.core.util.core_helpers import has_person
class ValidityRange(ExtensibleModel):
    """Validity range model.

    This is used to link data to a validity range.
    """

    objects = CurrentSiteManagerWithoutMigrations.from_queryset(ValidityRangeQuerySet)()

    school_term = models.ForeignKey(
        SchoolTerm,
        on_delete=models.CASCADE,
        verbose_name=_("School term"),
        related_name="validity_ranges",
    )
    name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True)

    date_start = models.DateField(verbose_name=_("Start date"))
    date_end = models.DateField(verbose_name=_("End date"))

    @classmethod
    def get_current(cls, day: Optional[date] = None):
        if not day:
            day = timezone.now().date()
        try:
            return cls.objects.on_day(day).first()
        except ValidityRange.DoesNotExist:
            return None

    @classproperty
    def current(cls):
        return cls.get_current()

    def clean(self):
        """Ensure there is only one validity range at each point of time."""
        if self.date_end < self.date_start:
            raise ValidationError(
                _("The start date must be earlier than the end date.")
            )

        if self.school_term:
            if (
                self.date_end > self.school_term.date_end
                or self.date_start < self.school_term.date_start
            ):
                raise ValidationError(
                    _("The validity range must be within the school term.")
                )

        qs = ValidityRange.objects.within_dates(self.date_start, self.date_end)
        if self.pk:
            qs.exclude(pk=self.pk)
        if qs.exists():
            raise ValidationError(
                _(
                    "There is already a validity range for this time or a part of this time."
                )
            )

    def __str__(self):
        return (
            self.name or f"{date_format(self.date_start)}{date_format(self.date_end)}"
        )

    class Meta:
        verbose_name = _("Validity range")
        verbose_name_plural = _("Validity ranges")
        unique_together = ["date_start", "date_end"]


class TimePeriod(ValidityRangeRelatedExtensibleModel):
    WEEKDAY_CHOICES = list(enumerate(i18n_day_names_lazy()))
    WEEKDAY_CHOICES_SHORT = list(enumerate(i18n_day_abbrs_lazy()))
Jonathan Weth's avatar
Jonathan Weth committed
    weekday = models.PositiveSmallIntegerField(
        verbose_name=_("Week day"), choices=WEEKDAY_CHOICES
    )
    period = models.PositiveSmallIntegerField(verbose_name=_("Number of period"))
Jonathan Weth's avatar
Jonathan Weth committed
    time_start = models.TimeField(verbose_name=_("Start time"))
    time_end = models.TimeField(verbose_name=_("End time"))
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    def __str__(self) -> str:
        return f"{self.get_weekday_display()}, {self.period}."
    def get_times_dict(cls) -> Dict[int, Tuple[datetime, datetime]]:
        for period in cls.objects.for_current_or_all().all():
            periods[period.period] = (period.time_start, period.time_end)

        return periods

    def get_date(self, week: Optional[Union[CalendarWeek, int]] = None) -> date:
        if isinstance(week, CalendarWeek):
            wanted_week = week
        else:
            year = date.today().year
            week_number = week or getattr(self, "_week", None) or CalendarWeek().week
            if week_number < SchoolTerm.current.date_start.isocalendar()[1]:
                year += 1

            wanted_week = CalendarWeek(year=year, week=week_number)

        return wanted_week[self.weekday]
Jonathan Weth's avatar
Jonathan Weth committed
    def get_datetime_start(
        self, week: Optional[Union[CalendarWeek, int]] = None
    ) -> datetime:
        """Get datetime of lesson start in a specific week."""
        day = self.get_date(week)
        return datetime.combine(day, self.time_start)

Jonathan Weth's avatar
Jonathan Weth committed
    def get_datetime_end(
        self, week: Optional[Union[CalendarWeek, int]] = None
    ) -> datetime:
        """Get datetime of lesson end in a specific week."""
        day = self.get_date(week)
        return datetime.combine(day, self.time_end)

Tom Teichler's avatar
Tom Teichler committed
    def get_next_relevant_day(
        cls, day: Optional[date] = None, time: Optional[time] = None, prev: bool = False
    ) -> date:
Tom Teichler's avatar
Tom Teichler committed
        """Return next (previous) day with lessons depending on date and time."""
        if day is None:
            day = timezone.now().date()

        if time is not None and cls.time_max and not prev:
            if time > cls.time_max:
                day += timedelta(days=1)

        cw = CalendarWeek.from_date(day)

        if day.weekday() > cls.weekday_max:
            if prev:
                day = cw[cls.weekday_max]
            else:
                cw += 1
                day = cw[cls.weekday_min]
        elif day.weekday() < TimePeriod.weekday_min:
            if prev:
                cw -= 1
                day = cw[cls.weekday_max]
            else:
                day = cw[cls.weekday_min]

        return day

    @classmethod
    def get_prev_next_by_day(cls, day: date, url: str) -> Tuple[str, str]:
Tom Teichler's avatar
Tom Teichler committed
        """Build URLs for previous/next day."""
        day_prev = cls.get_next_relevant_day(day - timedelta(days=1), prev=True)
        day_next = cls.get_next_relevant_day(day + timedelta(days=1))

        url_prev = reverse(url, args=[day_prev.year, day_prev.month, day_prev.day])
        url_next = reverse(url, args=[day_next.year, day_next.month, day_next.day])

        return url_prev, url_next

Jonathan Weth's avatar
Jonathan Weth committed
    def period_min(cls) -> int:
        return (
            cls.objects.for_current_or_all()
            .aggregate(period__min=Coalesce(Min("period"), 1))
            .get("period__min")
Jonathan Weth's avatar
Jonathan Weth committed
        )
Jonathan Weth's avatar
Jonathan Weth committed
    def period_max(cls) -> int:
        return (
            cls.objects.for_current_or_all()
            .aggregate(period__max=Coalesce(Max("period"), 7))
            .get("period__max")
Jonathan Weth's avatar
Jonathan Weth committed
        )
Jonathan Weth's avatar
Jonathan Weth committed
    def time_min(cls) -> Optional[time]:
        return (
            cls.objects.for_current_or_all()
            .aggregate(Min("time_start"))
            .get("time_start__min")
        )
Jonathan Weth's avatar
Jonathan Weth committed
    def time_max(cls) -> Optional[time]:
        return (
            cls.objects.for_current_or_all()
            .aggregate(Max("time_end"))
            .get("time_end__max")
        )
Jonathan Weth's avatar
Jonathan Weth committed
    def weekday_min(cls) -> int:
        return (
            cls.objects.for_current_or_all()
            .aggregate(weekday__min=Coalesce(Min("weekday"), 0))
            .get("weekday__min")
Jonathan Weth's avatar
Jonathan Weth committed
        )
Jonathan Weth's avatar
Jonathan Weth committed
    def weekday_max(cls) -> int:
        return (
            cls.objects.for_current_or_all()
            .aggregate(weekday__max=Coalesce(Max("weekday"), 6))
            .get("weekday__max")
Jonathan Weth's avatar
Jonathan Weth committed
        )
    class Meta:
        unique_together = [["weekday", "period", "validity"]]
        ordering = ["weekday", "period"]
        indexes = [models.Index(fields=["time_start", "time_end"])]
        verbose_name = _("Time period")
        verbose_name_plural = _("Time periods")
class Subject(ExtensibleModel):
Jonathan Weth's avatar
Jonathan Weth committed
    short_name = models.CharField(
        verbose_name=_("Short name"), max_length=255, unique=True
    )
    name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True)
    colour_fg = ColorField(verbose_name=_("Foreground colour"), blank=True)
    colour_bg = ColorField(verbose_name=_("Background colour"), blank=True)
    def __str__(self) -> str:
        return f"{self.short_name} ({self.name})"
        ordering = ["name", "short_name"]
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name = _("Subject")
        verbose_name_plural = _("Subjects")
class Room(ExtensibleModel):
Jonathan Weth's avatar
Jonathan Weth committed
    short_name = models.CharField(
        verbose_name=_("Short name"), max_length=255, unique=True
    )
    name = models.CharField(verbose_name=_("Long name"), max_length=255)
    def __str__(self) -> str:
        return f"{self.name} ({self.short_name})"
        ordering = ["name", "short_name"]
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name = _("Room")
        verbose_name_plural = _("Rooms")
class Lesson(
    ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin
):
Tom Teichler's avatar
Tom Teichler committed
    subject = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "Subject",
        on_delete=models.CASCADE,
        related_name="lessons",
        verbose_name=_("Subject"),
Tom Teichler's avatar
Tom Teichler committed
    )
    teachers = models.ManyToManyField(
        "core.Person", related_name="lessons_as_teacher", verbose_name=_("Teachers")
    )
    periods = models.ManyToManyField(
Jonathan Weth's avatar
Jonathan Weth committed
        "TimePeriod",
        related_name="lessons",
        through="LessonPeriod",
        verbose_name=_("Periods"),
    )
    groups = models.ManyToManyField(
        "core.Group", related_name="lessons", verbose_name=_("Groups")
Tom Teichler's avatar
Tom Teichler committed
    )
    def get_year(self, week: int) -> int:
        year = self.validity.date_start.year
        if week < int(self.validity.date_start.strftime("%V")):
            year += 1
        return year
    def get_calendar_week(self, week: int):
        year = self.get_year(week)

        return CalendarWeek(year=year, week=week)

    def __str__(self):
        return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}"
        ordering = ["validity__date_start", "subject"]
        verbose_name = _("Lesson")
        verbose_name_plural = _("Lessons")
class LessonSubstitution(ExtensibleModel):
    objects = LessonSubstitutionManager.from_queryset(LessonSubstitutionQuerySet)()
Jonathan Weth's avatar
Jonathan Weth committed
    week = models.IntegerField(
        verbose_name=_("Week"), default=CalendarWeek.current_week
    )
Tom Teichler's avatar
Tom Teichler committed
    lesson_period = models.ForeignKey(
        "LessonPeriod", models.CASCADE, "substitutions", verbose_name=_("Lesson period")
    )
Nik | Klampfradler's avatar
Nik | Klampfradler committed

    subject = models.ForeignKey(
        "Subject",
        on_delete=models.CASCADE,
        related_name="lesson_substitutions",
        null=True,
        blank=True,
        verbose_name=_("Subject"),
    )
    teachers = models.ManyToManyField(
Jonathan Weth's avatar
Jonathan Weth committed
        "core.Person",
        related_name="lesson_substitutions",
        blank=True,
        verbose_name=_("Teachers"),
    )
    room = models.ForeignKey(
        "Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room")
    cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled?"))
Tom Teichler's avatar
Tom Teichler committed
    cancelled_for_teachers = models.BooleanField(
        default=False, verbose_name=_("Cancelled for teachers?")
    )
    comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
    def clean(self) -> None:
        if self.subject and self.cancelled:
Jonathan Weth's avatar
Jonathan Weth committed
            raise ValidationError(
                _("Lessons can only be either substituted or cancelled.")
            )
Tom Teichler's avatar
Tom Teichler committed
    @property
    def date(self):
        week = CalendarWeek(week=self.week, year=self.lesson_period.lesson.get_year(self.week))
        return week[self.lesson_period.period.weekday]

    def __str__(self):
        return f"{self.lesson_period}, {date_format(self.date)}"
        unique_together = [["lesson_period", "week"]]
        ordering = [
            "lesson_period__lesson__validity__date_start",
            "week",
            "lesson_period__period__weekday",
            "lesson_period__period__period",
        ]
        constraints = [
            models.CheckConstraint(
                check=~Q(cancelled=True, subject__isnull=False),
                name="either_substituted_or_cancelled",
        verbose_name = _("Lesson substitution")
        verbose_name_plural = _("Lesson substitutions")
class LessonPeriod(ExtensibleModel):
    objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)()
Tom Teichler's avatar
Tom Teichler committed
    lesson = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "Lesson",
        models.CASCADE,
        related_name="lesson_periods",
        verbose_name=_("Lesson"),
Tom Teichler's avatar
Tom Teichler committed
    )
    period = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "TimePeriod",
        models.CASCADE,
        related_name="lesson_periods",
        verbose_name=_("Time period"),
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler committed
    room = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "Room",
        models.CASCADE,
        null=True,
        related_name="lesson_periods",
        verbose_name=_("Room"),
Tom Teichler's avatar
Tom Teichler committed
    )
    def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution:
        wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week

        # We iterate over all substitutions because this can make use of
        # prefetching when this model is loaded from outside, in contrast
        # to .filter()
        for substitution in self.substitutions.all():
mirabilos's avatar
mirabilos committed
            if substitution.week == wanted_week:
    def get_subject(self) -> Optional[Subject]:
        if self.get_substitution() and self.get_substitution().subject:
            return self.get_substitution().subject
    def get_teachers(self) -> models.query.QuerySet:
        if self.get_substitution():
            return self.get_substitution().teachers
    def get_room(self) -> Optional[Room]:
        if self.get_substitution() and self.get_substitution().room:
            return self.get_substitution().room
    def get_teacher_names(self, sep: Optional[str] = ", ") -> str:
        return sep.join([teacher.full_name for teacher in self.get_teachers().all()])

    def get_groups(self) -> models.query.QuerySet:
    def __str__(self) -> str:
        return f"{self.period}, {self.lesson}"
    @property
    def next(self) -> "LessonPeriod":
        """Get next lesson period of this lesson.

        .. warning::
            To use this property,  the provided lesson period must be annotated with a week.
        """
        return LessonPeriod.objects.filter(lesson=self.lesson).next_lesson(self)

    @property
    def prev(self) -> "LessonPeriod":
        """Get previous lesson period of this lesson.

        .. warning::
            To use this property,  the provided lesson period must be annotated with a week.
        """
        return LessonPeriod.objects.filter(lesson=self.lesson).next_lesson(self, -1)

    @property
    def week(self) -> Union[CalendarWeek, None]:
        """Get annotated week as `CalendarWeek`.

        Defaults to `None` if no week is annotated.
        """
        if hasattr(self, "_week"):
            return CalendarWeek(week=self._week, year=self._year)
        else:
            return None

Tom Teichler's avatar
Tom Teichler committed
        ordering = [
            "lesson__validity__date_start",
Tom Teichler's avatar
Tom Teichler committed
            "period__weekday",
            "period__period",
            "lesson__subject",
        ]
        indexes = [models.Index(fields=["lesson", "period"])]
        verbose_name = _("Lesson period")
        verbose_name_plural = _("Lesson periods")


class TimetableWidget(DashboardWidget):
    template = "chronos/widget.html"

    def get_context(self):
Tom Teichler's avatar
Tom Teichler committed
        from aleksis.apps.chronos.util.build import build_timetable  # noqa
        request = get_request()
        context = {"has_plan": True}
Jonathan Weth's avatar
Jonathan Weth committed
        wanted_day = TimePeriod.get_next_relevant_day(
            timezone.now().date(), datetime.now().time()
        )

        if has_person(request.user):
            person = request.user.person
            # Build timetable
            timetable = build_timetable("person", person, wanted_day)

                # If no student or teacher, redirect to all timetables
                context["has_plan"] = False
            else:
Jonathan Weth's avatar
Jonathan Weth committed
                context["holiday"] = Holiday.on_day(wanted_day)
                context["type"] = type_
                context["day"] = wanted_day
                context["periods"] = TimePeriod.get_times_dict()
                context["smart"] = True
        else:
            context["has_plan"] = False
Tom Teichler's avatar
Tom Teichler committed
    media = Media(css={"all": ("css/chronos/timetable.css",)})

    class Meta:
        proxy = True
        verbose_name = _("Timetable widget")
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Timetable widgets")
    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
Jonathan Weth's avatar
Jonathan Weth committed
    name = models.CharField(
        verbose_name=_("Name"), blank=True, null=True, max_length=255
    )
    def __str__(self):
        if self.name:
            return f"{self.short_name} ({self.name})"
        else:
            return self.short_name

    class Meta:
        verbose_name = _("Absence reason")
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Absence reasons")
class Absence(SchoolTermRelatedExtensibleModel):
    objects = CurrentSiteManager.from_queryset(AbsenceQuerySet)()
Tom Teichler's avatar
Tom Teichler committed
    reason = models.ForeignKey(
        "AbsenceReason",
        on_delete=models.SET_NULL,
        related_name="absences",
        blank=True,
        null=True,
        verbose_name=_("Absence reason"),
    )
Tom Teichler's avatar
Tom Teichler committed
    teacher = models.ForeignKey(
        "core.Person",
        on_delete=models.CASCADE,
        related_name="absences",
        null=True,
        blank=True,
        verbose_name=_("Teacher"),
    )
    group = models.ForeignKey(
        "core.Group",
        on_delete=models.CASCADE,
        related_name="absences",
        null=True,
        blank=True,
        verbose_name=_("Group"),
    )
    room = models.ForeignKey(
        "Room",
        on_delete=models.CASCADE,
        related_name="absences",
        null=True,
        blank=True,
        verbose_name=_("Room"),
    )
Jonathan Weth's avatar
Jonathan Weth committed
    date_start = models.DateField(verbose_name=_("Start date"), null=True)
    date_end = models.DateField(verbose_name=_("End date"), null=True)
Tom Teichler's avatar
Tom Teichler committed
    period_from = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("Start period"),
        null=True,
        related_name="+",
    )
    period_to = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("End period"),
        null=True,
        related_name="+",
    )
    comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
    def __str__(self):
        if self.teacher:
            return str(self.teacher)
        elif self.group:
            return str(self.group)
        elif self.room:
            return str(self.room)
        else:
            return _("Unknown absence")

Tom Teichler's avatar
Tom Teichler committed
        ordering = ["date_start"]
        indexes = [models.Index(fields=["date_start", "date_end"])]
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Absences")
class Exam(SchoolTermRelatedExtensibleModel):
Tom Teichler's avatar
Tom Teichler committed
    lesson = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "Lesson",
        on_delete=models.CASCADE,
        related_name="exams",
        verbose_name=_("Lesson"),
Tom Teichler's avatar
Tom Teichler committed
    )

    date = models.DateField(verbose_name=_("Date of exam"), null=True)
Tom Teichler's avatar
Tom Teichler committed
    period_from = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("Start period"),
        null=True,
        related_name="+",
    )
    period_to = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("End period"),
        null=True,
        related_name="+",
    )
    title = models.CharField(verbose_name=_("Title"), max_length=255)
    comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
Tom Teichler's avatar
Tom Teichler committed
        indexes = [models.Index(fields=["date"])]
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Exams")
    objects = CurrentSiteManager.from_queryset(HolidayQuerySet)()
Jonathan Weth's avatar
Jonathan Weth committed

    title = models.CharField(verbose_name=_("Title"), max_length=255)
Jonathan Weth's avatar
Jonathan Weth committed
    date_start = models.DateField(verbose_name=_("Start date"), null=True)
    date_end = models.DateField(verbose_name=_("End date"), null=True)
    comments = models.TextField(verbose_name=_("Comments"), blank=True, null=True)
Jonathan Weth's avatar
Jonathan Weth committed
    @classmethod
    def on_day(cls, day: date) -> Optional["Holiday"]:
        holidays = cls.objects.on_day(day)
        if holidays.exists():
            return holidays[0]
        else:
            return None

    @classmethod
    def in_week(cls, week: CalendarWeek) -> Dict[int, Optional["Holiday"]]:
        per_weekday = {}

        for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
            holiday_date = week[weekday]
            holidays = Holiday.objects.on_day(holiday_date)
            if holidays.exists():
                per_weekday[weekday] = holidays[0]

        return per_weekday

    def __str__(self):
        return self.title

Tom Teichler's avatar
Tom Teichler committed
        ordering = ["date_start"]
        indexes = [models.Index(fields=["date_start", "date_end"])]
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Holidays")
Tom Teichler's avatar
Tom Teichler committed


class SupervisionArea(ExtensibleModel):
    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
    name = models.CharField(verbose_name=_("Long name"), max_length=255)
    colour_fg = ColorField(default="#000000")
Tom Teichler's avatar
Tom Teichler committed
    colour_bg = ColorField()

    def __str__(self):
        return f"{self.name} ({self.short_name})"
Tom Teichler's avatar
Tom Teichler committed
    class Meta:
        ordering = ["name"]
        verbose_name = _("Supervision area")
        verbose_name_plural = _("Supervision areas")


class Break(ValidityRangeRelatedExtensibleModel):
    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
    name = models.CharField(verbose_name=_("Long name"), max_length=255)
Tom Teichler's avatar
Tom Teichler committed
    after_period = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("Time period after break starts"),
        related_name="break_after",
        blank=True,
        null=True,
    )
    before_period = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("Time period before break ends"),
        related_name="break_before",
        blank=True,
        null=True,
    )
Tom Teichler's avatar
Tom Teichler committed
    @property
    def weekday(self):
Jonathan Weth's avatar
Jonathan Weth committed
        return (
            self.after_period.weekday
            if self.after_period
            else self.before_period.weekday
        )
Tom Teichler's avatar
Tom Teichler committed
    @property
Jonathan Weth's avatar
Jonathan Weth committed
        return (
            self.after_period.period
            if self.after_period
            else self.before_period.period - 1
        )
Tom Teichler's avatar
Tom Teichler committed
    @property
    def before_period_number(self):
Jonathan Weth's avatar
Jonathan Weth committed
        return (
            self.before_period.period
            if self.before_period
            else self.after_period.period + 1
        )
Tom Teichler's avatar
Tom Teichler committed
    @property
    def time_start(self):
        return self.after_period.time_end if self.after_period else None

Tom Teichler's avatar
Tom Teichler committed
    @property
    def time_end(self):
        return self.before_period.time_start if self.before_period else None

    @classmethod
    def get_breaks_dict(cls) -> Dict[int, Tuple[datetime, datetime]]:
        breaks = {}
        for break_ in cls.objects.all():
            breaks[break_.before_period_number] = break_

        return breaks

    def __str__(self):
        return f"{self.name} ({self.short_name})"
    class Meta:
        ordering = ["after_period"]
        indexes = [models.Index(fields=["after_period", "before_period"])]
        verbose_name = _("Break")
        verbose_name_plural = _("Breaks")


class Supervision(ValidityRangeRelatedExtensibleModel):
    objects = CurrentSiteManager.from_queryset(SupervisionQuerySet)()
Tom Teichler's avatar
Tom Teichler committed
    area = models.ForeignKey(
        SupervisionArea,
        models.CASCADE,
        verbose_name=_("Supervision area"),
        related_name="supervisions",
    )
    break_item = models.ForeignKey(
        Break, models.CASCADE, verbose_name=_("Break"), related_name="supervisions"
    )
    teacher = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "core.Person",
        models.CASCADE,
        related_name="supervisions",
        verbose_name=_("Teacher"),
Tom Teichler's avatar
Tom Teichler committed
    )
Jonathan Weth's avatar
Jonathan Weth committed
    def get_substitution(
        self, week: Optional[int] = None
    ) -> Optional[SupervisionSubstitution]:
        wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week
        wanted_week = CalendarWeek(week=wanted_week)
        # We iterate over all substitutions because this can make use of
        # prefetching when this model is loaded from outside, in contrast
        # to .filter()
        for substitution in self.substitutions.all():
            for weekday in range(0, 7):
                if substitution.date == wanted_week[weekday]:
                    return substitution
        return None

Tom Teichler's avatar
Tom Teichler committed
    @property
    def teachers(self):
        return [self.teacher]

    def __str__(self):
        return f"{self.break_item}, {self.area}, {self.teacher}"
    class Meta:
        ordering = ["area", "break_item"]
Tom Teichler's avatar
Tom Teichler committed
        verbose_name = _("Supervision")
        verbose_name_plural = _("Supervisions")


class SupervisionSubstitution(ExtensibleModel):
    date = models.DateField(verbose_name=_("Date"))
Tom Teichler's avatar
Tom Teichler committed
    supervision = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        Supervision,
        models.CASCADE,
        verbose_name=_("Supervision"),
        related_name="substitutions",
Tom Teichler's avatar
Tom Teichler committed
    )
    teacher = models.ForeignKey(
        "core.Person",
        models.CASCADE,
        related_name="substituted_supervisions",
        verbose_name=_("Teacher"),
    )
Tom Teichler's avatar
Tom Teichler committed
    @property
    def teachers(self):
        return [self.teacher]

    def __str__(self):
        return f"{self.supervision}, {date_format(self.date)}"
    class Meta:
        ordering = ["date", "supervision"]
        verbose_name = _("Supervision substitution")
        verbose_name_plural = _("Supervision substitutions")
class Event(
    SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin
):
    objects = CurrentSiteManager.from_queryset(EventQuerySet)()
Jonathan Weth's avatar
Jonathan Weth committed
    title = models.CharField(
        verbose_name=_("Title"), max_length=255, blank=True, null=True
    )
Jonathan Weth's avatar
Jonathan Weth committed
    date_start = models.DateField(verbose_name=_("Start date"), null=True)
    date_end = models.DateField(verbose_name=_("End date"), null=True)
Tom Teichler's avatar
Tom Teichler committed
    period_from = models.ForeignKey(
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("Start time period"),
        related_name="+",
    )
    period_to = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "TimePeriod",
        on_delete=models.CASCADE,
        verbose_name=_("End time period"),
        related_name="+",
Tom Teichler's avatar
Tom Teichler committed
    )
Jonathan Weth's avatar
Jonathan Weth committed
    groups = models.ManyToManyField(
        "core.Group", related_name="events", verbose_name=_("Groups")
    )
    rooms = models.ManyToManyField(
        "Room", related_name="events", verbose_name=_("Rooms")
    )
Tom Teichler's avatar
Tom Teichler committed
    teachers = models.ManyToManyField(
        "core.Person", related_name="events", verbose_name=_("Teachers")
    )
Tom Teichler's avatar
Tom Teichler committed

    def __str__(self):
        if self.title:
            return self.title
        else:
            return _(f"Event {self.pk}")
Tom Teichler's avatar
Tom Teichler committed
    @property
    def period_from_on_day(self) -> int:
        day = getattr(self, "_date", timezone.now().date())
        if day != self.date_start:
            return TimePeriod.period_min
        else:
            return self.period_from.period

Tom Teichler's avatar
Tom Teichler committed
    @property
    def period_to_on_day(self) -> int:
        day = getattr(self, "_date", timezone.now().date())
        if day != self.date_end:
            return TimePeriod.period_max
        else:
            return self.period_to.period

Tom Teichler's avatar
Tom Teichler committed
    class Meta:
Tom Teichler's avatar
Tom Teichler committed
        ordering = ["date_start"]
Jonathan Weth's avatar
Jonathan Weth committed
        indexes = [
            models.Index(fields=["period_from", "period_to", "date_start", "date_end"])
        ]
Tom Teichler's avatar
Tom Teichler committed
        verbose_name = _("Event")
Jonathan Weth's avatar
Jonathan Weth committed
        verbose_name_plural = _("Events")
class ExtraLesson(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin):
    label_ = "extra_lesson"

    objects = CurrentSiteManager.from_queryset(ExtraLessonQuerySet)()
Jonathan Weth's avatar
Jonathan Weth committed
    week = models.IntegerField(
        verbose_name=_("Week"), default=CalendarWeek.current_week
    )
Tom Teichler's avatar
Tom Teichler committed
    period = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "TimePeriod",
        models.CASCADE,
        related_name="extra_lessons",
        verbose_name=_("Time period"),
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler committed
    subject = models.ForeignKey(
        "Subject",
        on_delete=models.CASCADE,
        related_name="extra_lessons",
        verbose_name=_("Subject"),
    )
    groups = models.ManyToManyField(
        "core.Group", related_name="extra_lessons", verbose_name=_("Groups")
    )
    teachers = models.ManyToManyField(
Jonathan Weth's avatar
Jonathan Weth committed
        "core.Person",
        related_name="extra_lessons_as_teacher",
        verbose_name=_("Teachers"),
Tom Teichler's avatar
Tom Teichler committed
    )
    room = models.ForeignKey(
Jonathan Weth's avatar
Jonathan Weth committed
        "Room",
        models.CASCADE,
        null=True,
        related_name="extra_lessons",
        verbose_name=_("Room"),
Tom Teichler's avatar
Tom Teichler committed
    )
Jonathan Weth's avatar
Jonathan Weth committed
    comment = models.CharField(
        verbose_name=_("Comment"), blank=True, null=True, max_length=255
    )

    def __str__(self):
        return f"{self.week}, {self.period}, {self.subject}"

    class Meta:
        verbose_name = _("Extra lesson")
        verbose_name_plural = _("Extra lessons")
class ChronosGlobalPermissions(ExtensibleModel):
    class Meta:
        managed = False
        permissions = (
            ("view_all_timetables", _("Can view all timetables")),
            ("view_timetable_overview", _("Can view timetable overview")),
            ("view_lessons_day", _("Can view all lessons per day")),
        )