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
13
.github/workflows/pythonpackage.yml
vendored
13
.github/workflows/pythonpackage.yml
vendored
@ -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
|
||||||
|
|
||||||
@ -21,7 +28,7 @@ jobs:
|
|||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }}-${{ matrix.django-version }}
|
key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }}-${{ matrix.django-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
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 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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -169,13 +172,26 @@ class __Query__:
|
|||||||
search_value = kwargs.get('search[value]', [""])[0]
|
search_value = kwargs.get('search[value]', [""])[0]
|
||||||
order_column = kwargs.get('order[0][column]', ['5'])[0]
|
order_column = kwargs.get('order[0][column]', ['5'])[0]
|
||||||
order = kwargs.get('order[0][dir]', ["asc"])[0]
|
order = kwargs.get('order[0][dir]', ["asc"])[0]
|
||||||
|
|
||||||
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||||
# django orm '-' -> desc
|
# django orm '-' -> desc
|
||||||
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
|
||||||
|
@ -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}
|
||||||
@ -70,8 +71,11 @@ 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:
|
||||||
model = FollowUpAttachment
|
model = FollowUpAttachment
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user