From d96c77332e791be2b8af2eefebcbfa8a82221152 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 2 Nov 2022 17:13:40 +0100 Subject: [PATCH] Add filter to daily lesson page --- CHANGELOG.rst | 5 + aleksis/apps/chronos/filters.py | 124 ++++++++++++++++++ .../templates/chronos/lessons_day.html | 21 ++- aleksis/apps/chronos/views.py | 19 ++- 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 aleksis/apps/chronos/filters.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 43faaabd..91111fe9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Added +~~~~~ + +* Add filter to daily lessons page. + Fixed ~~~~~ diff --git a/aleksis/apps/chronos/filters.py b/aleksis/apps/chronos/filters.py new file mode 100644 index 00000000..83d38698 --- /dev/null +++ b/aleksis/apps/chronos/filters.py @@ -0,0 +1,124 @@ +from typing import Sequence + +from django.db.models import Count, Q +from django.forms import RadioSelect +from django.utils.translation import gettext as _ + +from django_filters import BooleanFilter, FilterSet, ModelMultipleChoiceFilter +from django_select2.forms import ModelSelect2MultipleWidget +from material import Layout, Row + +from aleksis.core.models import Group, Person, SchoolTerm + +from .models import Room, Subject, TimePeriod + + +class MultipleModelMultipleChoiceFilter(ModelMultipleChoiceFilter): + """Filter for filtering multiple fields with one input. + + >>> multiple_filter = MultipleModelMultipleChoiceFilter(["room", "substitution_room"]) + """ + + def filter(self, qs, value): # noqa + if not value: + return qs + + if self.is_noop(qs, value): + return qs + + q = Q() + for v in set(value): + if v == self.null_value: + v = None + for field in self.lookup_fields: + q = q | Q(**{field: v}) + + qs = self.get_method(qs)(q) + + return qs.distinct() if self.distinct else qs + + def __init__(self, lookup_fields: Sequence[str], *args, **kwargs): + self.lookup_fields = lookup_fields + super().__init__(self, *args, **kwargs) + + +class LessonPeriodFilter(FilterSet): + period = ModelMultipleChoiceFilter(queryset=TimePeriod.objects.all()) + lesson__groups = ModelMultipleChoiceFilter( + queryset=Group.objects.all(), + widget=ModelSelect2MultipleWidget( + attrs={"data-minimum-input-length": 0, "class": "browser-default"}, + search_fields=[ + "name__icontains", + "short_name__icontains", + ], + ), + ) + room = MultipleModelMultipleChoiceFilter( + ["room", "current_substitution__room"], + queryset=Room.objects.all(), + label=_("Room"), + widget=ModelSelect2MultipleWidget( + attrs={"data-minimum-input-length": 0, "class": "browser-default"}, + search_fields=[ + "name__icontains", + "short_name__icontains", + ], + ), + ) + lesson__teachers = MultipleModelMultipleChoiceFilter( + ["lesson__teachers", "current_substitution__teachers"], + 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=_("Teachers"), + widget=ModelSelect2MultipleWidget( + attrs={"data-minimum-input-length": 0, "class": "browser-default"}, + search_fields=[ + "first_name__icontains", + "last_name__icontains", + "short_name__icontains", + ], + ), + ) + lesson__subject = MultipleModelMultipleChoiceFilter( + ["lesson__subject", "current_substitution__subject"], + queryset=Subject.objects.all(), + label=_("Subject"), + widget=ModelSelect2MultipleWidget( + attrs={"data-minimum-input-length": 0, "class": "browser-default"}, + search_fields=[ + "name__icontains", + "short_name__icontains", + ], + ), + ) + substituted = BooleanFilter( + field_name="current_substitution", + label=_("Substitution status"), + lookup_expr="isnull", + exclude=True, + widget=RadioSelect( + choices=[ + ("", _("All lessons")), + (True, _("Substituted")), + (False, _("Not substituted")), + ] + ), + ) + + def __init__(self, *args, **kwargs): + weekday = kwargs.pop("weekday") + super().__init__(*args, **kwargs) + self.filters["period"].queryset = TimePeriod.objects.filter(weekday=weekday) + self.form.layout = Layout( + Row("period", "lesson__groups", "room"), + Row("lesson__teachers", "lesson__subject", "substituted"), + ) diff --git a/aleksis/apps/chronos/templates/chronos/lessons_day.html b/aleksis/apps/chronos/templates/chronos/lessons_day.html index 660878fa..e0147132 100644 --- a/aleksis/apps/chronos/templates/chronos/lessons_day.html +++ b/aleksis/apps/chronos/templates/chronos/lessons_day.html @@ -1,11 +1,16 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} -{% load i18n %} +{% load i18n material_form any_js %} {% load render_table from django_tables2 %} +{% block extra_head %} + {{ lesson_periods_filter.form.media.css }} + {% include_css "select2-materialize" %} +{% endblock %} + {% block browser_title %}{% blocktrans %}Lessons{% endblocktrans %}{% endblock %} {% block no_page_title %}{% endblock %} @@ -14,6 +19,17 @@ var dest = Urls.lessonsDay(); </script> + <h2>{% trans "Filter lessons" %}</h2> + <form method="get"> + {% form form=lesson_periods_filter.form %}{% endform %} + {% trans "Search" as caption %} + {% include "core/partials/save_button.html" with caption=caption icon="mdi:search" %} + <button type="reset" class="btn red waves-effect waves-light"> + <i class="material-icons iconify left" data-icon="mdi:close"></i> + {% trans "Clear" %} + </button> + </form> + <div class="row no-margin"> <div class="col s12 m6 l8 no-padding"> <h1>{% blocktrans %}Lessons{% endblocktrans %} {{ day|date:"l" }}, {{ day }}</h1> @@ -24,4 +40,7 @@ </div> {% render_table lessons_table %} + + {% include_js "select2-materialize" %} + {{ lesson_periods_filter.form.media.js }} {% endblock %} diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index eb521be2..a5742ee1 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import Optional from django.apps import apps +from django.db.models import FilteredRelation, Q from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -18,6 +19,7 @@ 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 .managers import TimetableType from .models import Holiday, LessonPeriod, TimePeriod @@ -222,8 +224,23 @@ def lessons_day( # Get lessons lesson_periods = LessonPeriod.objects.on_day(wanted_day) + # Get filter + lesson_periods_filter = LessonPeriodFilter( + request.GET, + queryset=lesson_periods.annotate( + current_substitution=FilteredRelation( + "substitutions", + condition=( + Q(substitutions__week=wanted_day.isocalendar()[1], substitutions__year=year) + ), + ) + ), + weekday=wanted_day.weekday(), + ) + context["lesson_periods_filter"] = lesson_periods_filter + # Build table - lessons_table = LessonsTable(lesson_periods.all()) + lessons_table = LessonsTable(lesson_periods_filter.qs) RequestConfig(request).configure(lessons_table) context["lessons_table"] = lessons_table -- GitLab