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