From 10ff9ff9cfef42c7f7bc4d31020ae1f6947cb4a2 Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Thu, 12 Dec 2024 17:10:27 +0100
Subject: [PATCH] add task table searching through htmx partial

---
 scipost_django/tasks/forms.py                 | 77 +++++++++++++++++++
 scipost_django/tasks/tasks/task_kinds.py      | 56 ++++++++++++++
 .../tasks/templates/tasks/_hx_task_table.html | 45 +++++++++++
 .../tasks/templates/tasks/tasklist_new.html   | 62 ++++-----------
 scipost_django/tasks/views.py                 | 18 ++++-
 5 files changed, 205 insertions(+), 53 deletions(-)
 create mode 100644 scipost_django/tasks/forms.py
 create mode 100644 scipost_django/tasks/templates/tasks/_hx_task_table.html

diff --git a/scipost_django/tasks/forms.py b/scipost_django/tasks/forms.py
new file mode 100644
index 000000000..10bf51439
--- /dev/null
+++ b/scipost_django/tasks/forms.py
@@ -0,0 +1,77 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+from collections.abc import Collection
+from itertools import chain
+from typing import Dict
+from django import forms
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import Div, Field
+from tasks.tasks.task import Task
+from tasks.tasks.task_kinds import get_all_task_kinds
+
+
+class TaskListSearchForm(forms.Form):
+    search = forms.CharField(label="Search", required=False)
+
+    orderby = forms.ChoiceField(
+        label="Order by",
+        choices=[
+            ("", "-----"),
+            ("kind__name", "Type"),
+            ("title", "Title"),
+            ("due_date", "Due date"),
+        ],
+        initial="",
+        required=False,
+    )
+    ordering = forms.ChoiceField(
+        label="Ordering",
+        choices=[
+            ("-", "Descending"),
+            ("+", "Ascending"),
+        ],
+        required=False,
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.user = kwargs.pop("user")
+        self.task_kinds = get_all_task_kinds(self.user)
+        super().__init__(*args, **kwargs)
+
+        self.helper = FormHelper()
+
+        div_block_ordering = Div(
+            Div(Field("orderby"), css_class="col-6"),
+            Div(Field("ordering"), css_class="col-6"),
+            css_class="row mb-0",
+        )
+
+        self.helper.layout = Div(
+            Div(Field("search"), css_class="col-12"),
+            div_block_ordering,
+        )
+
+    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
+        # Apply the filter set to the form
+        for key in self.fields:
+            if key in filters:
+                self.fields[key].initial = filters[key]
+            elif none_on_empty:
+                if isinstance(self.fields[key], forms.MultipleChoiceField):
+                    self.fields[key].initial = []
+                else:
+                    self.fields[key].initial = None
+
+    def search_results(self) -> Collection[Task]:
+        search_text = self.cleaned_data.get("search", "")
+        orderby = self.cleaned_data.get("orderby", "")
+        ordering = self.cleaned_data.get("ordering", "-")
+
+        tasks = [
+            task
+            for task_kind in self.task_kinds
+            for task in task_kind.get_tasks(search_text)
+        ]
+
+        return tasks
diff --git a/scipost_django/tasks/tasks/task_kinds.py b/scipost_django/tasks/tasks/task_kinds.py
index 95a5cc094..94326dad7 100644
--- a/scipost_django/tasks/tasks/task_kinds.py
+++ b/scipost_django/tasks/tasks/task_kinds.py
@@ -90,6 +90,12 @@ class ScheduleSubsidyPayments(TaskKind):
             .prefetch_related("organization")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return Q(organization__name__icontains=text) | Q(
+            organization__acronym__icontains=text
+        )
+
 
 class ScheduleSubsidyCollectivePayments(TaskKind):
     name = "Schedule Subsidy Collective Payments"
@@ -135,6 +141,12 @@ class ScheduleSubsidyCollectivePayments(TaskKind):
             .prefetch_related("coordinator")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return Q(coordinator__name__icontains=text) | Q(
+            coordinator__acronym__icontains=text
+        )
+
 
 class SendSubsidyInvoiceTask(TaskKind):
     name = "Send Invoice"
@@ -178,6 +190,12 @@ class SendSubsidyInvoiceTask(TaskKind):
             .prefetch_related("organization")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return Q(organization__name__icontains=text) | Q(
+            organization__acronym__icontains=text
+        )
+
 
 class CheckSubsidyPaymentTask(TaskKind):
     name = "Check Payment"
@@ -222,6 +240,12 @@ class CheckSubsidyPaymentTask(TaskKind):
             .prefetch_related("organization")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return Q(organization__name__icontains=text) | Q(
+            organization__acronym__icontains=text
+        )
+
 
 #####################
 ## Fellow Tasks
@@ -264,6 +288,14 @@ class TreatOngoingAssignmentsTask(TaskKind):
             .prefetch_related("submission")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return (
+            Q(submission__title__icontains=text)
+            | Q(submission__preprint__identifier_w_vn_nr__icontains=text)
+            | Q(submission__author_list__unaccent__icontains=text)
+        )
+
 
 class VetCommentTask(TaskKind):
     name = "Vet Comment"
@@ -293,6 +325,14 @@ class VetCommentTask(TaskKind):
             .prefetch_related("author__user")
         )
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return (
+            Q(author__profile__last_name__unaccent__icontains=text)
+            | Q(author__profile__first_name__unaccent__icontains=text)
+            | Q(comment_text__icontains=text)
+        )
+
 
 class VetReportTask(TaskKind):
     name = "Vet Report"
