Skip to content
Snippets Groups Projects
models.py 25 KiB
Newer Older
Tom Teichler's avatar
Tom Teichler committed
from datetime import date, datetime, time, timedelta
from typing import Dict, Optional, Tuple, Union
from django.contrib.sites.managers import CurrentSiteManager
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,
    EventQuerySet,
    ExtraLessonQuerySet,
    GroupPropertiesMixin,
    HolidayQuerySet,
    LessonPeriodManager,
    LessonPeriodQuerySet,
    LessonSubstitutionManager,
    LessonSubstitutionQuerySet,
    SupervisionQuerySet,
    TeacherPropertiesMixin,
)
from aleksis.apps.chronos.util.format import format_m2m
from aleksis.core.mixins import ExtensibleModel
Tom Teichler's avatar
Tom Teichler committed
from aleksis.core.models import DashboardWidget
from aleksis.core.util.core_helpers import has_person
class TimePeriod(ExtensibleModel):
    WEEKDAY_CHOICES = list(enumerate(i18n_day_names_lazy()))
    WEEKDAY_CHOICES_SHORT = list(enumerate(i18n_day_abbrs_lazy()))
Tom Teichler's avatar
Tom Teichler 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]]:
        periods = {}
        for period in cls.objects.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 < self.school.current_term.date_start.isocalendar()[1]:
                year += 1

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

        return wanted_week[self.weekday]
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 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

    @classproperty
    def period_min(cls) -> int:
Tom Teichler's avatar
Tom Teichler committed
        return cls.objects.aggregate(period__min=Coalesce(Min("period"), 1)).get("period__min")

    @classproperty
    def period_max(cls) -> int:
Tom Teichler's avatar
Tom Teichler committed
        return cls.objects.aggregate(period__max=Coalesce(Max("period"), 7)).get("period__max")

    @classproperty
    def time_min(cls) -> Optional[time]:
        return cls.objects.aggregate(Min("time_start")).get("time_start__min")

    @classproperty
    def time_max(cls) -> Optional[time]:
Jonathan Weth's avatar
Jonathan Weth committed
        return cls.objects.aggregate(Max("time_end")).get("time_end__max")

    @classproperty
    def weekday_min(cls) -> int:
Tom Teichler's avatar
Tom Teichler committed
        return cls.objects.aggregate(weekday__min=Coalesce(Min("weekday"), 0)).get("weekday__min")

    @classproperty
    def weekday_max(cls) -> int:
Tom Teichler's avatar
Tom Teichler committed
        return cls.objects.aggregate(weekday__max=Coalesce(Max("weekday"), 6)).get("weekday__max")
    class Meta:
        unique_together = [["weekday", "period"]]
        ordering = ["weekday", "period"]
        indexes = [models.Index(fields=["time_start", "time_end"])]
        verbose_name = _("Time period")
        verbose_name_plural = _("Time periods")
class Subject(ExtensibleModel):
Tom Teichler's avatar
Tom Teichler 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):
Tom Teichler's avatar
Tom Teichler 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(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
Tom Teichler's avatar
Tom Teichler committed
    subject = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler 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(
Tom Teichler's avatar
Tom Teichler committed
        "TimePeriod", related_name="lessons", through="LessonPeriod", verbose_name=_("Periods"),
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler committed
    groups = models.ManyToManyField("core.Group", related_name="lessons", verbose_name=_("Groups"))
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)
    def get_calendar_week(self, week: int):
        year = self.date_start.year
        if week < int(self.date_start.strftime("%V")):
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            year += 1

        return CalendarWeek(year=year, week=week)

    def __str__(self):
        return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}"
        ordering = ["date_start", "subject"]
        indexes = [models.Index(fields=["date_start", "date_end"])]
        verbose_name = _("Lesson")
        verbose_name_plural = _("Lessons")
class LessonSubstitution(ExtensibleModel):
    objects = LessonSubstitutionManager.from_queryset(LessonSubstitutionQuerySet)()
Tom Teichler's avatar
Tom Teichler 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(
Tom Teichler's avatar
Tom Teichler committed
        "core.Person", related_name="lesson_substitutions", blank=True, verbose_name=_("Teachers"),
Tom Teichler's avatar
Tom Teichler committed
    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?")
    )
Tom Teichler's avatar
Tom Teichler committed
    comment = models.TextField(verbose_name=_("Comment"), blank=True)
    def clean(self) -> None:
        if self.subject and self.cancelled:
Tom Teichler's avatar
Tom Teichler 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)
        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__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(
Tom Teichler's avatar
Tom Teichler committed
        "Lesson", models.CASCADE, related_name="lesson_periods", verbose_name=_("Lesson"),
Tom Teichler's avatar
Tom Teichler committed
    )
    period = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler 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(
Tom Teichler's avatar
Tom Teichler 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}"
Tom Teichler's avatar
Tom Teichler committed
        ordering = [
            "lesson__date_start",
            "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}
Tom Teichler's avatar
Tom Teichler 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)
    name = models.CharField(verbose_name=_("Name"), blank=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")
    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="+",
    )
Tom Teichler's avatar
Tom Teichler committed
    comment = models.TextField(verbose_name=_("Comment"), blank=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")
Tom Teichler's avatar
Tom Teichler committed
    lesson = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler 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)
Tom Teichler's avatar
Tom Teichler committed
    comment = models.TextField(verbose_name=_("Comment"), blank=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)
Tom Teichler's avatar
Tom Teichler committed
    comments = models.TextField(verbose_name=_("Comments"), blank=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(ExtensibleModel):
    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):
Tom Teichler's avatar
Tom Teichler committed
        return self.after_period.weekday if self.after_period else self.before_period.weekday
Tom Teichler's avatar
Tom Teichler committed
    @property
Tom Teichler's avatar
Tom Teichler 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):
Tom Teichler's avatar
Tom Teichler 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(ExtensibleModel):
    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(
Tom Teichler's avatar
Tom Teichler committed
        "core.Person", models.CASCADE, related_name="supervisions", verbose_name=_("Teacher"),
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler 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(
Tom Teichler's avatar
Tom Teichler 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(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
    objects = CurrentSiteManager.from_queryset(EventQuerySet)()
Tom Teichler's avatar
Tom Teichler committed
    title = models.CharField(verbose_name=_("Title"), max_length=255, blank=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(
Tom Teichler's avatar
Tom Teichler committed
        "TimePeriod", on_delete=models.CASCADE, verbose_name=_("End time period"), related_name="+",
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler 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"]
Tom Teichler's avatar
Tom Teichler 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(ExtensibleModel, GroupPropertiesMixin):
    label_ = "extra_lesson"

    objects = CurrentSiteManager.from_queryset(ExtraLessonQuerySet)()
Tom Teichler's avatar
Tom Teichler committed
    week = models.IntegerField(verbose_name=_("Week"), default=CalendarWeek.current_week)
Tom Teichler's avatar
Tom Teichler committed
    period = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler 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(
Tom Teichler's avatar
Tom Teichler committed
        "core.Person", related_name="extra_lessons_as_teacher", verbose_name=_("Teachers"),
Tom Teichler's avatar
Tom Teichler committed
    )
    room = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler committed
        "Room", models.CASCADE, null=True, related_name="extra_lessons", verbose_name=_("Room"),
Tom Teichler's avatar
Tom Teichler committed
    )
Tom Teichler's avatar
Tom Teichler committed
    comment = models.CharField(verbose_name=_("Comment"), blank=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")),
        )