From bb519b696356e4daa485bd5393866704fea0ea68 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Thu, 10 Nov 2022 00:16:44 +0100
Subject: [PATCH] Add daily supervisions page and form for entering of
 supervision substitutions

---
 CHANGELOG.rst                                 |   2 +
 aleksis/apps/chronos/filters.py               |  50 ++++++-
 aleksis/apps/chronos/forms.py                 |  22 ++-
 aleksis/apps/chronos/menus.py                 |  11 ++
 .../0012_add_supervision_global_permission.py |  17 +++
 aleksis/apps/chronos/models.py                |   6 +
 aleksis/apps/chronos/rules.py                 |  16 +++
 aleksis/apps/chronos/tables.py                |  40 ++++--
 .../edit_supervision_substitution.html        |  31 ++++
 .../templates/chronos/supervisions_day.html   |  38 +++++
 aleksis/apps/chronos/urls.py                  |  16 +++
 aleksis/apps/chronos/util/chronos_helpers.py  |  18 ++-
 aleksis/apps/chronos/views.py                 | 133 +++++++++++++++++-
 13 files changed, 382 insertions(+), 18 deletions(-)
 create mode 100644 aleksis/apps/chronos/migrations/0012_add_supervision_global_permission.py
 create mode 100644 aleksis/apps/chronos/templates/chronos/edit_supervision_substitution.html
 create mode 100644 aleksis/apps/chronos/templates/chronos/supervisions_day.html

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f888e60b..58ff8cb5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,8 @@ Unreleased
 Added
 ~~~~~
 
+* Add overview page of all daily supervisions.
+* Add form to add substitutions to supervisions.
 * Add filter to daily lessons page.
 * Display initial lesson data with substituted lessons in daily lessons table.
 
diff --git a/aleksis/apps/chronos/filters.py b/aleksis/apps/chronos/filters.py
index 83d38698..76048fc9 100644
--- a/aleksis/apps/chronos/filters.py
+++ b/aleksis/apps/chronos/filters.py
@@ -10,7 +10,7 @@ from material import Layout, Row
 
 from aleksis.core.models import Group, Person, SchoolTerm
 
-from .models import Room, Subject, TimePeriod
+from .models import Break, Room, Subject, SupervisionArea, TimePeriod
 
 
 class MultipleModelMultipleChoiceFilter(ModelMultipleChoiceFilter):
@@ -122,3 +122,51 @@ class LessonPeriodFilter(FilterSet):
             Row("period", "lesson__groups", "room"),
             Row("lesson__teachers", "lesson__subject", "substituted"),
         )
+
+
+class SupervisionFilter(FilterSet):
+    break_item = ModelMultipleChoiceFilter(queryset=Break.objects.all())
+    area = ModelMultipleChoiceFilter(queryset=SupervisionArea.objects.all())
+    teacher = MultipleModelMultipleChoiceFilter(
+        ["teacher", "current_substitution__teacher"],
+        queryset=Person.objects.annotate(
+            lessons_count=Count(
+                "lessons_as_teacher",
+                filter=Q(lessons_as_teacher__validity__school_term=SchoolTerm.current)
+                if SchoolTerm.current
+                else Q(),
+            )
+        )
+        .filter(lessons_count__gt=0)
+        .order_by("short_name", "last_name"),
+        label=_("Teacher"),
+        widget=ModelSelect2MultipleWidget(
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            search_fields=[
+                "first_name__icontains",
+                "last_name__icontains",
+                "short_name__icontains",
+            ],
+        ),
+    )
+    substituted = BooleanFilter(
+        field_name="current_substitution",
+        label=_("Substitution status"),
+        lookup_expr="isnull",
+        exclude=True,
+        widget=RadioSelect(
+            choices=[
+                ("", _("All supervisions")),
+                (True, _("Substituted")),
+                (False, _("Not substituted")),
+            ]
+        ),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.filters["break_item"].queryset = Break.objects.filter(supervisions__in=self.queryset)
+        self.form.layout = Layout(
+            Row("break_item", "area"),
+            Row("teacher", "substituted"),
+        )
diff --git a/aleksis/apps/chronos/forms.py b/aleksis/apps/chronos/forms.py
index b1cfb87e..df2952e7 100644
--- a/aleksis/apps/chronos/forms.py
+++ b/aleksis/apps/chronos/forms.py
@@ -1,9 +1,9 @@
 from django import forms
 
-from django_select2.forms import ModelSelect2MultipleWidget
+from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
 from material import Layout
 
-from .models import AutomaticPlan, LessonSubstitution
+from .models import AutomaticPlan, LessonSubstitution, SupervisionSubstitution
 
 
 class LessonSubstitutionForm(forms.ModelForm):
@@ -24,6 +24,24 @@ class LessonSubstitutionForm(forms.ModelForm):
         }
 
 
