mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-22 16:03:19 +01:00
Merge pull request #1167 from samsplunks/followup_queue_change
Allow to track queue change in follow-ups
This commit is contained in:
commit
9f7c18e507
@ -1007,35 +1007,60 @@ 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."
|
||||||
|
|
||||||
|
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
|
# extract earliest from previous follow-up or ticket
|
||||||
try:
|
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")
|
prev_fup = prev_fup_qs.latest("date")
|
||||||
earliest = prev_fup.date
|
earliest = prev_fup.date
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
earliest = self.ticket.created
|
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 time is current follow-up date
|
||||||
latest = self.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
|
# split time interval by days
|
||||||
days = latest.toordinal() - earliest.toordinal()
|
days = latest.toordinal() - earliest.toordinal()
|
||||||
for day in range(days + 1):
|
for day in range(days + 1):
|
||||||
@ -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)
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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)
|
@ -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 = ()
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user