diff --git a/aleksis/apps/chronos/migrations/0010_absence_reason_name.py b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py new file mode 100644 index 0000000000000000000000000000000000000000..7c57327a2e19c8ff2865d14fabcd65acd317146f --- /dev/null +++ b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.5 on 2020-04-13 13:36 + +from django.db import migrations, models +from django.db.models import F + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0009_extended_data'), + ] + + operations = [ + migrations.AddField( + model_name='absencereason', + name='name', + field=models.CharField(default=F("description"), blank=True, max_length=255, null=True, verbose_name='Name'), + ), + migrations.AddField( + model_name='absencereason', + name='short_name', + field=models.CharField(default=F("title"), max_length=255, verbose_name='Short name'), + preserve_default=False, + ), + migrations.RemoveField( + model_name='absencereason', + name='description', + ), + migrations.RemoveField( + model_name='absencereason', + name='title', + ), + ] diff --git a/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py new file mode 100644 index 0000000000000000000000000000000000000000..008a723af7c68f96dbff9f8b149fb3c36305028e --- /dev/null +++ b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.5 on 2020-04-13 15:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0010_absence_reason_name'), + ] + + operations = [ + migrations.RemoveField( + model_name='absence', + name='person', + ), + migrations.AddField( + model_name='absence', + name='group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Group'), + ), + migrations.AddField( + model_name='absence', + name='room', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='chronos.Room'), + ), + migrations.AddField( + model_name='absence', + name='teacher', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Person'), + ), + ] diff --git a/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py new file mode 100644 index 0000000000000000000000000000000000000000..4e6611963aab5f79179b0489ca4bd41d25bd4433 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.5 on 2020-04-13 16:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0011_absence_for_groups_and_rooms'), + ] + + operations = [ + migrations.RemoveField( + model_name='event', + name='absence_reason', + ), + ] diff --git a/aleksis/apps/chronos/migrations/0013_event_title_optional.py b/aleksis/apps/chronos/migrations/0013_event_title_optional.py new file mode 100644 index 0000000000000000000000000000000000000000..4bc05c39f8307ca056d03102c8709c018a432f63 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0013_event_title_optional.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-14 16:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0012_event_remove_absence_reason'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='title', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Title'), + ), + ] diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 9e9a6418ba117dcdf5b1ec4603e5d7515f35b674..016204bc19a5beb6f67311bc3c419e4097dee8ef 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -4,6 +4,7 @@ from collections import OrderedDict from datetime import date, datetime, timedelta, time from typing import Dict, Optional, Tuple, Union +from constance import config from django.core import validators from django.core.exceptions import ValidationError from django.db import models @@ -142,18 +143,18 @@ class LessonDataQuerySet(models.QuerySet): def filter_teacher(self, teacher: Union[Person, int]): """ Filter for all lessons given by a certain teacher. """ - return self.filter( - Q(**{self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"),}) - | Q(**{self._period_path + "lesson__teachers": teacher}) - ) + qs1 = self.filter(**{self._period_path + "lesson__teachers": teacher}) + qs2 = self.filter(**{self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"), }) + + return qs1.union(qs2) def filter_room(self, room: Union[Room, int]): """ Filter for all lessons taking part in a certain room. """ - return self.filter( - Q(**{self._subst_path + "room": room, self._subst_path + "week": F("_week"),}) - | Q(**{self._period_path + "room": room}) - ) + qs1 = self.filter(**{self._period_path + "room": room}) + qs2 = self.filter(**{self._subst_path + "room": room, self._subst_path + "week": F("_week"),}) + + return qs1.union(qs2) def annotate_week(self, week: Union[CalendarWeek, int]): """ Annotate all lessons in the QuerySet with the number of the provided calendar week. """ @@ -221,24 +222,24 @@ class LessonPeriodQuerySet(LessonDataQuerySet): if type_ == "teacher": # Teacher - return person.lesson_periods_as_teacher + return self.filter_teacher(person) elif type_ == "group": # Student - return person.lesson_periods_as_participant + return self.filter(lesson__groups__members=person) else: # If no student or teacher return None def daily_lessons_for_person(self, person: Person, wanted_day: date) -> Optional[models.QuerySet]: - lesson_periods = LessonPeriod.objects.filter_from_person(person) - - if lesson_periods is None: + if person.timetable_type is None: return None - return lesson_periods.on_day(wanted_day) + lesson_periods = self.on_day(wanted_day).filter_from_person(person) + + return lesson_periods def per_period_one_day(self) -> OrderedDict: """ Group selected lessons per period for one day """ @@ -442,6 +443,18 @@ class Lesson(ExtensibleModel): def group_names(self, sep: Optional[str] = ", ") -> str: return sep.join([group.short_name for group in self.groups.all()]) + @property + def groups_to_show(self) -> models.QuerySet: + groups = self.groups.all() + if groups.count() == 1 and groups[0].parent_groups.all() and config.CHRONOS_USE_PARENT_GROUPS: + return groups[0].parent_groups.all() + else: + return groups + + @property + def groups_to_show_names(self, sep: Optional[str] = ", ") -> str: + return sep.join([group.short_name for group in self.groups_to_show]) + def get_calendar_week(self, week: int): year = self.date_start.year if week < int(self.date_start.strftime("%V")): @@ -485,9 +498,10 @@ class LessonSubstitution(ExtensibleModel): @property def type_(self): - # TODO: Add cases events and supervisions if self.cancelled: return "cancellation" + elif self.cancelled_for_teachers: + return "cancellation_for_teachers" else: return "substitution" @@ -508,6 +522,8 @@ class LessonSubstitution(ExtensibleModel): class LessonPeriod(ExtensibleModel): + label_ = "lesson_period" + objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)() lesson = models.ForeignKey("Lesson", models.CASCADE, related_name="lesson_periods") @@ -567,21 +583,25 @@ class TimetableWidget(DashboardWidget): template = "chronos/widget.html" def get_context(self): + from aleksis.apps.chronos.util.build import build_timetable # noqa + request = get_request() context = {"has_plan": True} wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) if has_person(request.user): person = request.user.person - - lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day) type_ = person.timetable_type + # Build timetable + timetable = build_timetable("person", person, wanted_day) + if type_ is None: # If no student or teacher, redirect to all timetables context["has_plan"] = False else: - context["lesson_periods"] = lesson_periods.per_period_one_day() + context["timetable"] = timetable + context["holiday"] = Holiday.on_day(wanted_day) context["type"] = type_ context["day"] = wanted_day context["periods"] = TimePeriod.get_times_dict() @@ -600,9 +620,36 @@ class TimetableWidget(DashboardWidget): verbose_name = _("Timetable widget") +class DateRangeQuerySet(models.QuerySet): + def within_dates(self, start: date, end: date): + """ Filter for all events within a date range. """ + + return self.filter(date_start__lte=end, date_end__gte=start) + + def in_week(self, wanted_week: CalendarWeek): + """ Filter for all events within a calendar week. """ + + return self.within_dates(wanted_week[0], wanted_week[6]) + + def on_day(self, day: date): + """ Filter for all events on a certain day. """ + + return self.within_dates(day, day) + + def at_time(self, when: Optional[datetime] = None): + """ Filter for the events taking place at a certain point in time. """ + + now = when or datetime.now() + + return self.on_day(now.date()).filter( + period_from__time_start__lte=now.time(), + period_to__time_end__gte=now.time() + ) + + class AbsenceReason(ExtensibleModel): - title = models.CharField(verbose_name=_("Title"), max_length=50) - description = models.TextField(verbose_name=_("Description"), blank=True, null=True) + short_name = models.CharField(verbose_name=_("Short name"), max_length=255) + name = models.CharField(verbose_name=_("Name"), blank=True, null=True, max_length=255) class Meta: verbose_name = _("Absence reason") @@ -610,7 +657,10 @@ class AbsenceReason(ExtensibleModel): class Absence(ExtensibleModel): reason = models.ForeignKey("AbsenceReason", on_delete=models.CASCADE, related_name="absences") - person = models.ManyToManyField("core.Person", related_name="absences") + + teacher = models.ForeignKey("core.Person", on_delete=models.CASCADE, related_name="absences", null=True, blank=True) + group = models.ForeignKey("core.Group", on_delete=models.CASCADE, related_name="absences", null=True, blank=True) + room = models.ForeignKey("Room", on_delete=models.CASCADE, related_name="absences", null=True, blank=True) date_start = models.DateField(verbose_name=_("Effective start date of absence"), null=True) date_end = models.DateField(verbose_name=_("Effective end date of absence"), null=True) @@ -642,12 +692,38 @@ class Exam(ExtensibleModel): verbose_name_plural = _("Exams") +class HolidayQuerySet(DateRangeQuerySet): + pass + + class Holiday(ExtensibleModel): + objects = models.Manager.from_queryset(HolidayQuerySet)() + title = models.CharField(verbose_name=_("Title of the holidays"), max_length=50) date_start = models.DateField(verbose_name=_("Effective start date of holidays"), null=True) date_end = models.DateField(verbose_name=_("Effective end date of holidays"), null=True) comments = models.TextField(verbose_name=_("Comments"), null=True, blank=True) + @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 + class Meta: ordering = ["date_start"] indexes = [models.Index(fields=["date_start", "date_end"])] @@ -686,6 +762,14 @@ class Break(ExtensibleModel): else self.before_period.weekday ) + @property + def after_period_number(self): + return ( + self.after_period.period + if self.after_period + else self.before_period.period - 1 + ) + @property def before_period_number(self): return ( @@ -694,6 +778,22 @@ class Break(ExtensibleModel): else self.after_period.period + 1 ) + @property + def time_start(self): + return self.after_period.time_end if self.after_period else None + + @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 + class Meta: ordering = ["after_period"] indexes = [models.Index(fields=["after_period", "before_period"])] @@ -701,11 +801,64 @@ class Break(ExtensibleModel): verbose_name_plural = _("Breaks") +class SupervisionQuerySet(models.QuerySet): + def annotate_week(self, week: Union[CalendarWeek, int]): + """ Annotate all lessons in the QuerySet with the number of the provided calendar week. """ + + if isinstance(week, CalendarWeek): + week_num = week.week + else: + week_num = week + + return self.annotate(_week=models.Value(week_num, models.IntegerField())) + + def filter_by_weekday(self, weekday: int): + self.filter( + Q(break_item__before_period__weekday=weekday) + | Q(break_item__after_period__weekday=weekday) + ) + + def filter_by_teacher(self, teacher: Union[Person, int]): + """ Filter for all supervisions given by a certain teacher. """ + + if self.count() > 0: + if hasattr(self[0], "_week"): + week = CalendarWeek(week=self[0]._week) + else: + week = CalendarWeek.current_week() + + dates = [week[w] for w in range(0, 7)] + + return self.filter(Q(substitutions__teacher=teacher, substitutions__date__in=dates) | Q(teacher=teacher)) + + return self + + class Supervision(ExtensibleModel): + objects = models.Manager.from_queryset(SupervisionQuerySet)() + 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("core.Person", models.CASCADE, related_name="supervisions", verbose_name=_("Teacher")) + 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 + + @property + def teachers(self): + return [self.teacher] + class Meta: ordering = ["area", "break_item"] verbose_name= _("Supervision") @@ -717,17 +870,82 @@ class SupervisionSubstitution(ExtensibleModel): supervision = models.ForeignKey(Supervision, models.CASCADE, verbose_name=_("Supervision"), related_name="substitutions") teacher = models.ForeignKey("core.Person", models.CASCADE, related_name="substituted_supervisions", verbose_name=_("Teacher")) + @property + def teachers(self): + return [self.teacher] + class Meta: ordering = ["date", "supervision"] verbose_name = _("Supervision substitution") verbose_name_plural = _("Supervision substitutions") +class EventQuerySet(DateRangeQuerySet): + def filter_participant(self, person: Union[Person, int]): + """ Filter for all lessons a participant (student) attends. """ + + return self.filter(Q(groups_members=person)) + + def filter_group(self, group: Union[Group, int]): + """ Filter for all events a group (class) attends. """ + + if isinstance(group, int): + group = Group.objects.get(pk=group) + + if group.parent_groups.all(): + # Prevent to show lessons multiple times + return self.filter(groups=group) + else: + return self.filter(Q(groups=group) | Q(groups__parent_groups=group)) + + def filter_teacher(self, teacher: Union[Person, int]): + """ Filter for all lessons given by a certain teacher. """ + + return self.filter(teachers=teacher) + + def filter_room(self, room: Union[Room, int]): + """ Filter for all lessons taking part in a certain room. """ + + return self.filter(rooms=room) + + def filter_from_type(self, type_: str, pk: int) -> Optional[models.QuerySet]: + if type_ == "group": + return self.filter_group(pk) + elif type_ == "teacher": + return self.filter_teacher(pk) + elif type_ == "room": + return self.filter_room(pk) + else: + return None + + def filter_from_person(self, person: Person) -> Optional[models.QuerySet]: + type_ = person.timetable_type + + if type_ == "teacher": + # Teacher + + return self.filter_teacher(person) + + elif type_ == "group": + # Student + + return self.filter_participant(person) + + else: + # If no student or teacher + return None + + class Event(ExtensibleModel): - title = models.CharField(verbose_name=_("Title"), max_length=50) + label_ = "event" + + objects = models.Manager.from_queryset(EventQuerySet)() + + title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True, null=True) + date_start = models.DateField(verbose_name=_("Effective start date of event"), null=True) date_end = models.DateField(verbose_name=_("Effective end date of event"), null=True) - absence_reason = models.ForeignKey("AbsenceReason", on_delete=models.CASCADE, related_name="absence_reason", verbose_name=_("Absence reason")) + period_from = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Effective start period of event"), related_name="+") period_to = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Effective end period of event"), related_name="+") diff --git a/aleksis/apps/chronos/static/css/chronos/timetable.css b/aleksis/apps/chronos/static/css/chronos/timetable.css index c25d6750eeed8f9965c06b3f6da267916ee4c616..bbdbb5b82c664ca908c2cd2088188c0a64395716 100644 --- a/aleksis/apps/chronos/static/css/chronos/timetable.css +++ b/aleksis/apps/chronos/static/css/chronos/timetable.css @@ -26,6 +26,10 @@ li.active > a > .sidenav-badge { min-height: 65px; } +.supervision-card { + min-height: 35px; +} + .lesson-card .card-content { padding: 0; text-align: center; @@ -53,6 +57,8 @@ li.active > a > .sidenav-badge { .timetable-mobile-title-card { margin-top: 50px; margin-bottom: .60rem; + display: flex; + flex-grow: 1; } .timetable-mobile-title-card:first-child { @@ -112,3 +118,23 @@ table.substitutions td, table.substitutions th { .black-text-a a { color: black; } + +.holiday-badge { + float: left !important; + position: relative; + margin-left: 0 !important; + left: 50%; + transform: translate(-50%); + width: auto; + height: auto !important; + min-height: 26px; + margin-top: 5px; +} + +.holiday-card .card-content{ + width: 100%; +} + +.holiday-card .holiday-badge { + margin-top: 0; +} diff --git a/aleksis/apps/chronos/templates/chronos/partials/elements.html b/aleksis/apps/chronos/templates/chronos/partials/elements.html new file mode 100644 index 0000000000000000000000000000000000000000..a2e55aeceaf612741c6a9520d90e4b025014be92 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/elements.html @@ -0,0 +1,11 @@ +<div class="card lesson-card"> + <div class="card-content"> + {% for element in elements %} + {% if element.label_ == "lesson_period" %} + {% include "chronos/partials/lesson.html" with lesson_period=element %} + {% elif element.label_ == "event" %} + {% include "chronos/partials/event.html" with event=element %} + {% endif %} + {% endfor %} + </div> +</div> diff --git a/aleksis/apps/chronos/templates/chronos/partials/event.html b/aleksis/apps/chronos/templates/chronos/partials/event.html new file mode 100644 index 0000000000000000000000000000000000000000..29b025ed48f4d84acef6094dea0b5b0ec95a6e5d --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/event.html @@ -0,0 +1,37 @@ +<div class="lesson-with-event"> + <p> + {# Teacher or room > Display groups #} + {% if type == "teacher" or type == "room" %} + {% include "chronos/partials/groups.html" with groups=event.groups.all %} + {% endif %} + + {# Class or room > Display teachers #} + {% if type == "room" or type == "group" %} + {% include "chronos/partials/teachers.html" with teachers=event.teachers.all %} + {% endif %} + + {# Teacher or class > Display rooms #} + {% if type == "teacher" or type == "group" %} + {% for room in event.rooms.all %} + <span class="tooltipped" data-position="bottom" data-tooltip="{{ room.name }}"> + <a href="{% url "timetable" "room" room.pk %}"> + {{ room.short_name }} + </a> + </span> + {% endfor %} + {% endif %} + + {% if type == "teacher" and not event.groups.all and not event.rooms.all and event.title %} + <em>{{ event.title }}</em> + {% elif type == "group" and not event.teachers.all and not event.groups.all and event.title %} + <em>{{ event.title }}</em> + {% elif type == "room" and not event.teachers.all and not event.groups.all and event.title %} + <em>{{ event.title }}</em> + {% elif event.title %} + <br/> + <small> + <em>{{ event.title }}</em> + </small> + {% endif %} + </p> +</div> diff --git a/aleksis/apps/chronos/templates/chronos/partials/holiday.html b/aleksis/apps/chronos/templates/chronos/partials/holiday.html new file mode 100644 index 0000000000000000000000000000000000000000..85fc42d4ffd50621da8fe04802b3c2d18a509b71 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/holiday.html @@ -0,0 +1 @@ +<span class="badge new blue center-align holiday-badge">{{ holiday.title }}</span> diff --git a/aleksis/apps/chronos/templates/chronos/partials/lesson.html b/aleksis/apps/chronos/templates/chronos/partials/lesson.html index 8dc4f9fb2f264cfe188241502d298cf3e095ccf3..3eef028d830f3842181c9ccb70dc660488b53981 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/lesson.html +++ b/aleksis/apps/chronos/templates/chronos/partials/lesson.html @@ -1,131 +1,105 @@ {% load i18n %} -<div class="card lesson-card"> - <div class="card-content"> - - {# Every element of the lesson #} - {% for lesson_period in lessons %} - <div style=" - {# Display background color only if no badge exists and it is not the old room and there are no holidays #} - {% if not lesson_period.get_substitution.cancelled and not lesson_period.is_hol %} - {% if not lesson_period.room != lesson_period.get_room or type != 1 %} - {% if lesson_period.lesson.subject.colour_fg %} - color: {{ lesson_period.lesson.subject.colour_fg }}; - {% endif %} - {% if lesson_period.lesson.subject.colour_bg %} - background-color: {{ lesson_period.lesson.subject.colour_bg }}; - {% endif %} - {% endif %} - {% endif %} - " - {# Add CSS class for sub when it's a sub #} - class="{% if lesson_period.get_substitution and smart %}{% if lesson_period.substitution.table.is_event %}lesson-with-event{% else %}lesson-with-sub{% endif %}{% endif %}" - > - <p> - {% if lesson_period.is_hol and smart %} - {# Do nothing #} - {% elif lesson_period.get_substitution and smart %} - {% with sub=lesson_period.get_substitution %} - {# SUBSTITUTION #} - {% if type == "room" and lesson_period.room != lesson_period.get_room %} - {# When it's the old room, let it empty #} - - {% elif lesson_period.get_substitution.cancelled %} - {# When a badge (cancellation, etc.) exists, then display it with the teacher#} - - {# Class or room > Display teacher #} - {% if type == "group" or type == "room" and lesson_period.lesson.teachers.all %} - {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}<br/> - {% endif %} - - {# Badge #} - <span class="badge new green darken-2">{% trans "Cancelled" %}</span> - - {% else %} - {# Display sub #} - - {# Teacher or room > display classes #} - {% if type == "teacher" or type == "room" %} - {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %} - {% endif %} - - {# Display teacher with tooltip #} - {% include "chronos/partials/subs/teachers.html" %} - - {# Display subject #} - {% include "chronos/partials/subs/subject.html" %} - - {# Teacher or class > display room #} - {% if type == "teacher" or type == "group" %} - {% include "chronos/partials/subs/room.html" %} - {% endif %} - {% endif %} - - - {# When it isn't a room or the old plan, then display the extra text (e. g. work orders) AND NOT A EVENT#} - {% if not lesson_period.substitution.table.is_event %} - {% if not type == "room" or not lesson_period.is_old %} - <br> - <small> - <em>{{ lesson_period.substitution.table.text|default:"" }}</em> - </small> - {% endif %} - {% endif %} - {# Display the extra text for events #} - {% if lesson_period.substitution.table.is_event %} - {% if type == 0 and lesson_period.substitution.table.classes == "" and lesson_period.substitution.table.rooms|length == 0 and lesson_period.substitutions.table.teachers|length == 0 %} - <em>{{ lesson_period.substitution.table.text|default:"" }}</em> - {% elif type == 2 and lesson_period.substitution.table.teachers|length == 0 and lesson_period.substitution.table.rooms|length == 0 %} - <em>{{ lesson_period.substitution.table.text|default:"" }}</em> - {% elif type == 1 and lesson_period.substitution.table.teachers|length == 0 and lesson_period.substitution.table.classes == "" %} - <em>{{ lesson_period.substitution.table.text|default:"" }}</em> - {% else %} - <br> - <small> - <em>{{ lesson_period.substitution.table.text|default:"" }}</em> - </small> - {% endif %} - {% endif %} - {% endwith %} - - - {% else %} - {# Normal plan #} - - {# Teacher or room > Display classes #} - {% if type == "teacher" or type == "room" %} - {# {{ element_container.element.classes }}#} - {% if lesson_period.lesson.groups %} - {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %} - {% endif %} +<div style=" + {# Display background color only if no badge exists and it is not the old room and there are no holidays #} + {% if not lesson_period.get_substitution.cancelled and not lesson_period.get_substitution.cancelled_for_teachers %} + {% if not type == "room" or lesson_period.room == lesson_period.get_room or lesson_period.get_room == el %} + {% if lesson_period.lesson.subject.colour_fg %} + color: {{ lesson_period.lesson.subject.colour_fg }}; {% endif %} - - {# Class or room > Display teacher #} - {% if type == "room" or type == "group" %} - {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %} + {% if lesson_period.lesson.subject.colour_bg %} + background-color: {{ lesson_period.lesson.subject.colour_bg }}; {% endif %} + {% endif %} + {% endif %} + " + {# Add CSS class for sub when it's a sub #} + class="{% if lesson_period.get_substitution and smart %}lesson-with-sub{% endif %}" +> + <p> + {% if lesson_period.is_hol and smart %} + {# Do nothing #} + {% elif lesson_period.get_substitution and smart %} + {% with sub=lesson_period.get_substitution %} + {# SUBSTITUTION #} + {% if type == "room" and lesson_period.room != lesson_period.get_room and lesson_period.get_room != el %} + {# When it's the old room, let it empty #} + + {% elif sub.cancelled or sub.cancelled_for_teachers %} + {# When a badge (cancellation, etc.) exists, then display it with the teacher#} + + {# Class or room > Display teacher #} + {% if type == "group" or type == "room" and lesson_period.lesson.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}<br/> + {% endif %} + + {# Badge #} + {% include "chronos/partials/subs/badge.html" with sub=sub %} + + {% else %} + {# Display sub #} + + {# Teacher or room > display classes #} + {% if type == "teacher" or type == "room" %} + {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %} + {% endif %} + + {# Display teacher with tooltip #} + {% include "chronos/partials/subs/teachers.html" with type="substitution" el=sub %} + + {# Display subject #} + {% include "chronos/partials/subs/subject.html" with type="substitution" el=sub %} + + {# Teacher or class > display room #} + {% if type == "teacher" or type == "group" %} + {% include "chronos/partials/subs/room.html" with type="substitution" el=sub %} + {% endif %} + {% endif %} + + + {# When it isn't a room or the old plan, then display the extra text (e. g. work orders) #} + {% if not lesson_period.room == lesson_period.get_room and lesson_period.get_room != el and sub.comment %} + <br> + <small> + <em>{{ lesson_period.get_substitution.comment }}</em> + </small> + {% endif %} + {% endwith %} + + + {% else %} + {# Normal plan #} - {# Display subject #} - <strong> + {# Teacher or room > Display classes #} + {% if type == "teacher" or type == "room" %} + {# {{ element_container.element.classes }}#} + {% if lesson_period.lesson.groups %} + {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %} + {% endif %} + {% endif %} + + {# Class or room > Display teacher #} + {% if type == "room" or type == "group" %} + {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %} + {% endif %} + + {# Display subject #} + <strong> <span data-position="bottom" class="tooltipped" data-tooltip="{{ lesson_period.lesson.subject.name }}">{{ lesson_period.lesson.subject.abbrev }}</span> - </strong> + </strong> - {# Teacher or class > Display room #} - {% if type == "teacher" or type == "group" %} - {% if lesson_period.room %} - <span class="tooltipped" data-position="bottom" - data-tooltip="{{ lesson_period.room.name }}"> + {# Teacher or class > Display room #} + {% if type == "teacher" or type == "group" %} + {% if lesson_period.room %} + <span class="tooltipped" data-position="bottom" + data-tooltip="{{ lesson_period.room.name }}"> <a href="{% url "timetable" "room" lesson_period.room.pk %}"> {{ lesson_period.room.short_name }} </a> </span> - {% endif %} - {% endif %} - {% endif %} - </p> - </div> - - {% endfor %} - </div> + {% endif %} + {% endif %} + {% endif %} + </p> </div> diff --git a/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html index abb2710a6a147d2bbc67ab17be784a6c52f5da69..25ddedc47f422916b2e44f5419151e1a36ce8332 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html +++ b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html @@ -1,10 +1,30 @@ -{% for period, lessons in lesson_periods.items %} +{% if holiday %} <div class="row"> - <div class="col s4"> - {% include "chronos/partials/period_time.html" with period=period periods=periods %} - </div> - <div class="col s8"> - {% include "chronos/partials/lesson.html" with lessons=lessons %} + <div class="col s12"> + <div class="card col s12 holiday-card"> + <div class="card-content"> + <p> + {% include "chronos/partials/holiday.html" with holiday=holiday %}<br/> + </p> + </div> + </div> </div> </div> -{% endfor %} +{% else %} + {% for row in timetable %} + <div class="row"> + <div class="col s4"> + {% if row.type == "period" %} + {% include "chronos/partials/period_time.html" with period=row.period periods=periods %} + {% endif %} + </div> + <div class="col s8"> + {% if row.type == "period" %} + {% include "chronos/partials/elements.html" with elements=row.col %} + {% else %} + {% include "chronos/partials/supervision.html" with supervision=row.col %} + {% endif %} + </div> + </div> + {% endfor %} +{% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html b/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html new file mode 100644 index 0000000000000000000000000000000000000000..c04c9dd331eeb4f271ae5e7c212b43a776c71a51 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html @@ -0,0 +1,7 @@ +{% load i18n %} + +{% if sub.cancelled %} + <span class="badge new green">{% trans "Cancelled" %}</span> +{% elif item.el.cancelled_for_teachers %} + <span class="badge new green">{% trans "Cancelled for teachers" %}</span> +{% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html b/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html new file mode 100644 index 0000000000000000000000000000000000000000..61979467194ad62262a9c639d414db0dadefe260 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html @@ -0,0 +1,9 @@ +{% if item.type == "substitution" %} + {% if item.el.cancelled or item.el.cancelled_for_teachers %} + green-text + {% else %} + black-text + {% endif %} +{% elif item.type == "supervision_substitution" %} + blue-text +{% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/period.html b/aleksis/apps/chronos/templates/chronos/partials/subs/period.html new file mode 100644 index 0000000000000000000000000000000000000000..8e8d31bd2c48e14cc28330cc948e3d43882d6f2f --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/period.html @@ -0,0 +1,9 @@ +<strong> + {% if type == "substitution" %} + {{ el.lesson_period.period.period }}. + {% elif type == "supervision_substitution" %} + {% with break=el.supervision.break_item %} + {{ break.after_period_number }}./{{ break.before_period_number }}. + {% endwith %} + {% endif %} +</strong> diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html index cf637b92303dfe31d1c19dc781809f1c3c10dac3..49010706ce2a67d2ca37e3e0752261398090525d 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html @@ -1,47 +1,45 @@ -{% if not sub.is_event %} - {% if sub.sub.type == 3 %} - {# Supervisement #} - {{ sub.sub.corridor.name }} - {% elif sub.sub.type == 1 or sub.sub.type == 2 %} +{% if type == "substitution" %} + {% if el.cancelled or el.cancelled_for_teachers %} {# Canceled lesson: no room #} - {% elif sub.room and sub.lesson_period.room %} + {% elif el.room and el.lesson_period.room %} {# New and old room available #} <span class="tooltipped" data-position="bottom" - data-tooltip="{{ sub.lesson_period.room.name }} → {{ sub.lesson_period.room.name }}"> - <a href="{% url "timetable" "room" sub.lesson_period.room.pk %}"> - <s>{{ sub.lesson_period.room.short_name }}</s> - </a> - → - <a href="{% url "timetable" "room" sub.room.pk %}"> - <strong>{{ sub.room.short_name }}</strong> - </a> - </span> - {% elif sub.room and not sub.lesson_period.room %} + data-tooltip="{{ el.lesson_period.room.name }} → {{ el.lesson_period.room.name }}" + title="{{ el.lesson_period.room.name }} → {{ el.lesson_period.room.name }}"> + <a href="{% url "timetable" "room" el.lesson_period.room.pk %}"> + <s>{{ el.lesson_period.room.short_name }}</s> + </a> + → + <a href="{% url "timetable" "room" el.room.pk %}"> + <strong>{{ el.room.short_name }}</strong> + </a> + </span> + {% elif el.room and not el.lesson_period.room %} {# Only new room available #} <span class="tooltipped" data-position="bottom" - data-tooltip="{{ sub.room.name }}"> - <a href="{% url "timetable" "room" sub.room.pk %}"> - {{ sub.room.short_name }} - </a> - </span> - {% elif not sub.room and not sub.lesson_period.room %} + data-tooltip="{{ el.room.name }}" + title="{{ el.room.name }}"> + <a href="{% url "timetable" "room" el.room.pk %}"> + {{ el.room.short_name }} + </a> + </span> + {% elif not el.room and not el.lesson_period.room %} {# Nothing to view #} {% else %} {# Only old room available #} <span class="tooltipped" data-position="bottom" - data-tooltip="{{ sub.lesson_period.room.name }}"> - <a href="{% url "timetable" "room" sub.lesson_period.room.pk %}"> - {{ sub.lesson_period.room.short_name }} - </a> - </span> + data-tooltip="{{ el.lesson_period.room.name }}" + title="{{ el.lesson_period.room.name }}"> + <a href="{% url "timetable" "room" el.lesson_period.room.pk %}"> + {{ el.lesson_period.room.short_name }} + </a> + </span> {% endif %} -{% else %} - {% for room in sub.rooms %} - <span class="tooltipped" data-position="bottom" - data-tooltip="{{ room.name }}"> - <a href="{% url "timetable_smart_plan" "room" room.id %}"> - <strong>{{ room.short_name }}{% if not forloop.last %},{% endif %}</strong> - </a> - </span> - {% endfor %} +{% elif type == "supervision_substitution" %} + {% with supervision=el.supervision %} + <span data-position="bottom" class="tooltipped" + data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}"> + {{ supervision.area.short_name }} + </span> + {% endwith %} {% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html index 6fe8eb5a02bb7a6afb75adf50012efa9d2712239..e6c9e86d016237276a2d5c894addba10c8a5942f 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html @@ -1,28 +1,28 @@ {% load i18n %} -{% if not sub.sub.is_event %} - {% if sub.sub.type == 3 %} - <strong>{% trans "Supervision" %}</strong> - {% elif not sub.lesson_period.lesson.subject and not sub.subject %} - {% elif sub.sub.type == 1 or sub.sub.type == 2 %} - <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}"> - <s>{{ sub.lesson_period.lesson.subject.abbrev }}</s> - </span> - {% elif sub.subject and sub.lesson_period.lesson.subject %} - <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}"> - <s>{{ sub.lesson_period.lesson.subject.abbrev }}</s> - </span> +{% if type == "substitution" %} + {% if not el.lesson_period.lesson.subject and not el.subject %} + {% elif el.cancelled or el.cancelled_for_teachers %} + <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}"> + <s>{{ el.lesson_period.lesson.subject.abbrev }}</s> + </span> + {% elif el.subject and el.lesson_period.lesson.subject %} + <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}"> + <s>{{ el.lesson_period.lesson.subject.abbrev }}</s> + </span> → - <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.subject.name }}"> - <strong>{{ sub.subject.abbrev }}</strong> - </span> - {% elif sub.subject and not sub.lesson_period.lesson.subject %} - <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.subject.name }}"> - <strong>{{ sub.subject.abbrev }}</strong> - </span> + <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.subject.name }}"> + <strong>{{ el.subject.abbrev }}</strong> + </span> + {% elif el.subject and not el.lesson_period.lesson.subject %} + <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.subject.name }}"> + <strong>{{ el.subject.abbrev }}</strong> + </span> {% else %} - <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}"> - <strong>{{ sub.lesson_period.lesson.subject.abbrev }}</strong> - </span> + <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}"> + <strong>{{ el.lesson_period.lesson.subject.abbrev }}</strong> + </span> {% endif %} +{% elif type == "supervision_substitution" %} + {% trans "Supervision" %} {% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html b/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html index ec17d8b811cfad34c1cad90d2a3302aa5752ba53..4fbd63fea66282e17f77870ea3d16ec47fb31da4 100644 --- a/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html +++ b/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html @@ -1,19 +1,25 @@ -{% if not sub.is_event %} - {% if sub.sub.type == 1 and sub.lesson_period.lesson.teachers.all %} - {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %} - {% elif sub.teachers.all and sub.lesson_period.lesson.teachers.all %} +{% if type == "substitution" %} + {% if el.cancelled and el.lesson_period.lesson.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %} + {% elif el.teachers.all and el.lesson_period.lesson.teachers.all %} <s> - {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %} </s> → <strong> - {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=el.teachers.all %} </strong> - {% elif sub.teachers.all and not sub.lesson_period.lesson.teachers.all %} - {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %} - {% elif sub.lesson_period.lesson.teachers.all %} - {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %} + {% elif el.teachers.all and not el.lesson_period.lesson.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=el.teachers.all %} + {% elif el.lesson_period.lesson.teachers.all %} + {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %} {% endif %} -{% else %} - {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %} +{% elif type == "supervision_substitution" %} + <s> + {% include "chronos/partials/teachers.html" with teachers=el.supervision.teachers %} + </s> + → + <strong> + {% include "chronos/partials/teachers.html" with teachers=el.teachers %} + </strong> {% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/partials/supervision.html b/aleksis/apps/chronos/templates/chronos/partials/supervision.html new file mode 100644 index 0000000000000000000000000000000000000000..10ff51888939929b2bb3183c254b5a7838b91a12 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/supervision.html @@ -0,0 +1,29 @@ +{% load i18n %} + +<div class="card lesson-card supervision-card"> + <div class="card-content"> + {% if supervision %} + <div style=" + {% if supervision.area.colour_fg %} + color: {{ supervision.area.colour_fg }}; + {% endif %} + {% if supervision.area.colour_bg %} + background-color: {{ supervision.area.colour_bg }}; + {% endif %} + " class="{% if supervision.get_substitution and smart %}lesson-with-sub{% endif %}"> + <p> + <strong>{% trans "Supervision" %}</strong> + <span data-position="bottom" class="tooltipped" + data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}"> + {{ supervision.area.short_name }} + </span> + {% if supervision.get_substitution and smart %} + {% include "chronos/partials/subs/teachers.html" with type="supervision_substitution" el=supervision.get_substitution %} + {% elif type == "supervision_area" %} + {% include "chronos/partials/teachers.html" with teachers=supervision.teachers %} + {% endif %} + </p> + </div> + {% endif %} + </div> +</div> diff --git a/aleksis/apps/chronos/templates/chronos/partials/week_select.html b/aleksis/apps/chronos/templates/chronos/partials/week_select.html new file mode 100644 index 0000000000000000000000000000000000000000..6739290f11b07a2b787d3789f912bc284d6f2ec2 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/week_select.html @@ -0,0 +1,45 @@ +{% load i18n %} + +<div class="col s12 m6 right"> + <div class="col s2 no-print"> + <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_prev }}"> + <i class="material-icons center">navigate_before</i> + </a> + </div> + + <div class="input-field col s8 no-margin hide-on-med-and-up"> + <select id="calendar-week-1"> + {% for week in weeks %} + <option value="{{ week.week }}" {% if week == wanted_week %} + selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }}) + </option> + {% endfor %} + </select> + </div> + + <div class="input-field col s8 no-margin hide-on-med-and-down"> + <select id="calendar-week-2"> + {% for week in weeks %} + <option value="{{ week.week }}" {% if week == wanted_week %} + selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"SHORT_DATE_FORMAT" }}) + </option> + {% endfor %} + </select> + </div> + + <div class="input-field col s8 no-margin hide-on-small-and-down hide-on-large-only"> + <select id="calendar-week-3"> + {% for week in weeks %} + <option value="{{ week.week }}" {% if week == wanted_week %} + selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"j.n." }}) + </option> + {% endfor %} + </select> + </div> + + <div class="col s2 no-print"> + <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_next }}"> + <i class="material-icons center">navigate_next</i> + </a> + </div> +</div> diff --git a/aleksis/apps/chronos/templates/chronos/substitutions.html b/aleksis/apps/chronos/templates/chronos/substitutions.html index febc6aeb9313a6ac73e75bd2a720093f530ef2a4..eff5da58288939ad8906126ae80666b903f75074 100644 --- a/aleksis/apps/chronos/templates/chronos/substitutions.html +++ b/aleksis/apps/chronos/templates/chronos/substitutions.html @@ -57,37 +57,34 @@ </p> </td> {% endif %} - {% for sub in substitutions %} - <tr class="{% if sub.type_ == "cancellation" %}green-text{% else %}black-text{% endif %}"> - {# TODO: Extend support for blue and purple (supervisions and events) #} + {% for item in substitutions %} + <tr class="{% include "chronos/partials/subs/colour.html" with item=item %}"> + {# TODO: Extend support for purple (events) #} <td> - {% include "chronos/partials/groups.html" with groups=sub.lesson_period.lesson.groups.all %} + {% if item.type == "substitution" %} + {% include "chronos/partials/groups.html" with groups=item.el.lesson_period.lesson.groups.all %} + {% endif %} </td> <td> - <strong> - {{ sub.lesson_period.period.period }}. - </strong> + {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/teachers.html" %} + {% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/subject.html" %} + {% include "chronos/partials/subs/subject.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/room.html" %} + {% include "chronos/partials/subs/room.html" with type=item.type el=item.el %} </td> <td> - {% if sub.cancelled %} - {# TODO: Support other cases#} - <span class="badge new green hide-on-med-and-up">{% trans "Cancelled" %}</span> - {% endif %} - {# <em>{{ sub.text|default:"" }}</em>#} + <span class="hide-on-med-and-up"> + {% include "chronos/partials/subs/badge.html" with sub=item.el %} + </span> + <em>{{ sub.comment|default:"" }}</em> </td> <td class="hide-on-small-and-down"> - {% if sub.cancelled %} - <span class="badge new green darken-2">{% trans "Cancelled" %}</span> - {% endif %} + {% include "chronos/partials/subs/badge.html" with sub=item.el %} </td> </tr> {% endfor %} diff --git a/aleksis/apps/chronos/templates/chronos/substitutions_print.html b/aleksis/apps/chronos/templates/chronos/substitutions_print.html index 400124f2391306d6353af0c2a5819d97ad89a228..14ecd637f2ba24a2916921ab955e2942b366780b 100644 --- a/aleksis/apps/chronos/templates/chronos/substitutions_print.html +++ b/aleksis/apps/chronos/templates/chronos/substitutions_print.html @@ -46,38 +46,35 @@ {% endif %} <tbody> - {% for sub in c.substitutions %} - <tr class="{% if sub.type_ == "cancellation" %}green-text{% else %}black-text{% endif %} "> + {% for item in c.substitutions %} + {% ifchanged item.el.lesson_period.lesson.groups_to_show_names %} + </tbody> + <tbody class="{% cycle "striped" "not-striped" %}"> + {% endifchanged %} + + <tr class="{% include "chronos/partials/subs/colour.html" with item=item %}"> <td> - {% include "chronos/partials/groups.html" with groups=sub.lesson_period.lesson.groups.all %} + {% if item.type == "substitution" %} + {% include "chronos/partials/groups.html" with groups=item.el.lesson_period.lesson.groups.all %} + {% endif %} </td> <td> - <strong> - {{ sub.lesson_period.period.period }}. - </strong> + {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/teachers.html" %} + {% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/subject.html" %} + {% include "chronos/partials/subs/subject.html" with type=item.type el=item.el %} </td> <td> - {% include "chronos/partials/subs/room.html" %} + {% include "chronos/partials/subs/room.html" with type=item.type el=item.el %} </td> <td> - {% if sub.cancelled %} - {# TODO: Support other cases#} - <span class="badge new green">{% trans "Cancelled" %}</span> - {% endif %} - {# <em>{{ sub.text|default:"" }}</em>#} + {% include "chronos/partials/subs/badge.html" with sub=item.el %} + <em>{{ sub.comment|default:"" }}</em> </td> </tr> - - {% ifchanged sub.lesson_period.lesson.groups %} - </tbody> - <tbody class="{% cycle "not-striped" "striped" %}"> - {% endifchanged %} {% endfor %} </tbody> </table> diff --git a/aleksis/apps/chronos/templates/chronos/timetable.html b/aleksis/apps/chronos/templates/chronos/timetable.html index 397eca05446fb5d609144a460a0c6270fe660f40..88b02be677b238d9b05d71e52104f19a12de47bb 100644 --- a/aleksis/apps/chronos/templates/chronos/timetable.html +++ b/aleksis/apps/chronos/templates/chronos/timetable.html @@ -57,51 +57,7 @@ </div> {# Week select #} - <div class="col s12 m6 right"> - <div class="col s2 no-print"> - <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_prev }}"> - <i class="material-icons center">navigate_before</i> - </a> - </div> - - {% with wanted_week=week %} - <div class="input-field col s8 no-margin hide-on-med-and-up"> - <select id="calendar-week-1"> - {% for week in weeks %} - <option value="{{ week.week }}" {% if week == wanted_week %} - selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }}) - </option> - {% endfor %} - </select> - </div> - - <div class="input-field col s8 no-margin hide-on-med-and-down"> - <select id="calendar-week-2"> - {% for week in weeks %} - <option value="{{ week.week }}" {% if week == wanted_week %} - selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }}) - </option> - {% endfor %} - </select> - </div> - - <div class="input-field col s8 no-margin hide-on-small-and-down hide-on-large-only"> - <select id="calendar-week-3"> - {% for week in weeks %} - <option value="{{ week.week }}" {% if week == wanted_week %} - selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n" }}–{{ week.6|date:"j.n" }}) - </option> - {% endfor %} - </select> - </div> - {% endwith %} - - <div class="col s2 no-print"> - <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_next }}"> - <i class="material-icons center">navigate_next</i> - </a> - </div> - </div> + {% include "chronos/partials/week_select.html" with wanted_week=week %} {% else %} {# Show if regular #} @@ -124,34 +80,34 @@ </div> {# Show short weekdays on tablets #} - {% for i, weekday, date in weekdays_short %} + {% for weekday in weekdays_short %} <div class="col s2 hide-on-large-only"> <div class="card timetable-title-card"> <div class="card-content"> - <span class="card-title"> - {{ weekday }} - </span> - {{ date }} - {# {% if day.1 %}#} - {# <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#} - {# {% endif %}#} + <span class="card-title"> + {{ weekday.name }} + </span> + {{ weekday.date }} + {% if weekday.holiday %} + <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %} + {% endif %} </div> </div> </div> {% endfor %} {# Show long weekdays elsewere #} - {% for i, weekday, date in weekdays %} + {% for weekday in weekdays %} <div class="col s2 hide-on-med-only"> <div class="card timetable-title-card"> <div class="card-content"> - <span class="card-title"> - {{ weekday }} - </span> - {{ date }} - {# {% if day.1 %}#} - {# <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#} - {# {% endif %}#} + <span class="card-title"> + {{ weekday.name }} + </span> + {{ weekday.date }} + {% if weekday.holiday %} + <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %} + {% endif %} </div> </div> </div> @@ -159,17 +115,22 @@ </div> {# Lessons #} - {% for period, lesson_periods_period in lesson_periods.items %} - + {% for row in timetable %} <div class="row"> <div class="col s2"> - {% include "chronos/partials/period_time.html" with period=period periods=periods %} + {% if row.type == "period" %} + {% include "chronos/partials/period_time.html" with period=row.period periods=periods %} + {% endif %} </div> - {% for weekday, lessons in lesson_periods_period.items %} + {% for col in row.cols %} {# A lesson #} <div class="col s2"> - {% include "chronos/partials/lesson.html" with lessons=lessons %} + {% if row.type == "period" %} + {% include "chronos/partials/elements.html" with elements=col %} + {% else %} + {% include "chronos/partials/supervision.html" with supervision=col %} + {% endif %} </div> {% endfor %} </div> @@ -178,30 +139,33 @@ {# show 5 seperate ones on mobiles #} <div class="timetable-plan hide-on-med-and-up"> - {% for i, weekday, date in weekdays %} + {% for weekday in weekdays %} <div class="card timetable-mobile-title-card"> <div class="card-content"> - <span class="card-title"> - {{ weekday }} - </span> - {{ date }} - {# {% if day.1 %}#} - {# <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#} - {# {% endif %}#} - {# #} + <span class="card-title"> + {{ weekday.name }} + </span> + {{ weekday.date }} + {% if weekday.holiday %} + <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %} + {% endif %} </div> </div> - {% for period, lesson_periods_period in lesson_periods.items %} + {% for row in timetable %} <div class="row"> <div class="col s4"> - {% include "chronos/partials/period_time.html" with period=period periods=periods %} + {% include "chronos/partials/period_time.html" with period=row.period periods=periods %} </div> - {% for weekday, lessons in lesson_periods_period.items %} - {% if forloop.counter0 == i %} + {% for col in row.cols %} + {% if forloop.counter0 == weekday.key %} <div class="col s8"> {# A lesson #} - {% include "chronos/partials/lesson.html" with lessons=lessons %} + {% if row.type == "period" %} + {% include "chronos/partials/elements.html" with elements=col %} + {% else %} + {% include "chronos/partials/supervision.html" with supervision=col %} + {% endif %} </div> {% endif %} {% endfor %} diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py new file mode 100644 index 0000000000000000000000000000000000000000..7148ce40928eb253a6292c28f470de1dcb9319ac --- /dev/null +++ b/aleksis/apps/chronos/util/build.py @@ -0,0 +1,305 @@ +from collections import OrderedDict +from datetime import date +from typing import Union, List, Tuple + +from calendarweek import CalendarWeek +from django.apps import apps + +from aleksis.core.models import Person + +LessonPeriod = apps.get_model("chronos", "LessonPeriod") +TimePeriod = apps.get_model("chronos", "TimePeriod") +Break = apps.get_model("chronos", "Break") +Supervision = apps.get_model("chronos", "Supervision") +LessonSubstitution = apps.get_model("chronos", "LessonSubstitution") +SupervisionSubstitution = apps.get_model("chronos", "SupervisionSubstitution") +Event = apps.get_model("chronos", "Event") +Holiday = apps.get_model("chronos", "Holiday") + + +def build_timetable( + type_: str, obj: Union[int, Person], date_ref: Union[CalendarWeek, date] +): + needed_breaks = [] + + if not isinstance(obj, int): + pk = obj.pk + else: + pk = obj + + is_person = False + if type_ == "person": + is_person = True + type_ = obj.timetable_type + + # Get matching holidays + if is_person: + holiday = Holiday.on_day(date_ref) + else: + holidays_per_weekday = Holiday.in_week(date_ref) + + # Get matching lesson periods + if is_person: + lesson_periods = LessonPeriod.objects.daily_lessons_for_person(obj, date_ref) + else: + lesson_periods = LessonPeriod.objects.in_week(date_ref).filter_from_type( + type_, obj + ) + + # Sort lesson periods in a dict + lesson_periods_per_period = {} + for lesson_period in lesson_periods: + period = lesson_period.period.period + weekday = lesson_period.period.weekday + + if period not in lesson_periods_per_period: + lesson_periods_per_period[period] = [] if is_person else {} + + if not is_person and weekday not in lesson_periods_per_period[period]: + lesson_periods_per_period[period][weekday] = [] + + if is_person: + lesson_periods_per_period[period].append(lesson_period) + else: + lesson_periods_per_period[period][weekday].append(lesson_period) + + # Get events + if is_person: + events = Event.objects.on_day(date_ref).filter_from_person(obj) + else: + events = Event.objects.in_week(date_ref).filter_from_type(type_, obj) + + # Sort events in a dict + events_per_period = {} + for event in events: + if not is_person and event.date_start < date_ref[TimePeriod.weekday_min]: + # If start date not in current week, set weekday and period to min + weekday_from = TimePeriod.weekday_min + period_from_first_weekday = TimePeriod.period_min + else: + weekday_from = event.date_start.weekday() + period_from_first_weekday = event.period_from.period + + if not is_person and event.date_end > date_ref[TimePeriod.weekday_max]: + # If end date not in current week, set weekday and period to max + weekday_to = TimePeriod.weekday_max + period_to_last_weekday = TimePeriod.period_max + else: + weekday_to = event.date_end.weekday() + period_to_last_weekday = event.period_to.period + + for weekday in range(weekday_from, weekday_to + 1): + if is_person and weekday != date_ref.weekday(): + # If daily timetable for person, skip other weekdays + continue + + if weekday == weekday_from: + # If start day, use start period + period_from = period_from_first_weekday + else: + # If not start day, use min period + period_from = TimePeriod.period_min + + if weekday == weekday_to: + # If end day, use end period + period_to = period_to_last_weekday + else: + # If not end day, use max period + period_to = TimePeriod.period_max + + for period in range(period_from, period_to +1): + if period not in events_per_period: + events_per_period[period] = [] if is_person else {} + + if not is_person and weekday not in events_per_period[period]: + events_per_period[period][weekday] = [] + + if is_person: + events_per_period[period].append(event) + else: + events_per_period[period][weekday].append(event) + + if type_ == "teacher": + # Get matching supervisions + if is_person: + week = CalendarWeek.from_date(date_ref) + else: + week = date_ref + supervisions = Supervision.objects.all().annotate_week(week).filter_by_teacher(obj) + + if is_person: + supervisions.filter_by_weekday(date_ref.weekday()) + + supervisions_per_period_after = {} + for supervision in supervisions: + weekday = supervision.break_item.weekday + period_after_break = supervision.break_item.before_period_number + print(supervision, weekday, period_after_break) + + if period_after_break not in needed_breaks: + needed_breaks.append(period_after_break) + + if ( + not is_person + and period_after_break not in supervisions_per_period_after + ): + supervisions_per_period_after[period_after_break] = {} + + if is_person: + supervisions_per_period_after[period_after_break] = supervision + else: + supervisions_per_period_after[period_after_break][weekday] = supervision + + # Get ordered breaks + breaks = OrderedDict(sorted(Break.get_breaks_dict().items())) + + rows = [] + for period, break_ in breaks.items(): # period is period after break + # Break + if type_ == "teacher" and period in needed_breaks: + row = { + "type": "break", + "after_period": break_.after_period_number, + "before_period": break_.before_period_number, + "time_start": break_.time_start, + "time_end": break_.time_end, + } + + if not is_person: + cols = [] + + for weekday in range( + TimePeriod.weekday_min, TimePeriod.weekday_max + 1 + ): + col = None + if ( + period in supervisions_per_period_after + and weekday not in holidays_per_weekday + ): + if weekday in supervisions_per_period_after[period]: + col = supervisions_per_period_after[period][weekday] + cols.append(col) + + row["cols"] = cols + else: + col = None + if period in supervisions_per_period_after and not holiday: + col = supervisions_per_period_after[period] + row["col"] = col + rows.append(row) + + # Period + if period <= TimePeriod.period_max: + row = { + "type": "period", + "period": period, + "time_start": break_.before_period.time_start, + "time_end": break_.before_period.time_end, + } + + if not is_person: + cols = [] + for weekday in range( + TimePeriod.weekday_min, TimePeriod.weekday_max + 1 + ): + col = [] + + # Add lesson periods + if ( + period in lesson_periods_per_period + and weekday not in holidays_per_weekday + ): + if weekday in lesson_periods_per_period[period]: + col += lesson_periods_per_period[period][weekday] + + # Add events + if ( + period in events_per_period + and weekday not in holidays_per_weekday + ): + if weekday in events_per_period[period]: + col += events_per_period[period][weekday] + + cols.append(col) + + row["cols"] = cols + else: + col = [] + + # Add lesson periods + if period in lesson_periods_per_period and not holiday: + col += lesson_periods_per_period[period] + + # Add events + if period in events_per_period and not holiday: + col += events_per_period[period] + + row["col"] = col + + rows.append(row) + + return rows + + +def build_substitutions_list(wanted_day: date) -> List[dict]: + rows = [] + + subs = LessonSubstitution.objects.on_day(wanted_day).order_by( + "lesson_period__lesson__groups", "lesson_period__period" + ) + + for sub in subs: + if not sub.cancelled_for_teachers: + sort_a = sub.lesson_period.lesson.group_names + else: + sort_a = "Z.{}".format(sub.lesson_period.lesson.teacher_names) + + row = { + "type": "substitution", + "sort_a": sort_a, + "sort_b": "{}".format(sub.lesson_period.period.period), + "el": sub, + } + + rows.append(row) + + # Get supervision substitutions + super_subs = SupervisionSubstitution.objects.filter(date=wanted_day) + + for super_sub in super_subs: + row = { + "type": "supervision_substitution", + "sort_a": "Z.{}".format(super_sub.teacher), + "sort_b": "{}".format(super_sub.supervision.break_item.after_period_number), + "el": super_sub + } + rows.append(row) + + # Sort all items + def sorter(row: dict): + return row["sort_a"] + row["sort_b"] + + rows.sort(key=sorter) + + return rows + + +def build_weekdays( + base: List[Tuple[int, str]], wanted_week: CalendarWeek +) -> List[dict]: + holidays_per_weekday = Holiday.in_week(wanted_week) + + weekdays = [] + for key, name in base[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]: + + weekday = { + "key": key, + "name": name, + "date": wanted_week[key], + "holiday": holidays_per_weekday[key] + if key in holidays_per_weekday + else None, + } + weekdays.append(weekday) + + return weekdays diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index 310b6f44841e28e944bb574caeb86b3feafa69d8..266910e8785a48bfff8ceea85c74109465a485fc 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -16,8 +16,9 @@ from aleksis.core.decorators import admin_required from aleksis.core.models import Person, Group, Announcement from aleksis.core.util import messages from .forms import LessonSubstitutionForm -from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room +from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room, Holiday from .tables import LessonsTable +from .util.build import build_timetable, build_substitutions_list, build_weekdays from .util.js import date_unix from .util.date import CalendarWeek, get_weeks_for_year from aleksis.core.util.core_helpers import has_person @@ -65,17 +66,19 @@ def my_timetable( if has_person(request.user): person = request.user.person + type_ = person.timetable_type - lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day) + # Build timetable + timetable = build_timetable("person", person, wanted_day) - if lesson_periods is None: + if type_ is None: # If no student or teacher, redirect to all timetables return redirect("all_timetables") - type_ = person.timetable_type super_el = person.timetable_object - context["lesson_periods"] = lesson_periods.per_period_one_day() + context["timetable"] = timetable + context["holiday"] = Holiday.on_day(wanted_day) context["super"] = {"type": type_, "el": super_el} context["type"] = type_ context["day"] = wanted_day @@ -120,55 +123,16 @@ def timetable( # TODO: On not used days show next week wanted_week = CalendarWeek() - lesson_periods = LessonPeriod.objects.in_week(wanted_week) - lesson_periods = lesson_periods.filter_from_type(type_, pk) - - # Regroup lesson periods per weekday - per_period = {} - for lesson_period in lesson_periods: - added = False - if lesson_period.period.period in per_period: - if lesson_period.period.weekday in per_period[lesson_period.period.period]: - per_period[lesson_period.period.period][ - lesson_period.period.weekday - ].append(lesson_period) - added = True - - if not added: - per_period.setdefault(lesson_period.period.period, {})[ - lesson_period.period.weekday - ] = [lesson_period] - - # Fill in empty lessons - for period_num in range(TimePeriod.period_min, TimePeriod.period_max + 1): - # Fill in empty weekdays - if period_num not in per_period.keys(): - per_period[period_num] = {} - - # Fill in empty lessons on this workday - for weekday_num in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1): - if weekday_num not in per_period[period_num].keys(): - per_period[period_num][weekday_num] = [] - - # Order this weekday by periods - per_period[period_num] = OrderedDict(sorted(per_period[period_num].items())) - - context["lesson_periods"] = OrderedDict(sorted(per_period.items())) + # Build timetable + timetable = build_timetable(type_, pk, wanted_week) + context["timetable"] = timetable + + # Add time periods context["periods"] = TimePeriod.get_times_dict() # Build lists with weekdays and corresponding dates (long and short variant) - context["weekdays"] = [ - (key, weekday, wanted_week[key]) - for key, weekday in TimePeriod.WEEKDAY_CHOICES[ - TimePeriod.weekday_min : TimePeriod.weekday_max + 1 - ] - ] - context["weekdays_short"] = [ - (key, weekday, wanted_week[key]) - for key, weekday in TimePeriod.WEEKDAY_CHOICES_SHORT[ - TimePeriod.weekday_min : TimePeriod.weekday_max + 1 - ] - ] + context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week) + context["weekdays_short"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week) context["weeks"] = get_weeks_for_year(year=wanted_week.year) context["week"] = wanted_week @@ -322,12 +286,14 @@ def substitutions( day_contexts = {wanted_day: {"day": wanted_day}} for day in day_contexts: - subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups", "lesson_period__period") + subs = build_substitutions_list(day) day_contexts[day]["substitutions"] = subs day_contexts[day]["announcements"] = Announcement.for_timetables().on_date(day).filter(show_in_timetables=True) if config.CHRONOS_SUBSTITUTIONS_SHOW_HEADER_BOX: + subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups", + "lesson_period__period") day_contexts[day]["affected_teachers"] = subs.affected_teachers() day_contexts[day]["affected_groups"] = subs.affected_groups()