Skip to content
Snippets Groups Projects
Commit daf9f599 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Merge branch '185-supervision-substitution-entering' into 'master'

Resolve "Supervision Substitution entering"

Closes #185

See merge request !279
parents 29e29d26 9cb053f3
No related branches found
No related tags found
1 merge request!279Resolve "Supervision Substitution entering"
Pipeline #98077 failed
Pipeline: AlekSIS

#98085

    Showing with 379 additions and 18 deletions
    ......@@ -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.
    ......
    ......@@ -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"),
    )
    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")
    ......
    ......@@ -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",
    ......
    # 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'))},
    ),
    ]
    ......@@ -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")),
    )
    ......@@ -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)
    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"),
    )
    {# -*- 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 %}
    {# -*- 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 %}
    ......@@ -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",
    ),
    ]
    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)
    ......
    ......@@ -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,122 @@ 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.on_day(wanted_day).filter_by_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)
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment