SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 93895fc2 authored by George Katsikas's avatar George Katsikas :goat:
Browse files

add search/filter form to tickes queue list view

parent b2d53b70
No related branches found
No related tags found
1 merge request!55Add search/filter form to tickets queue list view
......@@ -4,8 +4,14 @@ __license__ = "AGPL v3"
from django import forms
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from .models import Queue, Ticket, Followup
from .constants import TICKET_PRIORITIES, TICKET_STATUSES
from crispy_forms.helper import FormHelper, Layout
from crispy_bootstrap5.bootstrap5 import FloatingField, Field
from crispy_forms.layout import Div
from django.db.models import Q
class QueueForm(forms.ModelForm):
......@@ -87,3 +93,108 @@ class FollowupForm(forms.ModelForm):
self.fields["by"].widget = forms.HiddenInput()
self.fields["timestamp"].widget = forms.HiddenInput()
self.fields["action"].widget = forms.HiddenInput()
class TicketSearchForm(forms.Form):
title = forms.CharField(max_length=64, required=False)
description = forms.CharField(max_length=512, required=False)
priority = forms.MultipleChoiceField(choices=TICKET_PRIORITIES, required=False)
status = forms.MultipleChoiceField(choices=TICKET_STATUSES, required=False)
orderby = forms.ChoiceField(
label="Order by",
choices=(
("defined_on", "Opened date"),
("followups__latest__timestamp", "Latest activity"),
("status", "Status"),
("priority", "Priority"),
),
required=False,
)
ordering = forms.ChoiceField(
label="Ordering",
choices=(
# FIXME: Emperically, the ordering appers to be reversed for dates?
("-", "Descending"),
("+", "Ascending"),
),
required=False,
)
def __init__(self, *args, **kwargs):
if queue_slug := kwargs.pop("queue_slug", None):
self.queue = get_object_or_404(Queue, slug=queue_slug)
self.tickets = Ticket.objects.filter(queue=self.queue)
else:
self.tickets = Ticket.objects.all()
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Div(
Div(
Div(
Div(Field("priority", size=4), css_class="col-12"),
Div(FloatingField("title"), css_class="col-12"),
css_class="row mb-0",
),
css_class="col-6",
),
Div(Field("status", size=8), css_class="col-6"),
Div(FloatingField("description"), css_class="col-12"),
css_class="row mb-0",
),
Div(
Div(Field("ordering"), css_class="col-6"),
Div(Field("orderby"), css_class="col-6"),
css_class="row mb-0",
),
)
def search_results(self):
tickets = self.tickets
if title := self.cleaned_data.get("title"):
tickets = tickets.filter(title__icontains=title)
if description := self.cleaned_data.get("description"):
tickets = tickets.filter(description__icontains=description)
def is_in_or_null(queryset, key, value, implicit_all=True):
"""
Filter a queryset by a list of values. If the list contains a 0, then
also include objects where the key is null. If the list is empty, then
include all objects if implicit_all is True.
"""
value = self.cleaned_data.get(value)
has_unassigned = "0" in value
is_unassigned = Q(**{key + "__isnull": True})
is_in_values = Q(**{key + "__in": list(filter(lambda x: x != 0, value))})
if has_unassigned:
return queryset.filter(is_unassigned | is_in_values)
elif implicit_all and not value:
return queryset
else:
return queryset.filter(is_in_values)
tickets = is_in_or_null(tickets, "priority", "priority")
tickets = is_in_or_null(tickets, "status", "status")
# Ordering of streams
# Only order if both fields are set
if (orderby_value := self.cleaned_data.get("orderby")) and (
ordering_value := self.cleaned_data.get("ordering")
):
# Remove the + from the ordering value, causes a Django error
ordering_value = ordering_value.replace("+", "")
# Ordering string is built by the ordering (+/-), and the field name
# from the orderby field split by "," and joined together
tickets = tickets.order_by(
*[
ordering_value + order_part
for order_part in orderby_value.split(",")
]
)
return tickets
{% extends 'helpdesk/base.html' %}
{% load bootstrap %}
{% load guardian_tags %}
{% load automarkup %}
{% load crispy_forms_tags %}
{% block breadcrumb_items %}
{{ block.super }}
<span class="breadcrumb-item">Queue: {{ queue.name }}</span>
{{ block.super }} <span class="breadcrumb-item">Queue: {{ queue.name }}</span>
{% endblock %}
{% block pagetitle %}: Queue details{% endblock pagetitle %}
{% block pagetitle %}
: Queue details
{% endblock pagetitle %}
{% get_obj_perms request.user for queue as "user_perms" %}
{% block content %}
<div class="row">
<div class="col-12">
<h3 class="highlight">Queue: {{ queue.name }}</h3>
{% if queue.parent_queue %}
<p>Parent: <a href="{% url 'helpdesk:queue_detail' slug=queue.parent_queue.slug %}">{{ queue.parent_queue }}</a></p>
<p>
Parent: <a href="{% url 'helpdesk:queue_detail' slug=queue.parent_queue.slug %}">{{ queue.parent_queue }}</a>
</p>
{% endif %}
{% if queue.sub_queues.all|length > 0 %}
<p>Sub-queues: {% for sub in queue.sub_queues.all %}<a href="{% url 'helpdesk:queue_detail' slug=sub.slug %}">{{ sub }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
{% endif %}
<p>
Sub-queues:
{% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %}
<div class="container border border-danger p-2">
<h4>Admin actions:</h4>
{% if perms.helpdesk.delete_queue or "delete_queue" in user_perms %}
<a class="btn btn-sm btn-danger" role="button" href="{% url 'helpdesk:queue_delete' slug=queue.slug %}">{% include 'bi/trash-fill.html' %}&nbsp;Delete this Queue</a>
{% endif %}
{% if perms.helpdesk.update_queue or "update_queue" in user_perms %}
<a class="btn btn-sm btn-warning" role="button" href="{% url 'helpdesk:queue_update' slug=queue.slug %}">{% include 'bi/pencil-square.html' %}&nbsp;Update this Queue</a>
{% endif %}
{% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %}
<a class="btn btn-sm btn-primary" href="{% url 'helpdesk:queue_create' parent_slug=queue.slug %}">{% include 'bi/plus-square-fill.html' %}&nbsp;Add a sub-Queue to this Queue</a>
{% endif %}
<hr/>
<div class="card">
<div class="card-header">
Permissions on this Queue instance
<button class="btn btn-link small" data-bs-toggle="collapse" data-bs-target="#permissionsCard">
View/manage</button>
</div>
<div class="card-body collapse" id="permissionsCard">
<strong>Note: Permissions are handled at the group level. To change managing and/or response groups, click on the <span class="bg-warning p-1">Update</span> button above.</strong>
<br/><br/>
<h4>Managing group:</h4>
{% get_obj_perms queue.managing_group for queue as "group_perms" %}
<ul>
<li>{{ queue.managing_group }}: {{ group_perms }}</li>
</ul>
<h4>Response groups:</h4>
<ul>
{% for group in queue.response_groups.all %}
{% get_obj_perms group for queue as "group_perms" %}
<li>{{ group.name }}: {{ group_perms }}</li>
{% empty %}
<li>No group has permissions on this Queue</li>
{% endfor %}
</ul>
<p>Users with permissions:</p>
<ul>
{% for u in users_with_perms %}
{% get_obj_perms u for queue as "u_perms" %}
<li>{{ u.first_name }} {{ u.last_name }}: {{ u_perms }}</li>
{% empty %}
<li>No user has permissions on this Queue</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% for sub in queue.sub_queues.all %}
<a href="{% url 'helpdesk:queue_detail' slug=sub.slug %}">{{ sub }}</a>
{% if not forloop.last %},{% endif %}
{% endfor %}
</p>
{% endif %}
<h3 class="highlight">Description</h3>
{% automarkup queue.description %}
<h3 class="highlight">Tickets in this Queue</h3>
{% include 'helpdesk/tickets_table.html' with tickets=queue.tickets.all %}
{% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %}
<div class="container border border-danger p-2">
<h4>Admin actions:</h4>
{% if perms.helpdesk.delete_queue or "delete_queue" in user_perms %}
<a class="btn btn-sm btn-danger"
role="button"
href="{% url 'helpdesk:queue_delete' slug=queue.slug %}">
{% include 'bi/trash-fill.html' %}
&nbsp;Delete this Queue</a>
{% endif %}
{% if perms.helpdesk.update_queue or "update_queue" in user_perms %}
<a class="btn btn-sm btn-warning"
role="button"
href="{% url 'helpdesk:queue_update' slug=queue.slug %}">
{% include 'bi/pencil-square.html' %}
&nbsp;Update this Queue</a>
{% endif %}
{% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %}
<a class="btn btn-sm btn-primary"
href="{% url 'helpdesk:queue_create' parent_slug=queue.slug %}">
{% include 'bi/plus-square-fill.html' %}
&nbsp;Add a sub-Queue to this Queue</a>
{% endif %}
<hr />
<div class="card">
<div class="card-header">
Permissions on this Queue instance
<button class="btn btn-link small"
data-bs-toggle="collapse"
data-bs-target="#permissionsCard">View/manage</button>
</div>
<div class="card-body collapse" id="permissionsCard">
<strong>Note: Permissions are handled at the group level. To change managing and/or response groups, click on the <span class="bg-warning p-1">Update</span> button above.</strong>
<br />
<br />
<h4>Managing group:</h4>
{% get_obj_perms queue.managing_group for queue as "group_perms" %}
<ul>
<li>{{ queue.managing_group }}: {{ group_perms }}</li>
</ul>
<h4>Response groups:</h4>
<ul>
{% for group in queue.response_groups.all %}
{% get_obj_perms group for queue as "group_perms" %}
<li>{{ group.name }}: {{ group_perms }}</li>
{% empty %}
<li>No group has permissions on this Queue</li>
{% endfor %}
</ul>
<p>Users with permissions:</p>
<ul>
{% for u in users_with_perms %}
{% get_obj_perms u for queue as "u_perms" %}
<li>{{ u.first_name }} {{ u.last_name }}: {{ u_perms }}</li>
{% empty %}
<li>No user has permissions on this Queue</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
<h3 class="highlight">Description</h3>
{% automarkup queue.description %}
<div class="highlight d-flex justify-content-between align-items-center px-3">
<div class="fs-6">Tickets in this Queue</div>
<div id="indicator-search-tickets" class="htmx-indicator">
<button class="btn btn-warning text-white d-none d-md-block me-2"
type="button"
disabled>
<strong>Loading...</strong>
<div class="spinner-grow spinner-grow-sm ms-2"
role="status"
aria-hidden="true"></div>
</button>
</div>
</div>
<div id="ticket-search-form-container">
<form hx-post="{% url 'helpdesk:_hx_ticket_table' slug=queue.slug %}"
hx-trigger="load, keyup delay:500ms, change delay:500ms, click from:#refresh-button"
hx-sync="#search-tickets-form:replace"
hx-target="#search-tickets-results"
hx-indicator="#indicator-search-tickets">
<div id="search-tickets-form">{% crispy search_tickets_form %}</div>
</form>
</div>
<div id="search-tickets-results" class="mt-2"></div>
</div>
</div>
{% endblock content %}
......@@ -27,6 +27,11 @@ urlpatterns = [
name="queue_delete",
),
path("queue/<slug:slug>/", views.QueueDetailView.as_view(), name="queue_detail"),
path(
"queue/<slug:slug>/_hx_ticket_table",
views._hx_ticket_table,
name="_hx_ticket_table",
),
path(
"ticket/add/<int:concerning_type_id>/<int:concerning_object_id>/",
views.TicketCreateView.as_view(),
......
......@@ -6,7 +6,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.utils import timezone
from django.views.generic.detail import DetailView
......@@ -39,7 +39,13 @@ from .constants import (
TICKET_FOLLOWUP_ACTION_MARK_CLOSED,
)
from .models import Queue, Ticket, Followup
from .forms import QueueForm, TicketForm, TicketAssignForm, FollowupForm
from .forms import (
QueueForm,
TicketForm,
TicketAssignForm,
FollowupForm,
TicketSearchForm,
)
from mails.utils import DirectMailUtil
......@@ -163,6 +169,11 @@ class QueueDetailView(PermissionRequiredMixin, DetailView):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["users_with_perms"] = get_users_with_perms(self.object)
if queue := context.get("queue"):
search_tickets_form = TicketSearchForm(None, queue_slug=queue.slug)
context["search_tickets_form"] = search_tickets_form
return context
......@@ -370,3 +381,18 @@ class TicketMarkClosed(TicketFollowupView):
ticket.save()
self.object = form.save()
return redirect(self.get_success_url())
def _hx_ticket_table(request, slug):
form = TicketSearchForm(request.POST or None, queue_slug=slug)
if form.is_valid():
tickets = form.search_results()
else:
tickets = form.tickets
return render(
request,
"helpdesk/tickets_table.html",
{"tickets": tickets},
)
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