SciPost Code Repository

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

overhaul helpdesk ticket table view

fixes #285
parent 2b44c510
No related branches found
No related tags found
No related merge requests found
Showing
with 492 additions and 190 deletions
......@@ -2,16 +2,22 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import datetime
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.backends.db import SessionStore
from django.db.models.functions import Concat
from django.shortcuts import get_object_or_404
from profiles.models import Profile
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
from django.db.models import Q, Case, CharField, OuterRef, Subquery, Value, When
class QueueForm(forms.ModelForm):
......@@ -98,13 +104,34 @@ class FollowupForm(forms.ModelForm):
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)
assigned_to = forms.MultipleChoiceField(
required=False, choices=[("0", "Unassigned")]
)
defined_by = forms.CharField(
max_length=128,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": "Name, email, or ORCID. Partial matches may not work as expected."
}
),
)
priority = forms.MultipleChoiceField(
choices=[(key, key.title()) for key, _ in TICKET_PRIORITIES], required=False
)
status = forms.MultipleChoiceField(choices=TICKET_STATUSES, required=False)
concerning_object = forms.CharField(
max_length=128,
required=False,
widget=forms.TextInput(attrs={"placeholder": "ID of concerning object"}),
)
orderby = forms.ChoiceField(
label="Order by",
choices=(
("defined_on", "Opened date"),
("defined_on", "Defined on"),
("defined_by__contributor__profile__last_name", "Last name"),
("defined_by__contributor__profile__first_name", "First name"),
("followups__latest__timestamp", "Latest activity"),
("status", "Status"),
("priority", "Priority"),
......@@ -121,43 +148,161 @@ class TicketSearchForm(forms.Form):
required=False,
)
def save_fields_to_session(self):
# Save the form data to the session
if self.session_key is not None:
session = SessionStore(session_key=self.session_key)
for field_key in self.cleaned_data:
session_key = (
f"{self.form_id}_{field_key}"
if hasattr(self, "form_id")
else field_key
)
if field_value := self.cleaned_data.get(field_key):
if isinstance(field_value, datetime.date):
field_value = field_value.strftime("%Y-%m-%d")
session[session_key] = field_value
session.save()
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 __init__(self, *args, **kwargs):
if queue_slug := kwargs.pop("queue_slug", None):
self.queue = get_object_or_404(Queue, slug=queue_slug)
if not (user := kwargs.pop("user", None)):
raise ValueError("user is required to filter the tickets")
self.session_key = kwargs.pop("session_key", None)
if queue := kwargs.pop("queue", None):
self.queue = queue
self.tickets = Ticket.objects.filter(queue=self.queue)
else:
self.tickets = Ticket.objects.all()
self.tickets = self.tickets.visible_by(user)
super().__init__(*args, **kwargs)
self.fields["assigned_to"].choices += (
User.objects.filter(
pk__in=self.tickets.values_list("assigned_to", flat=True).distinct()
)
.annotate(
full_name=Concat(
"contributor__profile__first_name",
Value(" "),
"contributor__profile__last_name",
output_field=CharField(),
)
)
.values_list("id", "full_name")
)
# Set the initial values of the form fields from the session data
if self.session_key:
session = SessionStore(session_key=self.session_key)
for field_key in self.fields:
session_key = (
f"{self.form_id}_{field_key}"
if hasattr(self, "form_id")
else field_key
)
if session_value := session.get(session_key):
self.fields[field_key].initial = session_value
self.helper = FormHelper()
div_block_ordering = Div(
Div(Field("orderby"), css_class="col-12"),
Div(Field("ordering"), css_class="col-12"),
css_class="row mb-0",
)
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(Field("title"), css_class="col-12 col-md-6"),
Div(Field("defined_by"), css_class="col-12 col-md-6"),
Div(Field("description"), css_class="col-12 col-md"),
Div(Field("concerning_object"), css_class="col-12 col-md-4"),
css_class="row",
),
Div(
Div(Field("ordering"), css_class="col-6"),
Div(Field("orderby"), css_class="col-6"),
css_class="row mb-0",
Div(Field("assigned_to", size=7), css_class="col-12 col-sm-6 col-lg"),
Div(Field("status", size=7), css_class="col-12 col-sm-6 col-lg"),
Div(
Field("priority", size=5), css_class="col-auto col-sm-6 col-lg-auto"
),
Div(div_block_ordering, css_class="col col-sm-6 col-md"),
css_class="row",
),
)
def search_results(self):
self.save_fields_to_session()
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)
if defined_by := self.cleaned_data.get("defined_by"):
profiles_matched = Profile.objects.search(defined_by)
tickets = tickets.filter(
defined_by__contributor__profile__in=profiles_matched
)
if concerning_object := self.cleaned_data.get("concerning_object"):
from submissions.models import Submission, Report
# If the concerning object is a submission, also check it preprint identifier
report_type = ContentType.objects.get_for_model(Report)
submission_type = ContentType.objects.get_for_model(Submission)
tickets = tickets.annotate(
preprint_id=Case(
When(
concerning_object_type=submission_type,
then=Subquery(
Submission.objects.filter(
pk=OuterRef("concerning_object_id")
).values("preprint__identifier_w_vn_nr")
),
),
When(
concerning_object_type=report_type,
then=Subquery(
Report.objects.filter(
pk=OuterRef("concerning_object_id")
).values("submission__preprint__identifier_w_vn_nr")
),
),
default=Value(""),
output_field=CharField(),
)
)
# Include matches with the concerning object preprint ID
Q_concerning_object = Q(preprint_id__icontains=concerning_object)
# Include matches with the concerning object ID if input is an integer
if concerning_object.isdigit():
Q_concerning_object |= Q(concerning_object_id=concerning_object)
tickets = tickets.filter(
Q(concerning_object_id__isnull=False) & Q_concerning_object
)
def is_in_or_null(queryset, key, value, implicit_all=True):
"""
......@@ -179,6 +324,7 @@ class TicketSearchForm(forms.Form):
tickets = is_in_or_null(tickets, "priority", "priority")
tickets = is_in_or_null(tickets, "status", "status")
tickets = is_in_or_null(tickets, "assigned_to", "assigned_to")
# Ordering of streams
# Only order if both fields are set
......
......@@ -15,6 +15,8 @@ from .constants import (
TICKET_STATUS_CLOSED,
)
from guardian.shortcuts import get_objects_for_user
class QueueQuerySet(models.QuerySet):
def anchors(self):
......@@ -52,3 +54,28 @@ class TicketQuerySet(models.QuerySet):
def handled(self):
return self.filter(status__in=[TICKET_STATUS_RESOLVED, TICKET_STATUS_CLOSED])
def visible_by(self, user):
from helpdesk.models import Queue
# If user has permission to view all tickets in the queue, return all tickets
# in the queue. Otherwise, return only tickets assigned to the user.
if user.has_perm("helpdesk.can_view_all_tickets"):
return self
user_viewable_queues = get_objects_for_user(
user, "helpdesk.can_view_queue", klass=Queue
)
tickets_viewable_because_of_queue = self.filter(queue__in=user_viewable_queues)
user_viewable_tickets = get_objects_for_user(
user, "helpdesk.can_view_ticket", klass=self
)
user_handled_tickets = self.filter(assigned_to=user)
return (
tickets_viewable_because_of_queue
| user_viewable_tickets
| user_handled_tickets
)
......@@ -190,13 +190,16 @@ class Ticket(models.Model):
@property
def is_awaiting_handling(self):
return self.status in [TICKET_STATUS_ASSIGNED, TICKET_STATUS_PASSED_ON]
return self.status in [
TICKET_STATUS_ASSIGNED,
TICKET_STATUS_PASSED_ON,
TICKET_STATUS_AWAITING_RESPONSE_ASSIGNEE,
]
@property
def is_in_handling(self):
return self.status in [
TICKET_STATUS_PICKEDUP,
TICKET_STATUS_AWAITING_RESPONSE_ASSIGNEE,
TICKET_STATUS_AWAITING_RESPONSE_USER,
]
......
{% load crispy_forms_tags %}
<form id="ticket-search-form"
{% if queue %}
hx-post="{% url 'helpdesk:_hx_ticket_search_table' queue_slug=queue.slug %}"
{% else %}
hx-post="{% url 'helpdesk:_hx_ticket_search_table' %}"
{% endif %}
hx-trigger="load, keyup delay:500ms, change delay:500ms, click from:#refresh-button"
hx-sync="#ticket-search-form:replace"
hx-target="#ticket-search-results"
hx-indicator="#ticket-search-indicator">
{% crispy form %}
</form>
{% for ticket in page_obj %}
{% include 'helpdesk/_hx_ticket_search_table_row.html' %}
{% empty %}
<tr id="ticket-search-results-load-next" hx-swap-oob="true">
<td colspan="12" class="text-center p-0">
<div class="p-2 d-flex justify-content-center">
<strong>No Tickets could be found</strong>
</div>
</td>
</tr>
{% endfor %}
{% if page_obj.has_next %}
<tr id="ticket-search-results-load-next"
class="htmx-indicator"
hx-swap-oob="true"
{% if queue %}
hx-post="{% url 'helpdesk:_hx_ticket_search_table' queue_slug=queue.slug %}?page={{ page_obj.next_page_number }}"
{% else %}
hx-post="{% url 'helpdesk:_hx_ticket_search_table' %}?page={{ page_obj.next_page_number }}"
{% endif %}
hx-target="#ticket-search-results"
hx-include="#ticket-search-form"
hx-trigger="revealed"
hx-swap="beforeend"
hx-indicator="#ticket-search-results-load-next">
<td colspan="12" class="text-center p-0">
<div class="p-2 bg-primary bg-opacity-25 d-flex justify-content-center">
<strong>Loading page {{ page_obj.next_page_number }} out of {{ page_obj.paginator.num_pages }}</strong>
<div class="spinner-grow spinner-grow-sm ms-2"
role="status"
aria-hidden="true"></div>
</div>
</td>
</tr>
{% else %}
<tr id="ticket-search-results-load-next" hx-swap-oob="true">
<td colspan="12" class="text-center p-0">
<div class="p-2 d-flex justify-content-center">
<strong>All Tickets loaded</strong>
</div>
</td>
</tr>
{% endif %}
<tr class="text-nowrap">
<td class="text-truncate" style="max-width:1px;">
<span class="text-muted">{{ ticket.queue }}</span>
<br />
<a href="{{ ticket.get_absolute_url }}">{{ ticket.title }}</a>
{% if ticket.concerning_object %}
<span class="text-small text-muted" style="font-size: 80%;">
<br />
Re: <a href="{{ ticket.concerning_object.get_absolute_url }}" target="_blank">{{ ticket.concerning_object }}</a>
</span>
{% endif %}
</td>
<td>
{{ ticket.defined_on|date:"Y-m-d" }}
<br />
<a href="{{ ticket.defined_by.contributor.profile.get_absolute_url }}">{{ ticket.defined_by.contributor.profile.full_name }}</a>
</td>
<td data-bs-toggle="tooltip" title="{{ ticket.priority }}">
{% for a in "x"|ljust:ticket.priority_level %}
{% include 'bi/exclamation-square-fill.html' %}
{% endfor %}
</td>
{% with classes=ticket.status_classes %}
<td>
<span class="bg-{{ classes.class }} text-{{ classes.text }}">&emsp;</span>
{{ ticket.get_status_display }}
</td>
{% endwith %}
<td>
{% if ticket.assigned_to %}
{{ ticket.assigned_to }}
{% else %}
-
{% endif %}
</td>
<td>
{{ ticket.latest_activity }}
{% if ticket.is_open %}
<br />
<span class="text-small text-muted" style="font-size: 80%;">[{{ ticket.latest_activity|timesince }} ago]</span>
{% endif %}
</td>
</tr>
<section name="search_form">
<details id="ticket-search-details" class="card mt-3 mb-1" open>
<summary class="card-header d-flex flex-row align-items-center justify-content-between list-triangle">
<h2 class="fs-3 my-2">Search / Filter</h2>
<div class="d-none d-md-flex align-items-center">
<div id="ticket-search-indicator" 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>
{% if queue %}
<button class="btn btn-outline-secondary me-2"
type="button"
hx-get="{% url 'helpdesk:_hx_ticket_search_form' queue_slug=queue.slug filter_set="empty" %}"
hx-target="#ticket-search-form-container">Clear Filters</button>
{% else %}
<button class="btn btn-outline-secondary me-2"
type="button"
hx-get="{% url 'helpdesk:_hx_ticket_search_form' filter_set="empty" %}"
hx-target="#ticket-search-form-container">Clear Filters</button>
{% endif %}
<a id="refresh-button" class="me-2 btn btn-primary">
{% include "bi/arrow-clockwise.html" %}
&nbsp;Refresh</a>
</div>
</summary>
<div class="card-body">
{% if queue %}
<div id="ticket-search-form-container"
hx-get="{% url 'helpdesk:_hx_ticket_search_form' queue_slug=queue.slug filter_set='latest' %}"
hx-trigger="load, intersect once"></div>
{% else %}
<div id="ticket-search-form-container"
hx-get="{% url 'helpdesk:_hx_ticket_search_form' filter_set='latest' %}"
hx-trigger="load, intersect once"></div>
{% endif %}
</div>
</details>
</section>
<section class="table-responsive" name="search_results">
<table class="table table-hover table-center position-relative">
<colgroup>
<col width="40%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
</colgroup>
<thead class="table-light text-nowrap position-sticky top-0">
<tr>
<th>
<span class="text-muted">Queue</span>
<br />
Ticket
</th>
<th>
<span class="text-muted">Defined on</span>
<br />
Defined by
</th>
<th>Priority</th>
<th>Status</th>
<th>Assigned to</th>
<th>Latest activity</th>
</tr>
</thead>
<tbody id="ticket-search-results">
</tbody>
<tfoot>
<tr id="ticket-search-results-load-next"></tr>
</tfoot>
</table>
</section>
{% with unassigned=tickets.unassigned %}
<ul class="nav nav-tabs" id="ticketsTab{{ marker }}" role="tablist">
{% if unassigned|length > 0 %}
<li class="nav-item">
<a class="nav-link" id="ticketsUnassigned{{ marker }}-tab" data-bs-toggle="tab" href="#ticketsUnassigned{{ marker }}" role="tab" aria-controls="ticketsUnassigned{{ marker }}" aria-selected="true">Unassigned&emsp;<span class="badge bg-danger">{{ unassigned|length }}</span></a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" id="ticketsAwaiting{{ marker }}-tab" data-bs-toggle="tab" href="#ticketsAwaiting{{ marker }}" role="tab" aria-controls="ticketsAwaiting{{ marker }}" aria-selected="true">Awaiting handling&emsp;<span class="badge bg-warning">{{ tickets.awaiting_handling|length }}</span></a>
</li>
<li class="nav-item">
<a class="nav-link active" id="ticketsInHandling{{ marker }}-tab" data-bs-toggle="tab" href="#ticketsInHandling{{ marker }}" role="tab" aria-controls="ticketsInHandling{{ marker }}" aria-selected="false">In handling&emsp;<span class="badge bg-success">{{ tickets.in_handling|length }}</span></a>
</li>
<li class="nav-item">
<a class="nav-link" id="ticketsHandled{{ marker }}-tab" data-bs-toggle="tab" href="#ticketsHandled{{ marker }}" role="tab" aria-controls="ticketsHandled{{ marker }}" aria-selected="false">Handled&emsp;<span class="badge bg-primary">{{ tickets.handled|length }}</span></a>
</li>
</ul>
<div class="tab-content" id="ticketsTabContent{{ marker }}">
{% if unassigned|length > 0 %}
<div class="tab-pane" id="ticketsUnassigned{{ marker }}" role="tabpanel">
{% include 'helpdesk/tickets_table.html' with tickets=tickets.unassigned %}
</div>
{% endif %}
<div class="tab-pane" id="ticketsAwaiting{{ marker }}" role="tabpanel">
{% include 'helpdesk/tickets_table.html' with tickets=tickets.awaiting_handling %}
</div>
<div class="tab-pane show active" id="ticketsInHandling{{ marker }}" role="tabpanel">
{% include 'helpdesk/tickets_table.html' with tickets=tickets.in_handling %}
</div>
<div class="tab-pane" id="ticketsHandled{{ marker }}" role="tabpanel">
{% include 'helpdesk/tickets_table.html' with tickets=tickets.handled %}
</div>
</div>
{% endwith %}
......@@ -8,7 +8,9 @@
{% endblock %}
{% block pagetitle %}: Helpdesk{% endblock pagetitle %}
{% block pagetitle %}
: Helpdesk
{% endblock pagetitle %}
{% block content %}
......@@ -17,60 +19,42 @@
<h2 class="highlight">Helpdesk</h2>
<ul>
{% if perms.helpdesk.add_queue %}
<li><a href="{% url 'helpdesk:queue_create' %}">Create a new Queue</a></li>
{% endif %}
<li><a href="{% url 'helpdesk:ticket_create' %}">Open a new Ticket</a></li>
</ul>
{% if request.user.ticket_set.all|length > 0 %}
<br class="my-4">
<h3 class="highlight">Tickets you opened</h3>
<div class="p-2">
{% include 'helpdesk/_tickets_tablist.html' with tickets=request.user.ticket_set.all marker="own" %}
</div>
{% endif %}
{% if request.user.assigned_tickets.all|length > 0 %}
<br class="my-4">
<h2 class="highlight">Tickets assigned to you</h2>
<div class="p-2">
{% include 'helpdesk/_tickets_tablist.html' with tickets=request.user.assigned_tickets marker="assigned" %}
</div>
{% endif %}
{% if perms.helpdesk.add_queue %}
<li>
<a href="{% url 'helpdesk:queue_create' %}">Create a new Queue</a>
</li>
{% endif %}
{% if object_list.all|length > 0 %}
<br class="my-4">
<h3 class="highlight">Other Tickets in your Queues <small class="text-muted"><em>[please feel free to pick up or handle further]</em></small></h3>
<div class="p-2">
{% include 'helpdesk/_tickets_tablist.html' with tickets=object_list marker="other"%}
</div>
{% endif %}
<li>
<a href="{% url 'helpdesk:ticket_create' %}">Open a new Ticket</a>
</li>
</ul>
{% if managed_queues.all|length > 0 %}
<br class="my-4">
<h3 class="highlight">Queues for which you are in managing group</h3>
<div class="row p-2">
{% for queue in managed_queues %}
<div class="col-md-6 col-lg-4 mb-2">
{% include 'helpdesk/queue_card.html' with queue=queue %}
</div>
{% endfor %}
</div>
<h3 class="highlight">Queues you can manage</h3>
<div class="row p-2">
{% for queue in managed_queues %}
<div class="col-md-6 col-lg-4 mb-2">{% include 'helpdesk/queue_card.html' with queue=queue %}</div>
{% endfor %}
</div>
{% endif %}
{% if visible_queues.all|length > 0 %}
<br class="my-4">
<h3 class="highlight">Queues which you can view</h3>
<div class="row p-2">
{% for queue in visible_queues %}
<div class="col-md-6 col-lg-4 mb-2">
{% include 'helpdesk/queue_card.html' with queue=queue %}
</div>
{% endfor %}
</div>
<h3 class="highlight">Queues you can view</h3>
<div class="row p-2">
{% for queue in visible_queues %}
<div class="col-md-6 col-lg-4 mb-2">{% include 'helpdesk/queue_card.html' with queue=queue %}</div>
{% endfor %}
</div>
{% endif %}
{% include "helpdesk/_ticket_search_section.html" %}
</div>
</div>
......
......@@ -13,9 +13,6 @@
<h3 class="highlight">Description</h3>
{% automarkup object.description %}
<h3 class="highlight">Tickets</h3>
{% include 'helpdesk/tickets_table.html' with queue=object %}
</div>
</div>
......
......@@ -143,17 +143,7 @@
</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>
{% include "helpdesk/_ticket_search_section.html" %}
</div>
</div>
......
<table class="table table-hover">
<colgroup>
<col width="40%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
<col width="0%" />
</colgroup>
<thead class="table-light text-nowrap">
<tr>
<th><span class="text-muted">Queue</span><br/>Ticket</th>
<th><span class="text-muted">Defined on</span><br/>Defined by</th>
<th>Priority</th>
<th>Status</th>
<th>Assigned to</th>
<th>Latest activity</th>
</tr>
</thead>
<tbody>
{% for ticket in tickets %}
<tr class="text-nowrap">
<td class="text-truncate" style="max-width:1px;"><span class="text-muted">{{ ticket.queue }}</span><br/>
<a href="{{ ticket.get_absolute_url }}">{{ ticket.title }}</a>
{% if ticket.concerning_object %}
<span class="text-muted" style="font-size: 80%;">
<br/>Re: <a href="{{ ticket.concerning_object.get_absolute_url }}" target="_blank">{{ ticket.concerning_object }}</a>
</span>
{% endif %}
</td>
<td>
{{ ticket.defined_on|date:"Y-m-d" }}<br />
<a href="{{ ticket.defined_by.contributor.profile.get_absolute_url }}">{{ ticket.defined_by.contributor.profile.full_name }}</a>
</td>
<td data-bs-toggle="tooltip" title="{{ ticket.priority }}">
{% for a in "x"|ljust:ticket.priority_level %}
{% include 'bi/exclamation-square-fill.html' %}
{% endfor %}
</td>
{% with classes=ticket.status_classes %}
<td>
<span class="bg-{{ classes.class }} text-{{ classes.text }}">&emsp;</span>
{{ ticket.get_status_display }}
</td>
{% endwith %}
<td>{% if ticket.assigned_to %}{{ ticket.assigned_to }}{% else %}-{% endif %}</td>
<td>
{{ ticket.latest_activity }}
{% if ticket.is_open %}
<br/><span class="text-muted" style="font-size: 80%;">[{{ ticket.latest_activity|timesince }} ago]</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7">No ticket visible in this queue</td>
</tr>
{% endfor %}
</tbody>
</table>
......@@ -10,6 +10,16 @@ app_name = "helpdesk"
urlpatterns = [
path("", views.HelpdeskView.as_view(), name="helpdesk"),
path(
"_hx_ticket_search_form/<str:filter_set>",
views._hx_ticket_search_form,
name="_hx_ticket_search_form",
),
path(
"_hx_ticket_search_table",
views._hx_ticket_search_table,
name="_hx_ticket_search_table",
),
path(
"queue/<slug:parent_slug>/add/",
views.QueueCreateView.as_view(),
......@@ -28,9 +38,14 @@ urlpatterns = [
),
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",
"queue/<slug:queue_slug>/_hx_ticket_search_form/<str:filter_set>",
views._hx_ticket_search_form,
name="_hx_ticket_search_form",
),
path(
"queue/<slug:queue_slug>/_hx_ticket_search_table",
views._hx_ticket_search_table,
name="_hx_ticket_search_table",
),
path(
"ticket/add/<int:concerning_type_id>/<int:concerning_object_id>/",
......
......@@ -6,6 +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.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.utils import timezone
......@@ -170,10 +171,6 @@ class QueueDetailView(PermissionRequiredMixin, DetailView):
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
......@@ -383,16 +380,54 @@ class TicketMarkClosed(TicketFollowupView):
return redirect(self.get_success_url())
def _hx_ticket_table(request, slug):
form = TicketSearchForm(request.POST or None, queue_slug=slug)
def _hx_ticket_search_form(request, filter_set: str, queue_slug=None):
queue = get_object_or_404(Queue, slug=queue_slug) if queue_slug else None
form = TicketSearchForm(
request.POST or None,
user=request.user,
queue=queue,
session_key=request.session.session_key,
)
if filter_set == "empty":
form.apply_filter_set(
{
"show_email_unknown": True,
"show_with_CI": True,
"show_unavailable": True,
},
none_on_empty=True,
)
context = {"form": form, "queue": queue}
return render(request, "helpdesk/_hx_ticket_search_form.html", context)
def _hx_ticket_search_table(request, queue_slug=None):
queue = get_object_or_404(Queue, slug=queue_slug) if queue_slug else None
form = TicketSearchForm(
request.POST or None,
user=request.user,
queue=queue,
session_key=request.session.session_key,
)
if form.is_valid():
tickets = form.search_results()
else:
tickets = form.tickets
return render(
request,
"helpdesk/tickets_table.html",
{"tickets": tickets},
)
paginator = Paginator(tickets, 16)
page_nr = request.GET.get("page")
page_obj = paginator.get_page(page_nr)
count = paginator.count
start_index = page_obj.start_index
context = {
"queue": queue,
"count": count,
"page_obj": page_obj,
"start_index": start_index,
}
return render(request, "helpdesk/_hx_ticket_search_table.html", context)
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