tickets API changes & search box function

This commit is contained in:
Corban-Lee Jones 2024-01-15 00:27:39 +00:00
parent df82417790
commit 73eeedbb36
6 changed files with 378 additions and 345 deletions

View File

@ -1,10 +1,13 @@
# -*- encoding: utf-8 -*-
from django.urls import path
from django.urls import path, include
from . import views
from .views import TicketListApiView, FilterCountApiView
urlpatterns = [
path("ticket/", TicketListApiView.as_view(), name="ticket"),
path("filter-counts/", FilterCountApiView.as_view(), name="filter-counts"),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("tickets/", views.TicketListApiView.as_view(), name="tickets"),
path("filter-counts/", views.FilterCountApiView.as_view(), name="filter-counts"),
]

View File

@ -41,7 +41,11 @@ class TicketListApiView(APIView):
permission_classes = [permissions.IsAuthenticated]
pagination_class = TicketPaginiation
ALLOWED_FILTERS = ("uuid__in", "priority__in", "tags__in", "author__department__in")
ALLOWED_FILTERS = ("uuid__in", "priority__in", "tags__in", "author__department__in", "title__contains")
MATCHER_MAP = {
"contains": lambda k, v: {k: v[0]},
"in": lambda k, v: {k: v}
}
# @method_decorator(cache_page(60 * 5))
def get(self, request: HttpRequest) -> Response:
@ -68,7 +72,6 @@ class TicketListApiView(APIView):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
def _get_tickets(self, request: HttpRequest) -> Response:
"""Returns a response containing tickets matching the given filters.
@ -92,16 +95,20 @@ class TicketListApiView(APIView):
queryset = Ticket.objects.all()
for key in request.GET.keys():
values = request.GET.getlist(key) if key.endswith("[]") else [request.GET.get(key)]
for key, values in request.GET.lists():
key = key.removesuffix("[]")
if key not in self.ALLOWED_FILTERS:
raise KeyError(key)
for value in values:
if value == "all": continue
queryset = queryset.filter(Q(**{key: [value]}))
if "all" in values:
continue
if not key.endswith("__in"):
values = values[0]
filter_kwargs = {key: values}
queryset = queryset.filter(Q(**filter_kwargs))
tickets = queryset.order_by("-create_timestamp")
serializer = TicketSerializer(tickets, many=True)

View File

@ -22,7 +22,7 @@
"fields": {
"title": "Security Patch Installation",
"description": "There's a critical security patch that needs to be installed on all workstations to address recent vulnerabilities. Attempted to deploy the patch, but facing challenges on certain machines. Need high-priority assistance to ensure all systems are promptly updated for enhanced security measures.",
"author": "291cc4c1-3bb3-4417-aaa6-748606fede77",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"create_timestamp": "2024-01-09T00:09:20Z",
"edit_timestamp": "2024-01-09T00:09:20Z",
@ -92,7 +92,7 @@
"fields": {
"title": "Hardware Malfunction",
"description": "The printer on the third floor is not responding. Checked cables and power source, but issue persists. Need assistance to fix the hardware problem.",
"author": "291cc4c1-3bb3-4417-aaa6-748606fede77",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"create_timestamp": "2024-01-09T00:06:58Z",
"edit_timestamp": "2024-01-09T00:44:49Z",
@ -126,7 +126,7 @@
"fields": {
"title": "VPN Connection Issue",
"description": "Remote team members are encountering difficulties establishing a VPN connection. This is hindering their ability to access essential resources. Providing detailed information on the error messages received and troubleshooting steps taken so far. Requesting immediate attention to restore seamless VPN functionality.",
"author": "291cc4c1-3bb3-4417-aaa6-748606fede77",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"create_timestamp": "2024-01-09T00:09:39Z",
"edit_timestamp": "2024-01-09T00:13:27Z",
@ -143,7 +143,7 @@
"fields": {
"title": "IT Training Request",
"description": "Requesting IT training sessions for the marketing team to enhance their proficiency in utilizing specific software tools. Providing a detailed outline of the desired training topics and the anticipated benefits for the team. Seeking assistance in scheduling and conducting the training sessions.",
"author": "291cc4c1-3bb3-4417-aaa6-748606fede77",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"create_timestamp": "2024-01-09T00:12:17Z",
"edit_timestamp": "2024-01-09T00:12:17Z",
@ -190,7 +190,7 @@
"fields": {
"title": "Software Update Problem",
"description": "Unable to install the latest software update on my workstation. Getting error code XYZ. Detailed steps attempted are listed in the description.",
"author": "291cc4c1-3bb3-4417-aaa6-748606fede77",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"create_timestamp": "2024-01-09T00:06:32Z",
"edit_timestamp": "2024-01-09T00:06:32Z",

View File

@ -0,0 +1,343 @@
var displayedTicketID = -1;
filters = {};
editor = null;
searchTimeout = null;
loadingTickets = false;
const formControls = [
{
id: "newTitle",
validation: function(element) {
const value = element.val();
return (!element.attr("required") || value.trim() !== "")
},
errorMessage: function(element) {
return "This field is required."
}
}
];
$(document).ready(function() {
// $(".email-list-item").on("click", function() {
// displayTicket(this);
// });
ClassicEditor
.create( document.getElementById("newDesc"), {})
.then( newEditor => {
editor = newEditor;
})
.catch( error => {
console.error(error)
});
$("#searchTickets").keyup(() => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
value = $("#searchTickets").val();
if (value === "") {
delete filters["title__contains"];
return;
}
filters["title__contains"] = value;
loadAllTickets();
}, 500);
})
setupFilter("#filterSidebar .filter-department", "author__department__in");
setupFilter("#filterSidebar .filter-tag", "tags__in")
setupFilter("#filterSidebar .filter-priority", "priority__in")
loadFilterCounts();
loadAllTickets();
});
function setupFilter(selector, key) {
$(selector).each(function () {
var input = $(this).find("input[type=checkbox], input[type=radio]");
var uuid = input.val();
input.on("change", function () {
if (input.is(":checkbox")) {
if ($(this).is(":checked")) {
filters[key] = filters[key] || [];
filters[key].push(uuid);
} else {
// filters[key].splice(filters[key].indexOf(uuid), 1);
filters[key] = filters[key].filter(id => id !== uuid);
if (filters[key].length === 0) {
delete filters[key];
}
}
}
else if (input.is(":radio") && input.is(":checked")) {
filters[key] = [uuid];
}
console.log(JSON.stringify(filters, null, 4));
loadAllTickets();
});
});
}
function validateForm() {
$("#ticketModal form").find(".form-control,.form-select").removeClass("is-valid is-invalid");
$("#ticketModal form .invalid-feedback").text("");
var valid = true;
formControls.forEach(function(control) {
var element = $("#" + control.id);
if (!control.validation(element)) {
element.addClass("is-invalid");
element.siblings(".invalid-feedback").text(control.errorMessage(element));
valid = false;
}
else {
element.addClass("is-valid");
}
});
return valid;
}
$("#ticketModal form").on("submit", function(event) {
event.preventDefault();
if (!validateForm()) {
return;
}
$.ajax({
url: URL_NewTicket,
type: "POST",
dataType: "json",
data: {
csrfmiddlewaretoken: CSRFMiddlewareToken,
title: $("#newTitle").val(),
description: editor.getData(),
author_id: CurrentUserID,
priority_id: $("#newPriority").val(),
tag_ids: $("#newTags").val()
},
success: function(data) {
loadAllTickets();
loadFilterCounts();
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
});
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return day + 'th';
} else {
switch (day % 10) {
case 1: return day + 'st';
case 2: return day + 'nd';
case 3: return day + 'rd';
default: return day + 'th';
}
}
}
function updateFilterCounts(filterType, data) {
$("#filterSidebar .filter-" + filterType).each(function() {
var uuid = $(this).find("input[type=checkbox],input[type=radio]").val();
var count = data[filterType + '_counts'][uuid];
$(this).find(".badge").text(count);
});
}
function loadFilterCounts() {
$.ajax({
url: URL_FilterCounts,
type: "GET",
success: function(data) {
console.log(JSON.stringify(data, null, 4));
updateFilterCounts('priority', data);
updateFilterCounts('tag', data);
updateFilterCounts('department', data);
$("#filterPriorityAll .badge").text(data.ticket_count);
$("#ticketCount").text(data.ticket_count)
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
}
function loadAllTickets() {
if (loadingTickets === true) {
return;
}
$("#ticketsContainer").empty();
loadingTickets = true;
// alert(JSON.stringify(filters, null, 4));
$.ajax({
url: URL_Tickets,
type: "GET",
dataType: "json",
data: filters,
success: function(data) {
loadingTickets = false;
console.log(JSON.stringify(data, null, 4))
data.forEach(function(ticket) {
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var day = getOrdinalSuffix(timestamp.getDate());
var month = timestamp.toLocaleString('en-GB', { month: 'short' });
var year = timestamp.toLocaleString('en-GB', { year: 'numeric' });
var time = timestamp.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric' });
// Formatting the final result
var formattedTime = time + ', ' + day + ' ' + month + ' ' + year;
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
var item = $(`
<div class="email-list-item peers fxw-nw p-20 bdB bgcH-grey-100 cur-p" data-ticket-id="${ticket.uuid}" data-author-icon="${ticket.author.icon}">
<div class="peer mR-10">
<img src="${ticket.author.icon}" alt="" class="w-2r h-2r bdrs-50p me-2" style="object-fit: cover;">
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<h6 class="ticket-author">${ticket.author.forename} ${ticket.author.surname}</h6>
</div>
<div class="peer">
<small class="ticket-timestamp">${formattedTime}</small>
</div>
</div>
<h5 class="fsz-def tt-c c-grey-900 ticket-title">${ticket.title}</h5>
<span class="whs-nw w-100 ov-h tov-e d-b ticket-desc">${ticket.description}</span>
</div>
</div>
`);
$("#ticketsContainer").append(item);
});
$(".email-list-item").on("click", function(e) {
e.preventDefault();
displayTicket(this);
$('.email-content').toggleClass('open');
});
},
error: function(data) {
loadingTickets = false;
alert(JSON.stringify(data, null, 4));
console.error(`${data.responseJSON.error}\n${data.responseJSON.detail}`);
}
});
}
function displayTicket(ticketElement) {
ticket = $(ticketElement);
ticketID = ticket.data("ticket-id");
// $(".back-to-mailbox").off("click").on("click", function(event) {
// event.preventDefault();
// $('.email-content').toggleClass('open');
// displayTicket(ticketElement);
// });
if (displayedTicketID === ticketID) {
// displayedTicketID = -1;
return;
}
ticket.siblings().removeClass("bgc-grey-100");
ticket.addClass("bgc-grey-100");
$("#ticketTitle").text("")
$("#ticketDesc").empty();
$("#ticketAuthor").text("");
$("#ticketAuthorImg").hide();
$("#ticketAuthorImg").prop("src", "");
$("#ticketTimestamp").text("");
$("#btnGroupDrop2").hide();
$("#ticketBadges").empty().hide();
displayedTicketID = ticketID;
$.ajax({
url: URL_Tickets,
type: 'get',
dataType: 'json',
data: {
uuid__in: [ticketID]
},
success: function (data) {
console.log(JSON.stringify(data, null, 4));
var ticket = data[0];
var author = ticket.author;
var department = author.department;
var priority = ticket.priority;
$("#ticketTitle").text(ticket.title);
$("#ticketDesc").append($(`<div class="w-100">${ticket.description}</div>`));
$("#ticketAuthor").text(`${author.forename} ${author.surname}`);
$("#ticketAuthorImg").show();
$("#ticketAuthorImg").prop("src", author.icon);
$("#btnGroupDrop2").show();
$("#ticketBadges").show();
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${priority.colour}; background-color: ${priority.backgroundcolour};">${priority.title} Priority <i class="ti-control-record "></i></div>`));
if (department != null) {
$("#ticketBadges").append($(`<div class="badge bgc-deep-purple-500 me-1">${department.title}</div>`));
}
ticket.tags.forEach(function(tag) {
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${tag.colour}; background-color: ${tag.backgroundcolour};">${tag.title} <i class="ti-tag"></i></div>`));
});
// timestamp
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var options = { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' };
formattedTime = timestamp.toLocaleDateString('en-GB', options);
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
$("#ticketTimestamp").text(formattedTime);
},
error: function(message) {
alert(JSON.stringify(message, null, 4));
}
});
}

View File

@ -151,7 +151,7 @@
</div>
<div class="layer w-100">
<div class="bdT bdB">
<input type="text" class="form-control m-0 bdw-0 pY-15 pX-20 bdrs-0" placeholder="Search...">
<input type="text" id="searchTickets" class="form-control m-0 bdw-0 pY-15 pX-20 bdrs-0" placeholder="Search...">
</div>
</div>
@ -296,333 +296,14 @@
{% endblock content %}
<!-- Specific Page JS goes HERE -->
{% block javascripts %}
<!-- Define Variables -->
<script>
var displayedTicketID = -1;
filters = {};
editor = null;
const formControls = [
{
id: "newTitle",
validation: function(element) {
const value = element.val();
return (!element.attr("required") || value.trim() !== "")
},
errorMessage: function(element) {
return "This field is required."
}
}
];
$(document).ready(function() {
// $(".email-list-item").on("click", function() {
// displayTicket(this);
// });
ClassicEditor
.create( document.getElementById("newDesc"), {})
.then( newEditor => {
editor = newEditor;
})
.catch( error => {
console.error(error)
});
// $("#filterPriorityAll").click(function() {
// delete filters[".filter-priority"];
// loadAllTickets();
// });
setupFilter("#filterSidebar .filter-department", "author__department__in");
setupFilter("#filterSidebar .filter-tag", "tags__in")
setupFilter("#filterSidebar .filter-priority", "priority__in")
loadFilterCounts();
loadAllTickets();
});
function setupFilter(selector, key) {
$(selector).each(function () {
var input = $(this).find("input[type=checkbox], input[type=radio]");
var uuid = input.val();
input.on("change", function () {
if (input.is(":checkbox")) {
if ($(this).is(":checked")) {
filters[key] = filters[key] || [];
filters[key].push(uuid);
} else {
// filters[key].splice(filters[key].indexOf(uuid), 1);
filters[key] = filters[key].filter(id => id !== uuid);
if (filters[key].length === 0) {
delete filters[key];
}
}
}
else if (input.is(":radio") && input.is(":checked")) {
filters[key] = [uuid];
}
console.log(JSON.stringify(filters, null, 4));
loadAllTickets();
});
});
}
function validateForm() {
$("#ticketModal form").find(".form-control,.form-select").removeClass("is-valid is-invalid");
$("#ticketModal form .invalid-feedback").text("");
var valid = true;
formControls.forEach(function(control) {
var element = $("#" + control.id);
if (!control.validation(element)) {
element.addClass("is-invalid");
element.siblings(".invalid-feedback").text(control.errorMessage(element));
valid = false;
}
else {
element.addClass("is-valid");
}
});
return valid;
}
$("#ticketModal form").on("submit", function(event) {
event.preventDefault();
if (!validateForm()) {
return;
}
$.ajax({
url: "{% url 'ticket-new' %}",
type: "POST",
dataType: "json",
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
title: $("#newTitle").val(),
description: editor.getData(),
author_id: "{{ request.user.uuid }}",
priority_id: $("#newPriority").val(),
tag_ids: $("#newTags").val()
},
success: function(data) {
loadAllTickets();
loadFilterCounts();
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
});
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return day + 'th';
} else {
switch (day % 10) {
case 1: return day + 'st';
case 2: return day + 'nd';
case 3: return day + 'rd';
default: return day + 'th';
}
}
}
function updateFilterCounts(filterType, data) {
$("#filterSidebar .filter-" + filterType).each(function() {
var uuid = $(this).find("input[type=checkbox],input[type=radio]").val();
var count = data[filterType + '_counts'][uuid];
$(this).find(".badge").text(count);
});
}
function loadFilterCounts() {
$.ajax({
url: "{% url 'api:filter-counts' %}",
type: "GET",
success: function(data) {
console.log(JSON.stringify(data, null, 4));
updateFilterCounts('priority', data);
updateFilterCounts('tag', data);
updateFilterCounts('department', data);
$("#filterPriorityAll .badge").text(data.ticket_count);
$("#ticketCount").text(data.ticket_count)
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
}
function loadAllTickets() {
$("#ticketsContainer").empty();
// alert(JSON.stringify(filters, null, 4));
$.ajax({
url: "{% url 'api:ticket' %}",
type: "GET",
dataType: "json",
data: filters,
success: function(data) {
console.log(JSON.stringify(data, null, 4))
data.forEach(function(ticket) {
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var day = getOrdinalSuffix(timestamp.getDate());
var month = timestamp.toLocaleString('en-GB', { month: 'short' });
var year = timestamp.toLocaleString('en-GB', { year: 'numeric' });
var time = timestamp.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric' });
// Formatting the final result
var formattedTime = time + ', ' + day + ' ' + month + ' ' + year;
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
var item = $(`
<div class="email-list-item peers fxw-nw p-20 bdB bgcH-grey-100 cur-p" data-ticket-id="${ticket.uuid}" data-author-icon="${ticket.author.icon}">
<div class="peer mR-10">
<img src="${ticket.author.icon}" alt="" class="w-2r h-2r bdrs-50p me-2" style="object-fit: cover;">
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<h6 class="ticket-author">${ticket.author.forename} ${ticket.author.surname}</h6>
</div>
<div class="peer">
<small class="ticket-timestamp">${formattedTime}</small>
</div>
</div>
<h5 class="fsz-def tt-c c-grey-900 ticket-title">${ticket.title}</h5>
<span class="whs-nw w-100 ov-h tov-e d-b ticket-desc">${ticket.description}</span>
</div>
</div>
`);
$("#ticketsContainer").append(item);
});
$(".email-list-item").on("click", function(e) {
e.preventDefault();
displayTicket(this);
$('.email-content').toggleClass('open');
});
},
error: function(data) {
alert(JSON.stringify(data, null, 4));
console.error(`${data.responseJSON.error}\n${data.responseJSON.detail}`);
}
});
}
function displayTicket(ticketElement) {
ticket = $(ticketElement);
ticketID = ticket.data("ticket-id");
// $(".back-to-mailbox").off("click").on("click", function(event) {
// event.preventDefault();
// $('.email-content').toggleClass('open');
// displayTicket(ticketElement);
// });
if (displayedTicketID === ticketID) {
// displayedTicketID = -1;
return;
}
ticket.siblings().removeClass("bgc-grey-100");
ticket.addClass("bgc-grey-100");
$("#ticketTitle").text("")
$("#ticketDesc").empty();
$("#ticketAuthor").text("");
$("#ticketAuthorImg").hide();
$("#ticketAuthorImg").prop("src", "");
$("#ticketTimestamp").text("");
$("#btnGroupDrop2").hide();
$("#ticketBadges").empty().hide();
displayedTicketID = ticketID;
$.ajax({
url: `{% url 'api:ticket' %}`,
type: 'get',
dataType: 'json',
data: {
uuid__in: [ticketID]
},
success: function (data) {
console.log(JSON.stringify(data, null, 4));
var ticket = data[0];
var author = ticket.author;
var department = author.department;
var priority = ticket.priority;
$("#ticketTitle").text(ticket.title);
$("#ticketDesc").append($(`<div class="w-100">${ticket.description}</div>`));
$("#ticketAuthor").text(`${author.forename} ${author.surname}`);
$("#ticketAuthorImg").show();
$("#ticketAuthorImg").prop("src", author.icon);
$("#btnGroupDrop2").show();
$("#ticketBadges").show();
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${priority.colour}; background-color: ${priority.backgroundcolour};">${priority.title} Priority <i class="ti-control-record "></i></div>`));
if (department != null) {
$("#ticketBadges").append($(`<div class="badge bgc-deep-purple-500 me-1">${department.title}</div>`));
}
ticket.tags.forEach(function(tag) {
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${tag.colour}; background-color: ${tag.backgroundcolour};">${tag.title} <i class="ti-tag"></i></div>`));
});
// timestamp
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var options = { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' };
formattedTime = timestamp.toLocaleDateString('en-GB', options);
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
$("#ticketTimestamp").text(formattedTime);
},
error: function(message) {
alert(JSON.stringify(message, null, 4));
}
});
}
const URL_Tickets = "{% url 'api:tickets' %}";
const URL_NewTicket = "{% url 'ticket-new' %}";
const URL_FilterCounts = "{% url 'api:filter-counts' %}";
const CSRFMiddlewareToken = "{{ csrf_token }}";
const CurrentUserID = "{{ request.user.uuid }}";
</script>
<script src="{{ ASSETS_ROOT }}/js/tickets.js"></script>
{% endblock javascripts %}

View File

@ -6,8 +6,7 @@ from django.conf.urls.static import static
from django.urls import path, include # add this
urlpatterns = [
path('admin/', admin.site.urls), # Django admin route
path("api-auth/", include("rest_framework.urls")),
path('admin/', admin.site.urls),
path("api/", include(("apps.api.urls", "apps.api"), namespace="api")),
# ADD NEW Routes HERE