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" ...@@ -4,8 +4,14 @@ __license__ = "AGPL v3"
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from .models import Queue, Ticket, Followup 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): class QueueForm(forms.ModelForm):
...@@ -87,3 +93,108 @@ class FollowupForm(forms.ModelForm): ...@@ -87,3 +93,108 @@ class FollowupForm(forms.ModelForm):
self.fields["by"].widget = forms.HiddenInput() self.fields["by"].widget = forms.HiddenInput()
self.fields["timestamp"].widget = forms.HiddenInput() self.fields["timestamp"].widget = forms.HiddenInput()
self.fields["action"].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' %} {% extends 'helpdesk/base.html' %}
{% load bootstrap %} {% load bootstrap %}
{% load guardian_tags %} {% load guardian_tags %}
{% load automarkup %} {% load automarkup %}
{% load crispy_forms_tags %}
{% block breadcrumb_items %} {% block breadcrumb_items %}
{{ block.super }} {{ block.super }} <span class="breadcrumb-item">Queue: {{ queue.name }}</span>
<span class="breadcrumb-item">Queue: {{ queue.name }}</span>
{% endblock %} {% endblock %}
{% block pagetitle %}: Queue details{% endblock pagetitle %} {% block pagetitle %}
: Queue details
{% endblock pagetitle %}
{% get_obj_perms request.user for queue as "user_perms" %} {% get_obj_perms request.user for queue as "user_perms" %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h3 class="highlight">Queue: {{ queue.name }}</h3> <h3 class="highlight">Queue: {{ queue.name }}</h3>
{% if queue.parent_queue %} {% 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 %} {% endif %}
{% if queue.sub_queues.all|length > 0 %} {% 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> <p>
{% endif %} Sub-queues:
{% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %} {% for sub in queue.sub_queues.all %}
<div class="container border border-danger p-2"> <a href="{% url 'helpdesk:queue_detail' slug=sub.slug %}">{{ sub }}</a>
<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>
{% if not forloop.last %},{% endif %}
{% endfor %}
</p>
{% endif %} {% endif %}
<h3 class="highlight">Description</h3>
{% automarkup queue.description %}
<h3 class="highlight">Tickets in this Queue</h3> {% if perms.helpdesk.add_queue or request.user in queue.managing_group.users.all %}
{% include 'helpdesk/tickets_table.html' with tickets=queue.tickets.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>
</div> </div>
{% endblock content %} {% endblock content %}
...@@ -27,6 +27,11 @@ urlpatterns = [ ...@@ -27,6 +27,11 @@ urlpatterns = [
name="queue_delete", name="queue_delete",
), ),
path("queue/<slug:slug>/", views.QueueDetailView.as_view(), name="queue_detail"), 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( path(
"ticket/add/<int:concerning_type_id>/<int:concerning_object_id>/", "ticket/add/<int:concerning_type_id>/<int:concerning_object_id>/",
views.TicketCreateView.as_view(), views.TicketCreateView.as_view(),
......
...@@ -6,7 +6,7 @@ from django.contrib import messages ...@@ -6,7 +6,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType 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.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
...@@ -39,7 +39,13 @@ from .constants import ( ...@@ -39,7 +39,13 @@ from .constants import (
TICKET_FOLLOWUP_ACTION_MARK_CLOSED, TICKET_FOLLOWUP_ACTION_MARK_CLOSED,
) )
from .models import Queue, Ticket, Followup 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 from mails.utils import DirectMailUtil
...@@ -163,6 +169,11 @@ class QueueDetailView(PermissionRequiredMixin, DetailView): ...@@ -163,6 +169,11 @@ class QueueDetailView(PermissionRequiredMixin, DetailView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context["users_with_perms"] = get_users_with_perms(self.object) 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 return context
...@@ -370,3 +381,18 @@ class TicketMarkClosed(TicketFollowupView): ...@@ -370,3 +381,18 @@ class TicketMarkClosed(TicketFollowupView):
ticket.save() ticket.save()
self.object = form.save() self.object = form.save()
return redirect(self.get_success_url()) 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