Implement My Tickets view in public helpdesk

Note: This is a breaking change as it forces pagination on the API endoints.
This should have been done from the start as the API without pagination is
useless when there are large numbers of tickets.
This commit is contained in:
Timothy Hobbs 2023-11-23 21:50:44 +01:00
parent cec90aafdd
commit b92c83de39
6 changed files with 165 additions and 12 deletions

View File

@ -70,6 +70,46 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
return obj.kbitem.title if obj.kbitem else "" return obj.kbitem.title if obj.kbitem else ""
class PublicTicketListingSerializer(serializers.ModelSerializer):
"""
A serializer to be used by the public API for listing tickets. Don't expose private fields here!
"""
ticket = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField()
created = serializers.SerializerMethodField()
due_date = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
queue = serializers.SerializerMethodField()
kbitem = serializers.SerializerMethodField()
class Meta:
model = Ticket
# fields = '__all__'
fields = ('ticket', 'id', 'title', 'queue', 'status',
'created', 'due_date', 'submitter', 'kbitem')
def get_queue(self, obj):
return {"title": obj.queue.title, "id": obj.queue.id}
def get_ticket(self, obj):
return str(obj.id) + " " + obj.ticket
def get_status(self, obj):
return obj.get_status
def get_created(self, obj):
return humanize.naturaltime(obj.created)
def get_due_date(self, obj):
return humanize.naturaltime(obj.due_date)
def get_submitter(self, obj):
return obj.submitter_email
def get_kbitem(self, obj):
return obj.kbitem.title if obj.kbitem else ""
class FollowUpAttachmentSerializer(serializers.ModelSerializer): class FollowUpAttachmentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FollowUpAttachment model = FollowUpAttachment

View File

@ -0,0 +1,69 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %}
{% block helpdesk_body %}
<h2>{% trans "My Tickets" %}</h2>
<div class="container mt-4">
<table class="table table-striped" id="ticketsTable">
<thead>
<tr>
<th>Title</th>
<th>Queue</th>
<th>Status</th>
<th>Created</th>
<th>Due Date</th>
<th>Submitter</th>
</tr>
</thead>
<tbody>
<!-- Rows will be added here dynamically using jQuery -->
</tbody>
</table>
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<!-- Pagination buttons will be added here dynamically -->
</ul>
</nav>
</div>
<script>
// don't use jquery's document ready but rather the more basic window load
// because we need to wait for the page to load before we can fetch the tickets
window.addEventListener('load', function()
{
function fetchTickets(page = 1) {
const endpoint = '{% url "helpdesk:user_tickets-list" %}?page=' + page;
$.get(endpoint, function(data) {
$('#ticketsTable tbody').empty();
data.results.forEach(function(ticket) {
$('#ticketsTable tbody').append(`
<tr>
<td><a href="/view/?ticket=${ticket.id}&email=${ticket.submitter}">${ticket.title}</a></td>
<td>${ticket.queue.title}</td>
<td>${ticket.status}</td>
<td>${ticket.created}</td>
<td>${ticket.due_date ? ticket.due_date : 'N/A'}</td>
<td>${ticket.submitter}</td>
</tr>
`);
});
$('#pagination').empty();
for (let i = 1; i <= data.total_pages; i++) {
$('#pagination').append(`
<li class="page-item ${i === data.page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>
`);
}
});
}
fetchTickets();
});
</script>
{% endblock %}

View File

@ -68,6 +68,15 @@
<span>{% trans "New Ticket" %}</span> <span>{% trans "New Ticket" %}</span>
</a> </a>
</li> </li>
{% if user.is_authenticated %}
<li class="nav-item{% if 'my-tickets' in request.path %} active{% endif %}">
<a class="nav-link" href="{% url 'helpdesk:my-tickets' %}">
<i class="fas fa-fw fa-tasks"></i>
<span>{% trans "My Tickets" %}</span>
</a>
</li>
{% endif %}
{% if helpdesk_settings.HELPDESK_KB_ENABLED %} {% if helpdesk_settings.HELPDESK_KB_ENABLED %}
<li class="nav-item{% if 'kb' in request.path %} active{% endif %}"> <li class="nav-item{% if 'kb' in request.path %} active{% endif %}">
<a class="nav-link" href="{% url 'helpdesk:kb_index' %}"> <a class="nav-link" href="{% url 'helpdesk:kb_index' %}">

