diff --git a/scipost_django/helpdesk/forms.py b/scipost_django/helpdesk/forms.py index eae797776529b2ba143ca313920199647bcb77bf..47810c458ba9273206bb6d4fb70042fc07bb915d 100644 --- a/scipost_django/helpdesk/forms.py +++ b/scipost_django/helpdesk/forms.py @@ -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 diff --git a/scipost_django/helpdesk/managers.py b/scipost_django/helpdesk/managers.py index de77a2295aa8a3214bf5bdc72ec761333774710d..e63e9b749c3bf444bf073365a779e9220aa7643c 100644 --- a/scipost_django/helpdesk/managers.py +++ b/scipost_django/helpdesk/managers.py @@ -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 + ) diff --git a/scipost_django/helpdesk/models.py b/scipost_django/helpdesk/models.py index 10b4aaa443c9a3a4cbae241a2e6147961fe6cc4b..a9b61aa63d00589057d653208a78ed01fb91486b 100644 --- a/scipost_django/helpdesk/models.py +++ b/scipost_django/helpdesk/models.py @@ -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, ] diff --git a/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_form.html b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_form.html new file mode 100644 index 0000000000000000000000000000000000000000..a63c5db2bdc3f631a6f020c8461f825e469b9047 --- /dev/null +++ b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_form.html @@ -0,0 +1,15 @@ +{% 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> diff --git a/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table.html b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table.html new file mode 100644 index 0000000000000000000000000000000000000000..2031da1987b9545cd7288483929dd82bd4c84eb5 --- /dev/null +++ b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table.html @@ -0,0 +1,46 @@ +{% 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 %} diff --git a/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table_row.html b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table_row.html new file mode 100644 index 0000000000000000000000000000000000000000..854479335ba24e11eae8db32574436bf611f3f68 --- /dev/null +++ b/scipost_django/helpdesk/templates/helpdesk/_hx_ticket_search_table_row.html @@ -0,0 +1,51 @@ +<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 }}"> </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> diff --git a/scipost_django/helpdesk/templates/helpdesk/_ticket_search_section.html b/scipost_django/helpdesk/templates/helpdesk/_ticket_search_section.html new file mode 100644 index 0000000000000000000000000000000000000000..4dbf1aa4a031e46e8ab187851737872d5cc858ac --- /dev/null +++ b/scipost_django/helpdesk/templates/helpdesk/_ticket_search_section.html @@ -0,0 +1,88 @@ +<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" %} + 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> diff --git a/scipost_django/helpdesk/templates/helpdesk/_tickets_tablist.html b/scipost_django/helpdesk/templates/helpdesk/_tickets_tablist.html deleted file mode 100644 index fbda099d87f4e277143cb222f6a68bf5f3b0704d..0000000000000000000000000000000000000000 --- a/scipost_django/helpdesk/templates/helpdesk/_tickets_tablist.html +++ /dev/null @@ -1,34 +0,0 @@ -{% 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 <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 <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 <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 <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 %} diff --git a/scipost_django/helpdesk/templates/helpdesk/helpdesk.html b/scipost_django/helpdesk/templates/helpdesk/helpdesk.html index f64507016941223f276762a553c21fd7726c1dfb..88971d2fff8c3ae121e36f49f0eb569a50868af9 100644 --- a/scipost_django/helpdesk/templates/helpdesk/helpdesk.html +++ b/scipost_django/helpdesk/templates/helpdesk/helpdesk.html @@ -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> diff --git a/scipost_django/helpdesk/templates/helpdesk/queue_confirm_delete.html b/scipost_django/helpdesk/templates/helpdesk/queue_confirm_delete.html index a6cefc4259d38f00bd15fa5ed376bfca9336e9d8..2c3a849866ef8d2fe2d2e5f062d590a65377875f 100644 --- a/scipost_django/helpdesk/templates/helpdesk/queue_confirm_delete.html +++ b/scipost_django/helpdesk/templates/helpdesk/queue_confirm_delete.html @@ -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> diff --git a/scipost_django/helpdesk/templates/helpdesk/queue_detail.html b/scipost_django/helpdesk/templates/helpdesk/queue_detail.html index 00e508282111a32c6cbac466b99c88478eb6dbb9..6aecf31e708c2545f9256839f59ffc4a8b38d966 100644 --- a/scipost_django/helpdesk/templates/helpdesk/queue_detail.html +++ b/scipost_django/helpdesk/templates/helpdesk/queue_detail.html @@ -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> diff --git a/scipost_django/helpdesk/templates/helpdesk/tickets_table.html b/scipost_django/helpdesk/templates/helpdesk/tickets_table.html deleted file mode 100644 index 6f24fd48f03c6e87bc6b2e45918f915a41d4a62c..0000000000000000000000000000000000000000 --- a/scipost_django/helpdesk/templates/helpdesk/tickets_table.html +++ /dev/null @@ -1,61 +0,0 @@ -<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 }}"> </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> diff --git a/scipost_django/helpdesk/urls.py b/scipost_django/helpdesk/urls.py index df3ad968b2709a843b3c5925169cfe2ac85772c9..433e7bc097c29cd94056ef3ac8d87cf504fa14eb 100644 --- a/scipost_django/helpdesk/urls.py +++ b/scipost_django/helpdesk/urls.py @@ -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>/", diff --git a/scipost_django/helpdesk/views.py b/scipost_django/helpdesk/views.py index a6f976be7badcd3544d205e504e0e8feea1079fc..e4e4a20a6aa4503b3a460b84c43ddd0dbbdab1c6 100644 --- a/scipost_django/helpdesk/views.py +++ b/scipost_django/helpdesk/views.py @@ -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)