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 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"] include:
django-version: ["32","4"] # 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: steps:
- uses: actions/checkout@v4 - 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 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.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -65,8 +67,9 @@ DATATABLES_ORDER_COLUMN_CHOICES = Choices(
('6', 'due_date'), ('6', 'due_date'),
('7', 'assigned_to'), ('7', 'assigned_to'),
('8', 'submitter_email'), ('8', 'submitter_email'),
# ('9', 'time_spent'), ('9', 'last_followup'),
('10', 'kbitem'), # ('10', 'time_spent'),
('11', 'kbitem'),
) )
@ -175,7 +178,20 @@ class __Query__:
if order == 'desc': if order == 'desc':
order_column = '-' + order_column 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() total = queryset.count()
if search_value: # Dead code currently if search_value: # Dead code currently

View File

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

View File

@ -27,11 +27,11 @@
<tbody> <tbody>
<tr> <tr>
<td>{% trans "Average number of days until ticket is closed (all tickets): " %}</td> <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>
<tr> <tr>
<td>{% trans "Average number of days until ticket is closed (tickets opened in last 60 days): " %}</td> <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> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -76,6 +76,7 @@
<th>{% trans "Due Date" %}</th> <th>{% trans "Due Date" %}</th>
<th>{% trans "Owner" %}</th> <th>{% trans "Owner" %}</th>
<th>{% trans "Submitter" %}</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_ENABLE_TIME_SPENT_ON_TICKET %}<th>{% trans "Time Spent" %}</th>{% endif %}
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}<th>{% trans "KB item" %}</th>{% endif %} {% if helpdesk_settings.HELPDESK_KB_ENABLED %}<th>{% trans "KB item" %}</th>{% endif %}
</tr> </tr>
@ -330,7 +331,13 @@
{% else %} {% else %}
<script src="{% static 'helpdesk/vendor/timeline3/js/timeline.js' %}"></script> <script src="{% static 'helpdesk/vendor/timeline3/js/timeline.js' %}"></script>
{% endif %} {% endif %}
<script> <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) { function get_url(row) {
return "{% url 'helpdesk:view' 1234 %}".replace(/1234/, row.id.toString()); return "{% url 'helpdesk:view' 1234 %}".replace(/1234/, row.id.toString());
} }
@ -415,6 +422,27 @@
} }
}, },
{data: "submitter"}, {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 %} {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
{data: "time_spent", "visible": false}, {data: "time_spent", "visible": false},
{% endif %} {% 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)) 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: with att_retrieved.file.open('r') as f:
retrieved_content = f.read() 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): def test_email_with_inline_and_multipart_as_attachments(self):
""" """

View File

@ -64,8 +64,8 @@ class QueryTests(TestCase):
resp_json, resp_json,
{ {
"data": "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": "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, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], {"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, "recordsFiltered": 2,
"recordsTotal": 2, "recordsTotal": 2,
"draw": 0, "draw": 0,
@ -85,7 +85,7 @@ class QueryTests(TestCase):
{ {
"data": "data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", [{"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, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,
@ -105,7 +105,7 @@ class QueryTests(TestCase):
{ {
"data": "data":
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", [{"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, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,

View File

@ -1068,7 +1068,7 @@ def ticket_list(request):
# SORTING # SORTING
sort = request.GET.get('sort', None) 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' sort = 'created'
query_params['sorting'] = sort query_params['sorting'] = sort