Merge pull request #1167 from samsplunks/followup_queue_change

Allow to track queue change in follow-ups
This commit is contained in:
Benbb96 2024-04-12 15:07:26 +02:00 committed by GitHub
commit 9f7c18e507
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 178 additions and 22 deletions

View File

@ -1007,34 +1007,59 @@ class FollowUp(models.Model):
def time_spent_calculation(self): def time_spent_calculation(self):
"Returns timedelta according to rules settings." "Returns timedelta according to rules settings."
# extract earliest from previous follow-up or ticket open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
try: holidays = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS
exclude_statuses = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES
exclude_queues = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES
# queryset for this ticket previous follow-ups
prev_fup_qs = self.ticket.followup_set.all() prev_fup_qs = self.ticket.followup_set.all()
if self.id: if self.id:
prev_fup_qs = prev_fup_qs.filter(id__lt=self.id) # if the follow-up exist in DB, only keep previous follow-ups
prev_fup = prev_fup_qs.latest("date") prev_fup_qs = prev_fup_qs.filter(date__lt=self.date)
earliest = prev_fup.date
except ObjectDoesNotExist:
earliest = self.ticket.created
# extract previous status from follow-up or ticket # handle exclusions
# extract previous status from follow-up or ticket for exclusion check
if exclude_statuses:
try: try:
prev_fup_qs = self.ticket.followup_set.exclude(new_status__isnull=True)
if self.id:
prev_fup_qs = prev_fup_qs.filter(id__lt=self.id)
prev_fup = prev_fup_qs.latest("date") prev_fup = prev_fup_qs.latest("date")
prev_status = prev_fup.new_status prev_status = prev_fup.new_status
except ObjectDoesNotExist: except ObjectDoesNotExist:
prev_status = self.ticket.status prev_status = self.ticket.status
# latest time is current follow-up date # don't calculate status exclusions
latest = self.date if prev_status in exclude_statuses:
return datetime.timedelta(seconds=0)
# find the previous queue for exclusion check
if exclude_queues:
try:
prev_fup_ids = prev_fup_qs.values_list('id', flat=True)
prev_queue_change = TicketChange.objects.filter(followup_id__in=prev_fup_ids,
field=_('Queue')).latest('id')
prev_queue = Queue.objects.get(pk=prev_queue_change.new_value)
prev_queue_slug = prev_queue.slug
except ObjectDoesNotExist:
prev_queue_slug = self.ticket.queue.slug
# don't calculate queue exclusions
if prev_queue_slug in exclude_queues:
return datetime.timedelta(seconds=0)
# no exclusion found
time_spent_seconds = 0 time_spent_seconds = 0
open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
holidays = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS # extract earliest from previous follow-up or ticket
exclude_statuses = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES try:
exclude_queues = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES prev_fup = prev_fup_qs.latest("date")
earliest = prev_fup.date
except ObjectDoesNotExist:
earliest = self.ticket.created
# latest time is current follow-up date
latest = self.date
# split time interval by days # split time interval by days
days = latest.toordinal() - earliest.toordinal() days = latest.toordinal() - earliest.toordinal()
@ -1054,9 +1079,7 @@ class FollowUp(models.Model):
start_day_time = middle_day_time.replace(hour=0, minute=0, second=0) start_day_time = middle_day_time.replace(hour=0, minute=0, second=0)
end_day_time = middle_day_time.replace(hour=23, minute=59, second=59, microsecond=999999) end_day_time = middle_day_time.replace(hour=23, minute=59, second=59, microsecond=999999)
if (start_day_time.strftime("%Y-%m-%d") not in holidays and if start_day_time.strftime("%Y-%m-%d") not in holidays:
prev_status not in exclude_statuses and
self.ticket.queue.slug not in exclude_queues):
time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours) time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours)
return datetime.timedelta(seconds=time_spent_seconds) return datetime.timedelta(seconds=time_spent_seconds)

View File

