Skip to content
Snippets Groups Projects
Commit bb519b69 authored by Hangzhi Yu's avatar Hangzhi Yu
Browse files

Add daily supervisions page and form for entering of supervision substitutions

parent 042be4ae
No related branches found
No related tags found
1 merge request!279Resolve "Supervision Substitution entering"
Showing with 382 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,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)
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