View File

@ -14,7 +14,7 @@ from django.views.generic import TemplateView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import helpdesk_staff_member_required, protect_view from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, login, public, staff from helpdesk.views import feeds, login, public, staff
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet, UserTicketViewSet
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
@ -154,6 +154,7 @@ if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
urlpatterns += [ urlpatterns += [
path("", protect_view(public.Homepage.as_view()), name="home"), path("", protect_view(public.Homepage.as_view()), name="home"),
path("tickets/my-tickets/", public.MyTickets.as_view(), name="my-tickets"),
path("tickets/submit/", public.create_ticket, name="submit"), path("tickets/submit/", public.create_ticket, name="submit"),
path( path(
"tickets/submit_iframe/", "tickets/submit_iframe/",
@ -199,10 +200,9 @@ urlpatterns += [
] ]
# API is added to url conf based on the setting (False by default)
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
router = DefaultRouter() router = DefaultRouter()
router.register(r"tickets", TicketViewSet, basename="ticket") router.register(r"tickets", TicketViewSet, basename="ticket")
router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets")
router.register(r"followups", FollowUpViewSet, basename="followups") router.register(r"followups", FollowUpViewSet, basename="followups")
router.register(r"followups-attachments", router.register(r"followups-attachments",
FollowUpAttachmentViewSet, basename="followupattachments") FollowUpAttachmentViewSet, basename="followupattachments")

View File

@ -1,10 +1,29 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer, PublicTicketListingSerializer
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.mixins import CreateModelMixin from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from rest_framework.pagination import PageNumberPagination
class ConservativePagination(PageNumberPagination):
page_size = 25
page_size_query_param = 'page_size'
class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
"""
A list of all the tickets submitted by the current user
The view is paginated by default
"""
serializer_class = PublicTicketListingSerializer
pagination_class = ConservativePagination
def get_queryset(self):
return Ticket.objects.filter(submitter_email=self.request.user.email).order_by('-created')
class TicketViewSet(viewsets.ModelViewSet): class TicketViewSet(viewsets.ModelViewSet):
@ -13,6 +32,7 @@ class TicketViewSet(viewsets.ModelViewSet):
""" """
queryset = Ticket.objects.all() queryset = Ticket.objects.all()
serializer_class = TicketSerializer serializer_class = TicketSerializer
pagination_class = ConservativePagination
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]
def get_queryset(self): def get_queryset(self):
@ -30,12 +50,14 @@ class TicketViewSet(viewsets.ModelViewSet):
class FollowUpViewSet(viewsets.ModelViewSet): class FollowUpViewSet(viewsets.ModelViewSet):
queryset = FollowUp.objects.all() queryset = FollowUp.objects.all()
serializer_class = FollowUpSerializer serializer_class = FollowUpSerializer
pagination_class = ConservativePagination
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]
class FollowUpAttachmentViewSet(viewsets.ModelViewSet): class FollowUpAttachmentViewSet(viewsets.ModelViewSet):
queryset = FollowUpAttachment.objects.all() queryset = FollowUpAttachment.objects.all()
serializer_class = FollowUpAttachmentSerializer serializer_class = FollowUpAttachmentSerializer
pagination_class = ConservativePagination
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]

View File

@ -208,12 +208,14 @@ class ViewTicket(TemplateView):
try: try:
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
if request.user.is_authenticated and request.user.email == email:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
else: else:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
except (ObjectDoesNotExist, ValueError): except (ObjectDoesNotExist, ValueError):
return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.')) return SearchForTicketView.as_view()(request, _('Invalid ticket ID or e-mail address. Please try again.'))
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
from helpdesk.update_ticket import update_ticket from helpdesk.update_ticket import update_ticket
@ -247,6 +249,17 @@ class ViewTicket(TemplateView):
return redirect_url return redirect_url
class MyTickets(TemplateView):
template_name = 'helpdesk/my_tickets.html'
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse('helpdesk:login'))
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def change_language(request): def change_language(request):
return_to = '' return_to = ''
if 'return_to' in request.GET: if 'return_to' in request.GET: