from datetime import datetime from typing import Dict, Optional, Tuple from django.core import validators from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from biscuit.core.mixins import SchoolRelated from .util import current_week class TimePeriod(SchoolRelated): WEEKDAY_CHOICES = [ (0, _('Sunday')), (1, _('Monday')), (2, _('Tuesday')), (3, _('Wednesday')), (4, _('Thursday')), (5, _('Friday')), (6, _('Saturday')) ] weekday = models.PositiveSmallIntegerField(verbose_name=_( 'Week day'), choices=WEEKDAY_CHOICES) period = models.PositiveSmallIntegerField( verbose_name=_('Number of period')) time_start = models.TimeField(verbose_name=_('Time the period starts')) time_end = models.TimeField(verbose_name=_('Time the period ends')) def __str__(self) -> str: return '%s, %d. period (%s - %s)' % (self.weekday, self.period, self.time_start, self.time_end) @classmethod 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 class Meta: unique_together = [['school', 'weekday', 'period']] ordering = ['weekday', 'period'] indexes = [models.Index(fields=['time_start', 'time_end'])] class Subject(SchoolRelated): abbrev = models.CharField(verbose_name=_( 'Abbreviation of subject in timetable'), max_length=10) name = models.CharField(verbose_name=_( 'Long name of subject'), max_length=30) colour_fg = models.CharField(verbose_name=_('Foreground colour in timetable'), blank=True, validators=[ validators.RegexValidator(r'#[0-9A-F]{6}')], max_length=7) colour_bg = models.CharField(verbose_name=_('Background colour in timetable'), blank=True, validators=[ validators.RegexValidator(r'#[0-9A-F]{6}')], max_length=7) def __str__(self) -> str: return '%s - %s' % (self.abbrev, self.name) class Meta: ordering = ['name', 'abbrev'] unique_together = [['school', 'abbrev'], ['school', 'name']] class Room(SchoolRelated): short_name = models.CharField(verbose_name=_( 'Short name, e.g. room number'), max_length=10) name = models.CharField(verbose_name=_('Long name'), max_length=30) def __str__(self) -> str: return '%s (%s)' % (self.name, self.short_name) class Meta: ordering = ['name', 'short_name'] unique_together = [['school', 'short_name'], ['school', 'name']] class Lesson(SchoolRelated): subject = models.ForeignKey( 'Subject', on_delete=models.CASCADE, related_name='lessons') teachers = models.ManyToManyField('core.Person', related_name='lessons') periods = models.ManyToManyField( 'TimePeriod', related_name='lessons', through='LessonPeriod') groups = models.ManyToManyField('core.Group', related_name='lessons') date_start = models.DateField(verbose_name=_( 'Effective start date of lesson'), null=True) date_end = models.DateField(verbose_name=_( 'Effective end date of lesson'), null=True) @property def teacher_names(self, sep: Optional[str] = ', ') -> str: return sep.join([teacher.full_name for teacher in self.teachers.all()]) @property def group_names(self, sep: Optional[str] = ', ') -> str: return sep.join([group.short_name for group in self.groups.all()]) class Meta: ordering = ['date_start'] indexes = [models.Index(fields=['date_start', 'date_end'])] class LessonSubstitution(SchoolRelated): week = models.IntegerField(verbose_name=_('Week'), default=current_week) lesson_period = models.ForeignKey( 'LessonPeriod', models.CASCADE, 'substitutions') subject = models.ForeignKey( 'Subject', on_delete=models.CASCADE, related_name='lesson_substitutions', null=True, blank=True) teachers = models.ManyToManyField('core.Person', related_name='lesson_substitutions', blank=True, null=True) room = models.ForeignKey('Room', models.CASCADE, null=True, blank=True) cancelled = models.BooleanField(default=False) def clean(self) -> None: if self.subject and self.cancelled: raise ValidationError(_('Lessons can only be either substituted or cancelled.')) class Meta: unique_together = [['school', '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' ) ] class LessonPeriod(SchoolRelated): lesson = models.ForeignKey('Lesson', models.CASCADE, related_name='lesson_periods') period = models.ForeignKey('TimePeriod', models.CASCADE, related_name='lesson_periods') room = models.ForeignKey('Room', models.CASCADE, null=True, related_name='lesson_periods') def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution: wanted_week = week or getattr(self, '_week', None) or current_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(): if substitution.week == wanted_week: return substitution return None def get_subject(self) -> Optional[Subject]: if self.get_substitution(): return self.get_substitution().subject else: return self.lesson.subject def get_teachers(self) -> models.query.QuerySet: if self.get_substitution(): return self.get_substitution().teachers else: return self.lesson.teachers def get_room(self) -> Optional[Room]: if self.get_substitution(): return self.get_substitution().room else: return self.room def get_groups(self) -> models.query.QuerySet: return self.lesson.groups def __str__(self) -> str: return '%s, %d., %s, %s' % (self.period.get_weekday_display(), self.period.period, ', '.join(list(self.lesson.groups.values_list('short_name', flat=True))), self.lesson.subject.name) class Meta: ordering = ['lesson__date_start', 'period__weekday', 'period__period'] indexes = [models.Index(fields=['lesson', 'period'])]