@ -150,6 +150,9 @@
<dt><label for='id_priority'>{% trans "Priority" %}</label></dt> <dt><label for='id_priority'>{% trans "Priority" %}</label></dt>
<dd><select id='id_priority' name='priority'>{% for p in priorities %}{% if p.0 == ticket.priority %}<option value='{{ p.0 }}' selected='selected'>{{ p.1 }}</option>{% else %}<option value='{{ p.0 }}'>{{ p.1 }}</option>{% endif %}{% endfor %}</select></dd> <dd><select id='id_priority' name='priority'>{% for p in priorities %}{% if p.0 == ticket.priority %}<option value='{{ p.0 }}' selected='selected'>{{ p.1 }}</option>{% else %}<option value='{{ p.0 }}'>{{ p.1 }}</option>{% endif %}{% endfor %}</select></dd>
<dt><label for='id_queue'>{% trans "Queue" %}</label></dt>
<dd><select id='id_queue' name='queue'>{% for queue_id, queue_name in queues %}<option value='{{ queue_id }}'{% if queue_id == ticket.queue.id %} selected{% endif %}>{{ queue_name }}</option>{% endfor %}</select></dd>
<dt><label for='id_due_date'>{% trans "Due on" %}</label></dt> <dt><label for='id_due_date'>{% trans "Due on" %}</label></dt>
<dd>{{ form.due_date }}</dd> <dd>{{ form.due_date }}</dd>

View File

@ -7,6 +7,7 @@ from helpdesk import settings as helpdesk_settings
from helpdesk.models import CustomField, Queue, Ticket from helpdesk.models import CustomField, Queue, Ticket
from helpdesk.templatetags.ticket_to_link import num_to_link from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser
from django.utils.translation import gettext_lazy as _
try: # python 3 try: # python 3
@ -323,3 +324,46 @@ class TicketActionsTestCase(TestCase):
ticket_1_follow_up, ticket_2_follow_up]) ticket_1_follow_up, ticket_2_follow_up])
self.assertEqual(list(ticket_1.ticketcc_set.all()), self.assertEqual(list(ticket_1.ticketcc_set.all()),
[ticket_1_cc, ticket_2_cc]) [ticket_1_cc, ticket_2_cc])
def test_update_ticket_queue(self):
"""Tests whether user can change the queue in the Respond to this ticket section."""
# log user in
self.loginUser()
# create ticket
initial_data = {
'title': 'Queue change ticket test',
'queue': self.queue_public,
'assigned_to': self.user,
'status': Ticket.OPEN_STATUS,
}
ticket = Ticket.objects.create(**initial_data)
ticket_id = ticket.id
# initial queue
self.assertEqual(ticket.queue, self.queue_public)
# POST first follow-up with new queue
new_queue = Queue.objects.create(
title='New Queue',
slug='newqueue',
)
post_data = {
'comment': 'first follow-up in new queue',
'queue': str(new_queue.id),
}
response = self.client.post(reverse('helpdesk:update',
kwargs={'ticket_id': ticket_id}),
post_data)
# queue was correctly modified
ticket.refresh_from_db()
self.assertEqual(ticket.queue, new_queue)
# ticket change was saved
latest_fup = ticket.followup_set.latest('date')
latest_ticketchange = latest_fup.ticketchange_set.latest('id')
self.assertEqual(latest_ticketchange.field, _('Queue'))
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id)
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)

View File

@ -2,7 +2,10 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.test.client import Client
from django.urls import reverse
from helpdesk.models import FollowUp, Queue, Ticket from helpdesk.models import FollowUp, Queue, Ticket
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
import uuid import uuid
@ -33,6 +36,21 @@ class TimeSpentAutoTestCase(TestCase):
is_active=True is_active=True
) )
self.client = Client()
def loginUser(self, is_staff=True):
"""Create a staff user and login"""
User = get_user_model()
self.user = User.objects.create(
username='User_1',
is_staff=is_staff,
)
self.user.set_password('pass')
self.user.save()
self.client.login(username='User_1', password='pass')
def test_add_two_followups_time_spent_auto(self): def test_add_two_followups_time_spent_auto(self):
"""Tests automatic time_spent calculation.""" """Tests automatic time_spent calculation."""
# activate automatic calculation # activate automatic calculation
@ -252,3 +270,56 @@ class TimeSpentAutoTestCase(TestCase):
# Remove queues exclusion # Remove queues exclusion
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = () helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
def test_http_followup_time_spent_auto_exclude_queues(self):
"""Tests automatic time_spent calculation queues exclusion with client"""
# activate automatic calculation
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('stop1', 'stop2')
# make staff user
self.loginUser()
# create queues
queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end')
queues = dict()
for slug in queues_sequence:
queues[slug] = Queue.objects.create(
title=slug,
slug=slug,
)
# create ticket
initial_data = {
'title': 'Queue change ticket test',
'queue': queues['new'],
'assigned_to': self.user,
'status': Ticket.OPEN_STATUS,
'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
}
ticket = Ticket.objects.create(**initial_data)
# create a change queue follow-up every hour
# first follow-up created at the same time of the ticket without queue change
# new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end
for (i, queue) in enumerate(queues_sequence):
# create follow-up
post_data = {
'comment': 'ticket in queue {}'.format(queue),
'queue': queues[queue].id,
}
response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket.id}), post_data)
latest_fup = ticket.followup_set.latest('id')
latest_fup.date = ticket.created + timedelta(hours=i)
latest_fup.time_spent = None
latest_fup.save()
# total ticket time for followups is 5 hours
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
# calculated time spent with 2 hours exclusion is 3 hours
self.assertEqual(ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds())
# remove queues exclusion
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()

