Skip to content
Snippets Groups Projects
models.py 32.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, List, 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.formats import date_format
Jonathan Weth's avatar
Jonathan Weth committed
from django.utils.functional import classproperty
from django.utils.translation import gettext_lazy as _
Jonathan Weth's avatar
Jonathan Weth committed
from cache_memoize import cache_memoize
from calendarweek.django import (
    CalendarWeek,
    i18n_day_abbr_choices_lazy,
    i18n_day_name_choices_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,
    BreakManager,
Hangzhi Yu's avatar
Hangzhi Yu committed
    CurrentSiteManager,
Jonathan Weth's avatar
Jonathan Weth committed
    EventManager,
Tom Teichler's avatar
Tom Teichler committed
    EventQuerySet,
Jonathan Weth's avatar
Jonathan Weth committed
    ExtraLessonManager,
Tom Teichler's avatar
Tom Teichler committed
    ExtraLessonQuerySet,
    GroupPropertiesMixin,
    HolidayQuerySet,
    LessonPeriodManager,
    LessonPeriodQuerySet,
    LessonSubstitutionManager,
    LessonSubstitutionQuerySet,
Jonathan Weth's avatar
Jonathan Weth committed
    SupervisionManager,
Tom Teichler's avatar
Tom Teichler committed
    SupervisionQuerySet,
Jonathan Weth's avatar
Jonathan Weth committed
    SupervisionSubstitutionManager,
Tom Teichler's avatar
Tom Teichler committed
    TeacherPropertiesMixin,
Jonathan Weth's avatar
Jonathan Weth committed
    ValidityRangeQuerySet,
from aleksis.apps.chronos.mixins import (
    ValidityRangeRelatedExtensibleModel,
    WeekAnnotationMixin,
    WeekRelatedMixin,
)
from aleksis.apps.chronos.util.date import get_current_year
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:
Jonathan Weth's avatar
Jonathan Weth committed
            qs = 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 = i18n_day_name_choices_lazy()
    WEEKDAY_CHOICES_SHORT = i18n_day_abbr_choices_lazy()
Jonathan Weth's avatar
Jonathan Weth committed
    weekday = models.PositiveSmallIntegerField(
        verbose_name=_("Week day"), choices=i18n_day_name_choices_lazy()
Jonathan Weth's avatar
Jonathan Weth committed
    )
    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[CalendarWeek] = None) -> date:
        if isinstance(week, CalendarWeek):
            wanted_week = week
        else:
            year = getattr(self, "_year", None) or date.today().year
            week_number = getattr(self, "_week", None) or CalendarWeek().week

            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:
Loading
Loading full blame...