diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 9f3c2fcb17b32d9c915610bd6aaa8b948752ee44..ed4caa85b485d4496603f8c92d2a73598c8f130c 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -9,6 +9,8 @@ from django.db import models from django.db.models import F, Max, Min, Q from django.db.models.functions import Coalesce from django.http.request import QueryDict +from django.urls import reverse +from django.utils import timezone from django.utils.decorators import classproperty from django.utils.translation import ugettext_lazy as _ @@ -205,6 +207,21 @@ class LessonSubstitutionQuerySet(LessonDataQuerySet): _period_path = "lesson_period__" _subst_path = "" + def affected_lessons(self): + """ Return all lessons which are affected by selected substitutions """ + + return Lesson.objects.filter(lesson_periods__substitutions__in=self) + + def affected_teachers(self): + """ Return all teachers which are affected by selected substitutions (as substituted or substituting) """ + + return Person.objects.filter(Q(lessons_as_teacher__in=self.affected_lessons()) | Q(lesson_substitutions__in=self)) + + def affected_groups(self): + """ Return all groups which are affected by selected substitutions """ + + return Group.objects.filter(lessons__in=self.affected_lessons()) + class TimePeriod(models.Model): WEEKDAY_CHOICES = list(enumerate(i18n_day_names_lazy())) @@ -246,6 +263,46 @@ class TimePeriod(models.Model): return wanted_week[self.weekday] + @classmethod + def get_next_relevant_day(cls, day: Optional[date] = None, time: Optional[time] = None, prev: bool = False) -> date: + """ Returns 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]: + """ 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: return cls.objects.aggregate(period__min=Coalesce(Min("period"), 1)).get("period__min") diff --git a/aleksis/apps/chronos/settings.py b/aleksis/apps/chronos/settings.py index 1ae08d71d21171078752c8f2d80b719b2668e4cc..2c861857f006770095861fdfdd249195a3477c16 100644 --- a/aleksis/apps/chronos/settings.py +++ b/aleksis/apps/chronos/settings.py @@ -1,8 +1,18 @@ from django.utils.translation import gettext_lazy as _ CONSTANCE_CONFIG = { - "CHRONOS_SUBSTITUTIONS_PRINT_DAY_NUMBER": (2, _("Number of days shown on substitutions print view")), + "CHRONOS_SUBSTITUTIONS_PRINT_DAY_NUMBER": ( + 2, + _("Number of days shown on substitutions print view"), + ), + "CHRONOS_SUBSTITUTIONS_SHOW_HEADER_BOX": ( + True, + _("The header box shows affected teachers/groups."), + ), } CONSTANCE_CONFIG_FIELDSETS = { - "Chronos settings": ("CHRONOS_SUBSTITUTIONS_PRINT_DAY_NUMBER",), + "Chronos settings": ( + "CHRONOS_SUBSTITUTIONS_PRINT_DAY_NUMBER", + "CHRONOS_SUBSTITUTIONS_SHOW_HEADER_BOX", + ), } diff --git a/aleksis/apps/chronos/static/css/chronos/timetable.css b/aleksis/apps/chronos/static/css/chronos/timetable.css index 324272585c7a0447c8f2e901fafbfb7be94dfa7a..16846c6059e92d49a8ba89258a7b259793c427b0 100644 --- a/aleksis/apps/chronos/static/css/chronos/timetable.css +++ b/aleksis/apps/chronos/static/css/chronos/timetable.css @@ -108,3 +108,7 @@ table.substitutions td, table.substitutions th { margin: 2px; letter-spacing: 0.3pt; } + +.black-text-a a { + color: black; +} diff --git a/aleksis/apps/chronos/templates/chronos/partials/headerbox.html b/aleksis/apps/chronos/templates/chronos/partials/headerbox.html new file mode 100644 index 0000000000000000000000000000000000000000..bc5bd0f34891d644fbaf4f5e2c065f2be450dec9 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/headerbox.html @@ -0,0 +1,33 @@ +{% load i18n %} + +{% if affected_teachers and affected_groups %} + <div class="{% if not print %}card{% endif %}"> + <div class="{% if not print %}card-content{% endif %}"> + {% if affected_teachers %} + <div class="row no-margin"> + <div class="col s12 m3"> + <strong class="truncate"> + {% trans "Affected teachers" %} + </strong> + </div> + <div class="col s12 m9 black-text-a"> + {% include "chronos/partials/teachers.html" with teachers=affected_teachers %} + </div> + </div> + {% endif %} + {% if affected_groups %} + <div class="row no-margin"> + <div class="col s12 m3"> + <strong class="truncate"> + {% trans "Affected groups" %} + </strong> + </div> + <div class="col s12 m9 black-text-a"> + {% include "chronos/partials/groups.html" with groups=affected_groups %} + </div> + </div> + {% endif %} + </div> + </div> + {% if print %}<br/>{% endif %} +{% endif %} diff --git a/aleksis/apps/chronos/templates/chronos/substitutions.html b/aleksis/apps/chronos/templates/chronos/substitutions.html index 32e98c56d705fc815f709a7cbcc063c5206cb191..0dc5e242852375b05ff6f48e2b35430d61510727 100644 --- a/aleksis/apps/chronos/templates/chronos/substitutions.html +++ b/aleksis/apps/chronos/templates/chronos/substitutions.html @@ -26,22 +26,7 @@ <div class="row no-print"> <div class="col s12 m6 l8"> - {% if header_info.is_box_needed %} - <div class="card"> - <div class="card-content"> - {% for row in header_info.rows %} - <div class="row no-margin"> - <div class="col s3"> - <strong class="truncate">{{ row.0 }}</strong> - </div> - <div class="col s9"> - {{ row.1 }} - </div> - </div> - {% endfor %} - </div> - </div> - {% endif %} + {% include "chronos/partials/headerbox.html" %} {# {% include "chronos/hintsinsub.html" %}#} </div> diff --git a/aleksis/apps/chronos/templates/chronos/substitutions_print.html b/aleksis/apps/chronos/templates/chronos/substitutions_print.html index 2a4e474bb2fc407f1297efb754d984140fc30fd0..bc041f24c2c366b6ae2764afde621a2e8802a291 100644 --- a/aleksis/apps/chronos/templates/chronos/substitutions_print.html +++ b/aleksis/apps/chronos/templates/chronos/substitutions_print.html @@ -18,20 +18,7 @@ {# {% include "timetable/hintsinsubprint.html" %}#} - {# <div style="margin-bottom: 20px">#} - {# {% if c.header_info.is_box_needed %}#} - {# {% for row in c.header_info.rows %}#} - {# <div class="row no-margin">#} - {# <div class="col s3 no-padding">#} - {# <strong>{{ row.0 }}</strong>#} - {# </div>#} - {# <div class="col s9 no-padding">#} - {# {{ row.1 }}#} - {# </div>#} - {# </div>#} - {# {% endfor %}#} - {# {% endif %}#} - {# </div>#} + {% include "chronos/partials/headerbox.html" with affected_teachers=c.affected_teachers affected_groups=c.affected_groups print=1 %} <table class="substitutions"> <thead> diff --git a/aleksis/apps/chronos/util/prev_next.py b/aleksis/apps/chronos/util/prev_next.py deleted file mode 100644 index 4f954c3082eb69e5c18afb12896c975ed07135b0..0000000000000000000000000000000000000000 --- a/aleksis/apps/chronos/util/prev_next.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import timedelta, date, time -from typing import Optional, Tuple - -from calendarweek import CalendarWeek -from django.urls import reverse -from django.utils import timezone - -from ..models import TimePeriod - - -def get_next_relevant_day(day: Optional[date] = None, time: Optional[time] = None, prev: bool = False) -> date: - """ Returns 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 > TimePeriod.time_max: - day += timedelta(days=1) - - cw = CalendarWeek.from_date(day) - - if day.weekday() > TimePeriod.weekday_max: - if prev: - day = cw[TimePeriod.weekday_max] - else: - cw += 1 - day = cw[TimePeriod.weekday_min] - elif day.weekday() < TimePeriod.weekday_min: - if prev: - cw -= 1 - day = cw[TimePeriod.weekday_max] - else: - day = cw[TimePeriod.weekday_min] - - return day - - -def get_prev_next_by_day(day: date, url: str) -> Tuple[str, str]: - """ Build URLs for previous/next day """ - - day_prev = get_next_relevant_day(day - timedelta(days=1), prev=True) - day_next = 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 diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index b3dedd30fc75c63be1600a76e851fc418b42468e..22ab295fd14e89b30ff288e15b337e771b9e5d84 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -19,7 +19,6 @@ from .forms import LessonSubstitutionForm from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room from .tables import LessonsTable from .util.js import date_unix -from .util.prev_next import get_next_relevant_day, get_prev_next_by_day from .util.date import CalendarWeek, get_weeks_for_year from aleksis.core.util.core_helpers import has_person @@ -56,9 +55,9 @@ def my_timetable( if day: wanted_day = timezone.datetime(year=year, month=month, day=day).date() - wanted_day = get_next_relevant_day(wanted_day) + wanted_day = TimePeriod.get_next_relevant_day(wanted_day) else: - wanted_day = get_next_relevant_day(timezone.now().date(), datetime.now().time()) + wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) if has_person(request.user): person = request.user.person @@ -98,7 +97,7 @@ def my_timetable( context["periods"] = TimePeriod.get_times_dict() context["smart"] = True - context["url_prev"], context["url_next"] = get_prev_next_by_day( + context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day( wanted_day, "my_timetable_by_date" ) @@ -209,9 +208,9 @@ def lessons_day( if day: wanted_day = timezone.datetime(year=year, month=month, day=day).date() - wanted_day = get_next_relevant_day(wanted_day) + wanted_day = TimePeriod.get_next_relevant_day(wanted_day) else: - wanted_day = get_next_relevant_day(timezone.now().date(), datetime.now().time()) + wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) # Get lessons lesson_periods = LessonPeriod.objects.on_day(wanted_day) @@ -229,7 +228,7 @@ def lessons_day( "dest": reverse("lessons_day") } - context["url_prev"], context["url_next"] = get_prev_next_by_day( + context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day( wanted_day, "lessons_day_by_date" ) @@ -305,9 +304,9 @@ def substitutions( if day: wanted_day = timezone.datetime(year=year, month=month, day=day).date() - wanted_day = get_next_relevant_day(wanted_day) + wanted_day = TimePeriod.get_next_relevant_day(wanted_day) else: - wanted_day = get_next_relevant_day(timezone.now().date(), datetime.now().time()) + wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) day_number = config.CHRONOS_SUBSTITUTIONS_PRINT_DAY_NUMBER day_contexts = {} @@ -316,14 +315,17 @@ def substitutions( next_day = wanted_day for i in range(day_number): day_contexts[next_day] = {"day": next_day} - next_day = get_next_relevant_day(next_day + timedelta(days=1)) + next_day = TimePeriod.get_next_relevant_day(next_day + timedelta(days=1)) else: day_contexts = {wanted_day: {"day": wanted_day}} for day in day_contexts: - day_contexts[day]["substitutions"] = LessonSubstitution.objects.on_day( - day - ).order_by("lesson_period__lesson__groups", "lesson_period__period") + subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups", "lesson_period__period") + day_contexts[day]["substitutions"] = subs + + if config.CHRONOS_SUBSTITUTIONS_SHOW_HEADER_BOX: + day_contexts[day]["affected_teachers"] = subs.affected_teachers() + day_contexts[day]["affected_groups"] = subs.affected_groups() if not is_print: context = day_contexts[wanted_day] @@ -332,7 +334,7 @@ def substitutions( "dest": reverse("substitutions"), } - context["url_prev"], context["url_next"] = get_prev_next_by_day( + context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day( wanted_day, "substitutions_by_date" )