View File

@ -200,6 +200,7 @@ def update_ticket(
owner=-1, owner=-1,
ticket_title=None, ticket_title=None,
priority=-1, priority=-1,
queue=-1,
new_status=None, new_status=None,
time_spent=None, time_spent=None,
due_date=None, due_date=None,
@ -213,6 +214,8 @@ def update_ticket(
title = ticket.title title = ticket.title
if priority == -1: if priority == -1:
priority = ticket.priority priority = ticket.priority
if queue == -1:
queue = ticket.queue.id
if new_status is None: if new_status is None:
new_status = ticket.status new_status = ticket.status
if new_checklists is None: if new_checklists is None:
@ -302,6 +305,14 @@ def update_ticket(
c.save() c.save()
ticket.priority = priority ticket.priority = priority
if queue != ticket.queue.id:
c = f.ticketchange_set.create(
field=_('Queue'),
old_value=ticket.queue.id,
new_value=queue,
)
ticket.queue_id = queue
if due_date != ticket.due_date: if due_date != ticket.due_date:
c = TicketChange( c = TicketChange(
followup=f, followup=f,

View File

@ -428,6 +428,7 @@ def view_ticket(request, ticket_id):
'form': form, 'form': form,
'active_users': users, 'active_users': users,
'priorities': Ticket.PRIORITY_CHOICES, 'priorities': Ticket.PRIORITY_CHOICES,
'queues': queue_choices,
'preset_replies': PreSetReply.objects.filter( 'preset_replies': PreSetReply.objects.filter(
Q(queues=ticket.queue) | Q(queues__isnull=True)), Q(queues=ticket.queue) | Q(queues__isnull=True)),
'ticketcc_string': ticketcc_string, 'ticketcc_string': ticketcc_string,
@ -566,6 +567,7 @@ def update_ticket_view(request, ticket_id, public=False):
title = request.POST.get('title', '') title = request.POST.get('title', '')
owner = int(request.POST.get('owner', -1)) owner = int(request.POST.get('owner', -1))
priority = int(request.POST.get('priority', ticket.priority)) priority = int(request.POST.get('priority', ticket.priority))
queue = int(request.POST.get('queue', ticket.queue.id))
# Check if a change happened on checklists # Check if a change happened on checklists
new_checklists = {} new_checklists = {}
@ -589,6 +591,7 @@ def update_ticket_view(request, ticket_id, public=False):
new_status == ticket.status, new_status == ticket.status,
title == ticket.title, title == ticket.title,
priority == int(ticket.priority), priority == int(ticket.priority),
queue == int(ticket.queue.id),
due_date == ticket.due_date, due_date == ticket.due_date,
(owner == -1) or (not owner and not ticket.assigned_to) or (owner == -1) or (not owner and not ticket.assigned_to) or
(owner and User.objects.get(id=owner) == ticket.assigned_to), (owner and User.objects.get(id=owner) == ticket.assigned_to),
@ -605,6 +608,7 @@ def update_ticket_view(request, ticket_id, public=False):
public = request.POST.get('public', False), public = request.POST.get('public', False),
owner = int(request.POST.get('owner', -1)), owner = int(request.POST.get('owner', -1)),
priority = int(request.POST.get('priority', -1)), priority = int(request.POST.get('priority', -1)),
queue = int(request.POST.get('queue', -1)),
new_status = new_status, new_status = new_status,
time_spent = get_time_spent_from_request(request), time_spent = get_time_spent_from_request(request),
due_date = get_due_date_from_request_or_ticket(request, ticket), due_date = get_due_date_from_request_or_ticket(request, ticket),