@@ -336,6 +376,14 @@ class VetReportTask(TaskKind):
 
         return qs.awaiting_vetting().prefetch_related("submission")
 
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return (
+            Q(submission__title__icontains=text)
+            | Q(submission__preprint__identifier_w_vn_nr__icontains=text)
+            | Q(submission__author_list__unaccent__icontains=text)
+        )
+
 
 class SelectRefereeingCycleTask(TaskKind):
     name = "Select Refereeing Cycle"
@@ -370,3 +418,11 @@ class SelectRefereeingCycleTask(TaskKind):
             editor_in_charge=cls.user.contributor,
             refereeing_cycle__isnull=False,
         )
+
+    @staticmethod
+    def search_query(text: str) -> Q:
+        return (
+            Q(title__icontains=text)
+            | Q(preprint__identifier_w_vn_nr__icontains=text)
+            | Q(author_list__unaccent__icontains=text)
+        )
diff --git a/scipost_django/tasks/templates/tasks/_hx_task_table.html b/scipost_django/tasks/templates/tasks/_hx_task_table.html
new file mode 100644
index 000000000..6259129a8
--- /dev/null
+++ b/scipost_django/tasks/templates/tasks/_hx_task_table.html
@@ -0,0 +1,45 @@
+<table class="table">
+  <thead>
+    <tr>
+      <th scope="col">Type</th>
+      <th scope="col">Name</th>
+      <th scope="col">Due</th>
+      <th scope="col">Actions</th>
+    </tr>
+  </thead>
+  <tbody>
+
+    {% for task in tasks %}
+      <tr>
+        <td>{{ task.kind.name }}</td>
+        <td>{{ task.title }}</td>
+        <td>{{ task.due_date }}</td>
+        <td>
+
+          {% for action in task.actions|slice:":2" %}{{ action.as_html|safe }}{% endfor %}
+
+          {% if task.actions|length > 2 %}
+            <div class="btn-group" role="group">
+              <button class="btn btn-sm btn-light dropdown-toggle"
+                      type="button"
+                      data-bs-toggle="dropdown"
+                      aria-expanded="false">
+                <span>More</span>
+              </button>
+              <ul class="dropdown-menu dropdown-menu-end">
+
+                {% for action in task.actions|slice:"2:" %}
+                  <li class="dropdown-item">{{ action.element|safe }}</li>
+                {% endfor %}
+
+
+              </ul>
+            </div>
+          {% endif %}
+
+        </td>
+      </tr>
+    {% endfor %}
+
+  </tbody>
+</table>
diff --git a/scipost_django/tasks/templates/tasks/tasklist_new.html b/scipost_django/tasks/templates/tasks/tasklist_new.html
index 5cc36aed3..148bda4e4 100644
--- a/scipost_django/tasks/templates/tasks/tasklist_new.html
+++ b/scipost_django/tasks/templates/tasks/tasklist_new.html
@@ -1,5 +1,7 @@
 {% extends "scipost/base.html" %}
 
