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