Merge pull request #1245 from DavidVadnais/ENHANCEMENT-sort-tickets-by-last-followup

Enhancement sort tickets by last followup
This commit is contained in:
Christopher Broderick 2025-03-22 21:53:36 +00:00 committed by GitHub
commit f5c679f6df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 77 additions and 21 deletions

View File

@ -8,8 +8,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
django-version: ["32","4"]
include:
# Explicitly include Python 3.8 and 3.9 only with Django 4
- python-version: "3.8"
django-version: "4"
- python-version: "3.9"
django-version: "4"
# Define the general matrix for Python with Django
python-version: ["3.10", "3.11", "3.12"]
django-version: ["4"]
steps:
- uses: actions/checkout@v4

1
constraints-Django5.txt Normal file
View File

@ -0,0 +1 @@
Django >=5,<6

View File

@ -1,6 +1,8 @@
from base64 import b64decode, b64encode
from django.db.models import Q
from django.db.models import Q, Max
from django.db.models import F, Window, Subquery, OuterRef
from .models import FollowUp
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext as _
@ -65,8 +67,9 @@ DATATABLES_ORDER_COLUMN_CHOICES = Choices(
('6', 'due_date'),
('7', 'assigned_to'),
('8', 'submitter_email'),
# ('9', 'time_spent'),
('10', 'kbitem'),
('9', 'last_followup'),
# ('10', 'time_spent'),
('11', 'kbitem'),
)
@ -175,7 +178,20 @@ class __Query__:
if order == 'desc':
order_column = '-' + order_column
queryset = objects.all().order_by(order_by)
queryset = objects.annotate(
last_followup=Subquery(
FollowUp.objects.order_by().annotate(
last_followup=Window(
expression=Max("date"),
partition_by=[F("ticket_id"),],
order_by="-date"
)
).filter(
ticket_id=OuterRef("id")
).values("last_followup").distinct()
)
).order_by(order_by)
total = queryset.count()
if search_value: # Dead code currently

View File

@ -18,6 +18,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
ticket = serializers.SerializerMethodField()
assigned_to = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField()
last_followup = serializers.SerializerMethodField()
created = serializers.SerializerMethodField()
due_date = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
@ -30,8 +31,8 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
model = Ticket
# fields = '__all__'
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
'created', 'due_date', 'assigned_to', 'submitter', 'row_class',
'time_spent', 'kbitem')
'created', 'due_date', 'assigned_to', 'submitter', 'last_followup',
'row_class', 'time_spent', 'kbitem')
def get_queue(self, obj):
return {"title": obj.queue.title, "id": obj.queue.id}
@ -71,6 +72,9 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
def get_kbitem(self, obj):
return obj.kbitem.title if obj.kbitem else ""
def get_last_followup(self, obj):
return obj.last_followup
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
class Meta:

View File

@ -27,11 +27,11 @@
<tbody>
<tr>
<td>{% trans "Average number of days until ticket is closed (all tickets): " %}</td>
<td><strong style="color: red;">{{ basic_ticket_stats.average_nbr_days_until_ticket_closed }}</strong>.</td>
<td><strong style="color: red;">{{ basic_ticket_stats.average_nbr_days_until_ticket_closed|floatformat:2 }}</strong>.</td>
</tr>
<tr>
<td>{% trans "Average number of days until ticket is closed (tickets opened in last 60 days): " %}</td>
<td><strong style="color: red;">{{ basic_ticket_stats.average_nbr_days_until_ticket_closed_last_60_days }}</strong>. {% trans "Click" %} <strong><a href="{% url 'helpdesk:report_index' %}daysuntilticketclosedbymonth">here</a></strong> {% trans "for detailed average by month." %} </td>
<td><strong style="color: red;">{{ basic_ticket_stats.average_nbr_days_until_ticket_closed_last_60_days|floatformat:2 }}</strong>. {% trans "Click" %} <strong><a href="{% url 'helpdesk:report_index' %}daysuntilticketclosedbymonth">here</a></strong> {% trans "for detailed average by month." %} </td>
</tr>
</tbody>
</table>

View File

@ -76,6 +76,7 @@
<th>{% trans "Due Date" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Submitter" %}</th>
<th>{% trans "Last Followup" %}</th>
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}<th>{% trans "Time Spent" %}</th>{% endif %}
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}<th>{% trans "KB item" %}</th>{% endif %}
</tr>
@ -330,7 +331,13 @@
{% else %}
<script src="{% static 'helpdesk/vendor/timeline3/js/timeline.js' %}"></script>
{% endif %}
<script>
window.helpdesk_settings = {
LANGUAGE_CODE: "{{ helpdesk_settings.LANGUAGE_CODE|default:'en-US' }}",
NO_FOLLOWUP_TEXT: "{% trans helpdesk_settings.ALTERNATIVE_UI_STRINGS.No_followup_found|default:'No followup found' %}"
};
function get_url(row) {
return "{% url 'helpdesk:view' 1234 %}".replace(/1234/, row.id.toString());
}
@ -415,6 +422,27 @@
}
},
{data: "submitter"},
{
data: "last_followup",
render: function (data, type, row) {
let locale = navigator.language || navigator.userLanguage || window.helpdesk_settings.LANGUAGE_CODE;
if (isNaN(Date.parse(data))) {
return window.helpdesk_settings.NO_FOLLOWUP_TEXT;
}
let date = new Date(data);
return date.toLocaleString(locale, {
weekday: "short",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: true
});
}
},
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
{data: "time_spent", "visible": false},
{% endif %}

View File

@ -280,7 +280,7 @@ class GetEmailCommonTests(TestCase):
self.assertTrue(att_retrieved.filename.endswith(att_filename), "Filename of attached multipart not detected: %s" % (att_retrieved.filename))
with att_retrieved.file.open('r') as f:
retrieved_content = f.read()
self.assertEquals(att_content, retrieved_content, "Retrieved attachment content different to original :\n\n%s\n\n%s" % (att_content, retrieved_content))
self.assertEqual(att_content, retrieved_content, "Retrieved attachment content different to original :\n\n%s\n\n%s" % (att_content, retrieved_content))
def test_email_with_inline_and_multipart_as_attachments(self):
"""

View File

@ -64,8 +64,8 @@ class QueryTests(TestCase):
resp_json,
{
"data":
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": ""},
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""},
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 2,
"recordsTotal": 2,
"draw": 0,
@ -85,7 +85,7 @@ class QueryTests(TestCase):
{
"data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,
@ -105,7 +105,7 @@ class QueryTests(TestCase):
{
"data":
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": ""}],
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,

View File

@ -1068,7 +1068,7 @@ def ticket_list(request):
# SORTING
sort = request.GET.get('sort', None)
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority', 'kbitem'):
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority', 'last_followup', 'kbitem'):
sort = 'created'
query_params['sorting'] = sort