+class SupervisionSubstitutionForm(forms.ModelForm):
+    """Form to manage supervisions substitutions."""
+
+    class Meta:
+        model = SupervisionSubstitution
+        fields = ["teacher"]
+        widgets = {
+            "teacher": ModelSelect2Widget(
+                search_fields=[
+                    "first_name__icontains",
+                    "last_name__icontains",
+                    "short_name__icontains",
+                ],
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            ),
+        }
+
+
 class AutomaticPlanForm(forms.ModelForm):
     layout = Layout("slug", "name", "number_of_days", "show_header_box")
 
diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py
index dc07d04b..810e88c6 100644
--- a/aleksis/apps/chronos/menus.py
+++ b/aleksis/apps/chronos/menus.py
@@ -45,6 +45,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Daily supervisions"),
+                    "url": "supervisions_day",
+                    "svg_icon": "mdi:calendar-outline",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "chronos.view_supervisions_day_rule",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Substitutions"),
                     "url": "substitutions",
diff --git a/aleksis/apps/chronos/migrations/0012_add_supervision_global_permission.py b/aleksis/apps/chronos/migrations/0012_add_supervision_global_permission.py
new file mode 100644
index 00000000..e41820df
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0012_add_supervision_global_permission.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.13 on 2022-11-09 23:07
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0011_exam'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='chronosglobalpermissions',
+            options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day'), ('view_supervisions_day', 'Can view all supervisions per day'))},
+        ),
+    ]
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 22268363..e6553ab3 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -988,6 +988,11 @@ class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin):
             year += 1
         return year
 
+    def get_calendar_week(self, week: int):
+        year = self.get_year(week)
+
+        return CalendarWeek(year=year, week=week)
+
     def get_substitution(
         self, week: Optional[CalendarWeek] = None
     ) -> Optional[SupervisionSubstitution]:
@@ -1362,4 +1367,5 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
             ("view_all_person_timetables", _("Can view all person timetables")),
             ("view_timetable_overview", _("Can view timetable overview")),
             ("view_lessons_day", _("Can view all lessons per day")),
+            ("view_supervisions_day", _("Can view all supervisions per day")),
         )
diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py
index 5dcfd20a..85a0d1c4 100644
--- a/aleksis/apps/chronos/rules.py
+++ b/aleksis/apps/chronos/rules.py
@@ -48,6 +48,22 @@ view_substitutions_predicate = has_person & (
 )
 add_perm("chronos.view_substitutions_rule", view_substitutions_predicate)
 
+# View all supervisions per day
+view_supervisions_day_predicate = has_person & has_global_perm("chronos.view_supervisions_day")
+add_perm("chronos.view_supervisions_day_rule", view_supervisions_day_predicate)
+
+# Edit supervision substitution
+edit_supervision_substitution_predicate = has_person & (
+    has_global_perm("chronos.change_supervisionsubstitution")
+)
+add_perm("chronos.edit_supervision_substitution_rule", edit_supervision_substitution_predicate)
+
+# Delete supervision substitution
+delete_supervision_substitution_predicate = has_person & (
+    has_global_perm("chronos.delete_supervisionsubstitution")
+)
+add_perm("chronos.delete_supervision_substitution_rule", delete_supervision_substitution_predicate)
+
 # View room (timetable)
 view_room_predicate = has_person & has_room_timetable_perm
 add_perm("chronos.view_room_rule", view_room_predicate)
diff --git a/aleksis/apps/chronos/tables.py b/aleksis/apps/chronos/tables.py
index 887291f7..89eb15f8 100644
--- a/aleksis/apps/chronos/tables.py
+++ b/aleksis/apps/chronos/tables.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Optional
+from typing import Optional, Union
 
 from django.utils.html import format_html
 from django.utils.translation import gettext_lazy as _
