mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-06-20 17:47:58 +02:00
Merge pull request #1245 from DavidVadnais/ENHANCEMENT-sort-tickets-by-last-followup
Enhancement sort tickets by last followup
This commit is contained in:
commit
f5c679f6df
11
.github/workflows/pythonpackage.yml
vendored
11
.github/workflows/pythonpackage.yml
vendored
@ -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
1
constraints-Django5.txt
Normal file
@ -0,0 +1 @@
|
||||
Django >=5,<6
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user