+{% load crispy_forms_tags %}
+
 {% block pagetitle %}: Tasklist{% endblock %}
 
 {% block content %}
@@ -13,57 +15,19 @@
     <h1>Tasklist</h1>
   </div>
 
-  <div class="d-flex flex-column gap-3">
-
-    <table class="table">
-      <thead>
-        <tr>
-          <th scope="col">Type</th>
-          <th scope="col">Name</th>
-          <th scope="col">Due</th>
-          <th scope="col">Actions</th>
-        </tr>
-      </thead>
-      <tbody>
-
-        {% for task_type, tasks in kinds_with_tasks.items %}
-
-          {% for task in tasks %}
-            <tr>
-              <td>{{ task.kind.name }}</td>
-              <td>{{ task.title }}</td>
-              <td>{{ task.due_date }}</td>
-              <td>
-
-                {% for action in task.actions|slice:":2" %}{{ action.as_html|safe }}{% endfor %}
-
-                {% if task.actions|length > 2 %}
-                  <div class="btn-group" role="group">
-                    <button class="btn btn-sm btn-light dropdown-toggle"
-                            type="button"
-                            data-bs-toggle="dropdown"
-                            aria-expanded="false">
-                      <span>More</span>
-                    </button>
-                    <ul class="dropdown-menu dropdown-menu-end">
-
-                      {% for action in task.actions|slice:"2:" %}
-                        <li class="dropdown-item">{{ action.element|safe }}</li>
-                      {% endfor %}
-
-
-                    </ul>
-                  </div>
-                {% endif %}
-
-              </td>
-            </tr>
-          {% endfor %}
-        {% endfor %}
+  <section aria-label="Search and filter tasks">
+    <form hx-get="{% url 'tasks:tasklist_new' %}"
+          hx-target="#tasklist-table"
+          hx-push-url="true"
+          hx-params="not csrfmiddlewaretoken"
+          hx-trigger="change delay:500ms">
+      {% crispy form %}
+    </form>
+  </section>
 
-      </tbody>
+  <div id="tasklist-table" class="d-flex flex-column gap-3">
 
-    </table>
+    {% include "tasks/_hx_task_table.html" %}
 
   </div>
 {% endblock %}
diff --git a/scipost_django/tasks/views.py b/scipost_django/tasks/views.py
index f29ff650b..7a07bd910 100644
--- a/scipost_django/tasks/views.py
+++ b/scipost_django/tasks/views.py
@@ -8,6 +8,7 @@ from django.shortcuts import render
 from colleges.permissions import is_edadmin_or_active_fellow
 from submissions.models.assignment import EditorialAssignment
 from submissions.models.recommendation import EICRecommendation
+from tasks.forms import TaskListSearchForm
 from tasks.tasks.task_kinds import get_all_task_kinds
 
 
@@ -45,10 +46,19 @@ def tasklist_new_grouped(request):
 @login_required
 @user_passes_test(is_edadmin_or_active_fellow)
 def tasklist_new(request):
+    form = TaskListSearchForm(request.GET, user=request.user)
+
+    tasks = []
+    if form.is_valid():
+        tasks = form.search_results()
+
     context = {
-        "kinds_with_tasks": {
-            task_type: task_type.get_tasks()
-            for task_type in get_all_task_kinds(request.user)
-        }
+        "form": form,
+        "tasks": tasks,
     }
+
+    # If htmx request, return only the task list
+    if request.headers.get("HX-Request") == "true":
+        return render(request, "tasks/_hx_task_table.html", context)
+
     return render(request, "tasks/tasklist_new.html", context)
-- 
GitLab