Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to track queue change in follow-ups #1167

Merged
merged 12 commits into from
Apr 12, 2024
67 changes: 45 additions & 22 deletions helpdesk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,36 +1006,61 @@ def time_spent_formated(self):

def time_spent_calculation(self):
"Returns timedelta according to rules settings."

open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
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()
if self.id:
# if the follow-up exist in DB, only keep previous follow-ups
prev_fup_qs = prev_fup_qs.filter(date__lt=self.date)

# handle exclusions

# extract previous status from follow-up or ticket for exclusion check
if exclude_statuses:
try:
prev_fup = prev_fup_qs.latest("date")
prev_status = prev_fup.new_status
except ObjectDoesNotExist:
prev_status = self.ticket.status

# don't calculate status exclusions
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

# extract earliest from previous follow-up or ticket
try:
prev_fup_qs = self.ticket.followup_set.all()
if self.id:
prev_fup_qs = prev_fup_qs.filter(id__lt=self.id)
prev_fup = prev_fup_qs.latest("date")
earliest = prev_fup.date
except ObjectDoesNotExist:
earliest = self.ticket.created

# extract previous status from follow-up or ticket
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_status = prev_fup.new_status
except ObjectDoesNotExist:
prev_status = self.ticket.status

# latest time is current follow-up date
latest = self.date

time_spent_seconds = 0
open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
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

# split time interval by days
days = latest.toordinal() - earliest.toordinal()
for day in range(days + 1):
Expand All @@ -1054,9 +1079,7 @@ def time_spent_calculation(self):
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)

if (start_day_time.strftime("%Y-%m-%d") not in holidays and
prev_status not in exclude_statuses and
self.ticket.queue.slug not in exclude_queues):
if start_day_time.strftime("%Y-%m-%d") not in holidays:
time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours)

return datetime.timedelta(seconds=time_spent_seconds)
Expand Down
3 changes: 3 additions & 0 deletions helpdesk/templates/helpdesk/ticket.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ <h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
<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>

<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>
<dd>{{ form.due_date }}</dd>

Expand Down
44 changes: 44 additions & 0 deletions helpdesk/tests/test_ticket_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser
from django.utils.translation import gettext_lazy as _


try: # python 3
Expand Down Expand Up @@ -323,3 +324,46 @@ def test_merge_tickets(self):
ticket_1_follow_up, ticket_2_follow_up])
self.assertEqual(list(ticket_1.ticketcc_set.all()),
[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)
71 changes: 71 additions & 0 deletions helpdesk/tests/test_time_spent_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from datetime import datetime, timedelta
from django.contrib.auth.hashers import make_password
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.client import Client
from django.urls import reverse
from helpdesk.models import FollowUp, Queue, Ticket
from helpdesk import settings as helpdesk_settings
import uuid
Expand Down Expand Up @@ -33,6 +36,21 @@ def setUp(self):
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):
"""Tests automatic time_spent calculation."""
# activate automatic calculation
Expand Down Expand Up @@ -251,4 +269,57 @@ def test_followup_time_spent_auto_exclude_queues(self):
self.assertEqual(ticket.time_spent.total_seconds(), 0.0)

# Remove queues exclusion
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 = ()
11 changes: 11 additions & 0 deletions helpdesk/update_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def update_ticket(
owner=-1,
ticket_title=None,
priority=-1,
queue=-1,
new_status=None,
time_spent=None,
due_date=None,
Expand All @@ -213,6 +214,8 @@ def update_ticket(
title = ticket.title
if priority == -1:
priority = ticket.priority
if queue == -1:
queue = ticket.queue.id
if new_status is None:
new_status = ticket.status
if new_checklists is None:
Expand Down Expand Up @@ -302,6 +305,14 @@ def update_ticket(
c.save()
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:
c = TicketChange(
followup=f,
Expand Down
4 changes: 4 additions & 0 deletions helpdesk/views/staff.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def view_ticket(request, ticket_id):
'form': form,
'active_users': users,
'priorities': Ticket.PRIORITY_CHOICES,
'queues': queue_choices,
'preset_replies': PreSetReply.objects.filter(
Q(queues=ticket.queue) | Q(queues__isnull=True)),
'ticketcc_string': ticketcc_string,
Expand Down Expand Up @@ -566,6 +567,7 @@ def update_ticket_view(request, ticket_id, public=False):
title = request.POST.get('title', '')
owner = int(request.POST.get('owner', -1))
priority = int(request.POST.get('priority', ticket.priority))
queue = int(request.POST.get('queue', ticket.queue.id))

# Check if a change happened on checklists
new_checklists = {}
Expand All @@ -589,6 +591,7 @@ def update_ticket_view(request, ticket_id, public=False):
new_status == ticket.status,
title == ticket.title,
priority == int(ticket.priority),
queue == int(ticket.queue.id),
due_date == ticket.due_date,
(owner == -1) or (not owner and not ticket.assigned_to) or
(owner and User.objects.get(id=owner) == ticket.assigned_to),
Expand All @@ -605,6 +608,7 @@ def update_ticket_view(request, ticket_id, public=False):
public = request.POST.get('public', False),
owner = int(request.POST.get('owner', -1)),
priority = int(request.POST.get('priority', -1)),
queue = int(request.POST.get('queue', -1)),
new_status = new_status,
time_spent = get_time_spent_from_request(request),
due_date = get_due_date_from_request_or_ticket(request, ticket),
Expand Down