@@ -8,15 +8,16 @@ from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 from django_tables2.utils import A, Accessor
 
-from .models import LessonPeriod
+from .models import LessonPeriod, Supervision
 
 
-def _css_class_from_lesson_state(
-    record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None
+def _css_class_from_lesson_or_supervision_state(
+    record: Optional[Union[LessonPeriod, Supervision]] = None,
+    table: Optional[Union[LessonsTable, SupervisionsTable]] = None,
 ) -> str:
-    """Return CSS class depending on lesson state."""
+    """Return CSS class depending on lesson or supervision state."""
     if record.get_substitution():
-        if record.get_substitution().cancelled:
+        if hasattr(record.get_substitution(), "cancelled") and record.get_substitution().cancelled:
             return "success"
         else:
             return "warning"
@@ -25,7 +26,7 @@ def _css_class_from_lesson_state(
 
 
 class SubstitutionColumn(tables.Column):
-    def render(self, value, record: Optional[LessonPeriod] = None):
+    def render(self, value, record: Optional[Union[LessonPeriod, Supervision]] = None):
         if record.get_substitution():
             return format_html(
                 "<s>{}</s> → {}",
@@ -44,7 +45,7 @@ class LessonsTable(tables.Table):
 
     class Meta:
         attrs = {"class": "highlight"}
-        row_attrs = {"class": _css_class_from_lesson_state}
+        row_attrs = {"class": _css_class_from_lesson_or_supervision_state}
 
     period__period = tables.Column(accessor="period__period")
     lesson__groups = tables.Column(accessor="lesson__group_names", verbose_name=_("Groups"))
@@ -64,3 +65,26 @@ class LessonsTable(tables.Table):
         attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
         verbose_name=_("Manage substitution"),
     )
+
+
+class SupervisionsTable(tables.Table):
+    """Table for daily supervisions and management of substitutions."""
+
+    class Meta:
+        attrs = {"class": "highlight"}
+        row_attrs = {"class": _css_class_from_lesson_or_supervision_state}
+
+    break_item = tables.Column(accessor="break_item")
+    area = tables.Column(accessor="area")
+    teacher = SubstitutionColumn(
+        accessor="teacher",
+        substitution_accessor="teacher",
+        verbose_name=_("Teachers"),
+    )
+    edit_substitution = tables.LinkColumn(
+        "edit_supervision_substitution",
+        args=[A("id"), A("_week")],
+        text=_("Substitution"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
+        verbose_name=_("Manage substitution"),
+    )
diff --git a/aleksis/apps/chronos/templates/chronos/edit_supervision_substitution.html b/aleksis/apps/chronos/templates/chronos/edit_supervision_substitution.html
new file mode 100644
index 00000000..2ee637e8
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/edit_supervision_substitution.html
@@ -0,0 +1,31 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n any_js %}
+
+{% block extra_head %}
+    {{ edit_supervision_substitution_form.media.css }}
+    {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Edit substitution.{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit substitution{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <p class="flow-text">{{ date }}: {{ supervision }}</p>
+  <form method="post">
+    {% csrf_token %}
+
+    {% form form=edit_supervision_substitution_form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+    {% if substitution %}
+      <a href="{% url 'delete_supervision_substitution' substitution.supervision.id week %}"
+         class="btn red waves-effect waves-light">
+        <i class="material-icons iconify left" data-icon="mdi:delete-outline"></i> {% trans "Delete" %}
+      </a>
+    {% endif %}
+  </form>
+  {% include_js "select2-materialize" %}
+  {{ edit_supervision_substitution_form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/chronos/templates/chronos/supervisions_day.html b/aleksis/apps/chronos/templates/chronos/supervisions_day.html
new file mode 100644
index 00000000..8c28324e
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/supervisions_day.html
@@ -0,0 +1,38 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load i18n material_form any_js %}
+
+
+{% load render_table from django_tables2 %}
+
+{% block extra_head %}
+  {{ supervisions_filter.form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Lessons{% endblocktrans %}{% endblock %}
+{% block no_page_title %}{% endblock %}
+
+{% block content %}
+  <h2>{% trans "Filter supervisions" %}</h2>
+  <form method="get">
+    {% form form=supervisions_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="mdi:search" %}
+  </form>
+
+  <div class="row no-margin">
+    <div class="col s12 m6 l8 no-padding">
+      <h1>{% blocktrans %}Supervisions{% endblocktrans %} {{ day|date:"l" }}, {{ day }}</h1>
+    </div>
+    <div class="col s12 m6 l4 no-padding">
+      {% include "chronos/partials/datepicker.html" %}
+    </div>
+  </div>
+
+  {% render_table supervisions_table %}
+
+  {% include_js "select2-materialize" %}
+  {{ supervisions_filter.form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/chronos/urls.py b/aleksis/apps/chronos/urls.py
index 411e5cd8..e4cf2619 100644
--- a/aleksis/apps/chronos/urls.py
+++ b/aleksis/apps/chronos/urls.py
@@ -61,4 +61,20 @@ urlpatterns = [
         {"is_print": True},
         name="substitutions_print_by_date",
     ),
+    path("supervisions/", views.supervisions_day, name="supervisions_day"),
+    path(
+        "supervisions/<int:year>/<int:month>/<int:day>/",
+        views.supervisions_day,
+        name="supervisions_day_by_date",
+    ),
+    path(
+        "supervisions/<int:id_>/<int:week>/substitution/",
+        views.edit_supervision_substitution,
+        name="edit_supervision_substitution",
+    ),
+    path(
+        "supervisions/<int:id_>/<int:week>/substitution/delete/",
+        views.delete_supervision_substitution,
+        name="delete_supervision_substitution",
+    ),
 ]
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index e93637d1..4f427b63 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -1,4 +1,4 @@
-from datetime import timedelta
+from datetime import datetime, timedelta
 from typing import TYPE_CHECKING, Optional
 
 from django.db.models import Count, Q
@@ -14,7 +14,15 @@ from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
 
 from ..managers import TimetableType
-from ..models import Absence, LessonPeriod, LessonSubstitution, Room, TimePeriod
+from ..models import (
+    Absence,
+    LessonPeriod,
+    LessonSubstitution,
+    Room,
+    Supervision,
+    SupervisionSubstitution,
+    TimePeriod,
+)
 from .build import build_substitutions_list
 from .js import date_unix
 
@@ -57,6 +65,12 @@ def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
     ).first()
 
 
+def get_supervision_substitution_by_id(request: HttpRequest, id_: int, date: datetime.date):
+    supervision = get_object_or_404(Supervision, pk=id_)
+
+    return SupervisionSubstitution.objects.filter(date=date, supervision=supervision).first()
+
+
 def get_teachers(user: "User"):
     """Get the teachers whose timetables are allowed to be seen by current user."""
     checker = ObjectPermissionChecker(user)
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index a5742ee1..59f8444c 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -19,11 +19,11 @@ from aleksis.core.util import messages
 from aleksis.core.util.core_helpers import has_person
 from aleksis.core.util.pdf import render_pdf
 
-from .filters import LessonPeriodFilter
-from .forms import LessonSubstitutionForm
+from .filters import LessonPeriodFilter, SupervisionFilter
+from .forms import LessonSubstitutionForm, SupervisionSubstitutionForm
 from .managers import TimetableType
-from .models import Holiday, LessonPeriod, TimePeriod
-from .tables import LessonsTable
+from .models import Holiday, LessonPeriod, Supervision, TimePeriod
+from .tables import LessonsTable, SupervisionsTable
 from .util.build import build_timetable, build_weekdays
 from .util.change_tracker import TimetableDataChangeTracker
 from .util.chronos_helpers import (
@@ -32,6 +32,7 @@ from .util.chronos_helpers import (
     get_rooms,
     get_substitution_by_id,
     get_substitutions_context_data,
+    get_supervision_substitution_by_id,
     get_teachers,
 )
 from .util.date import CalendarWeek, get_weeks_for_year, week_weekday_to_date
@@ -222,7 +223,7 @@ def lessons_day(
         wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
 
     # Get lessons
-    lesson_periods = LessonPeriod.objects.on_day(wanted_day)
+    lesson_periods = LessonPeriod.objects.all()
 
     # Get filter
     lesson_periods_filter = LessonPeriodFilter(
@@ -337,3 +338,125 @@ def substitutions(
         return render(request, "chronos/substitutions.html", context)
     else:
         return render_pdf(request, "chronos/substitutions_print.html", context)
+
+
+@permission_required("chronos.view_supervisions_day_rule")
+def supervisions_day(
+    request: HttpRequest,
+    year: Optional[int] = None,
+    month: Optional[int] = None,
+    day: Optional[int] = None,
+) -> HttpResponse:
+    """View all supervisions taking place on a specified day."""
+    context = {}
+
+    if day:
+        wanted_day = timezone.datetime(year=year, month=month, day=day).date()
+        wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
+    else:
+        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
+
+    # Get supervisions
+    supervisions = Supervision.objects.filter(
+        Q(break_item__after_period__weekday=wanted_day.weekday())
+        | Q(break_item__before_period__weekday=wanted_day.weekday())
+    )
+
+    # Get filter
+    supervisions_filter = SupervisionFilter(
+        request.GET,
+        queryset=supervisions.annotate(
+            current_substitution=FilteredRelation(
+                "substitutions",
+                condition=(Q(substitutions__date=wanted_day)),
+            )
+        ),
+    )
+    context["supervisions_filter"] = supervisions_filter
+
+    # Build table
+    supervisions_table = SupervisionsTable(
+        supervisions_filter.qs.annotate_week(week=CalendarWeek.from_date(wanted_day))
+    )
+    RequestConfig(request).configure(supervisions_table)
+
+    context["supervisions_table"] = supervisions_table
+    context["day"] = wanted_day
+    context["supervisions"] = supervisions
+
+    context["datepicker"] = {
+        "date": date_unix(wanted_day),
+        "dest": reverse("supervisions_day"),
+    }
+
+    context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
+        wanted_day, "supervisions_day_by_date"
+    )
+
+    return render(request, "chronos/supervisions_day.html", context)
+
+
+@never_cache
+@permission_required("chronos.edit_supervision_substitution_rule")
+def edit_supervision_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
+    """View a form to edit a supervision substitution."""
+    context = {}
+
+    supervision = get_object_or_404(Supervision, pk=id_)
+    wanted_week = supervision.get_calendar_week(week)
+    context["week"] = week
+    context["supervision"] = supervision
+    date = week_weekday_to_date(wanted_week, supervision.break_item.weekday)
+    context["date"] = date
+
+    supervision_substitution = get_supervision_substitution_by_id(request, id_, date)
+
+    if supervision_substitution:
+        edit_supervision_substitution_form = SupervisionSubstitutionForm(
+            request.POST or None, instance=supervision_substitution
+        )
+    else:
+        edit_supervision_substitution_form = SupervisionSubstitutionForm(
+            request.POST or None,
+        )
+
+    context["substitution"] = supervision_substitution
+
+    if request.method == "POST":
+        if edit_supervision_substitution_form.is_valid():
+            with reversion.create_revision(atomic=True):
+                tracker = TimetableDataChangeTracker()
+
+                supervision_substitution = edit_supervision_substitution_form.save(commit=False)
+                if not supervision_substitution.pk:
+                    supervision_substitution.supervision = supervision
+                    supervision_substitution.date = date
+                supervision_substitution.save()
+                edit_supervision_substitution_form.save_m2m()
+
+                messages.success(request, _("The substitution has been saved."))
+
+            return redirect(
+                "supervisions_day_by_date", year=date.year, month=date.month, day=date.day
+            )
+
+    context["edit_supervision_substitution_form"] = edit_supervision_substitution_form
+
+    return render(request, "chronos/edit_supervision_substitution.html", context)
+
+
+@permission_required("chronos.delete_supervision_substitution_rule")
+def delete_supervision_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
+    """Delete a supervision substitution.
+
+    Redirects back to supervision list on success.
+    """
+    supervision = get_object_or_404(Supervision, pk=id_)
+    wanted_week = supervision.get_calendar_week(week)
+    date = week_weekday_to_date(wanted_week, supervision.break_item.weekday)
+
+    get_supervision_substitution_by_id(request, id_, date).delete()
+
+    messages.success(request, _("The substitution has been deleted."))
+
+    return redirect("supervisions_day_by_date", year=date.year, month=date.month, day=date.day)
-- 
GitLab