Merge pull request #1159 from samsplunks/main

Time tracking features
This commit is contained in:
Christopher Broderick 2024-02-27 03:18:24 +00:00 committed by GitHub
commit ed321159fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 188 additions and 19 deletions

View File

@ -291,6 +291,56 @@ Options that change ticket properties
(7, _('7. Hot')), (7, _('7. Hot')),
) )
Time Tracking Options
---------------------
- **HELPDESK_FOLLOWUP_TIME_SPENT_AUTO** If ``True``, calculate follow-up 'time_spent' with previous follow-up or ticket creation time.
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_AUTO = False``
- **HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS** If defined, calculates follow-up 'time_spent' according to open hours.
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}``
If HELPDESK_FOLLOWUP_TIME_SPENT_AUTO is ``True``, you may set open hours to remove off hours from 'time_spent'::
HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {
"monday": (8.5, 19),
"tuesday": (8.5, 19),
"wednesday": (8.5, 19),
"thursday": (8.5, 19),
"friday": (8.5, 19),
"saturday": (0, 0),
"sunday": (0, 0),
}
Valid hour values must be set between 0 and 23.9999.
In this example 8.5 is interpreted as 8:30AM, saturdays and sundays don't count.
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS** List of days in format "%Y-%m-%d" to exclude from automatic follow-up 'time_spent' calculation.
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ()``
This example removes Christmas and New Year's Eve in 2024::
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ("2024-12-25", "2024-12-31",)
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES** List of ticket statuses to exclude from automatic follow-up 'time_spent' calculation.
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()``
This example will have follow-ups to resolved ticket status not to be counted in::
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (HELPDESK_TICKET_RESOLVED_STATUS,)
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES** List of ticket queues slugs to exclude from automatic follow-up 'time_spent' calculation.
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()``
This example will have follow-ups to ticket queue 'time-not-counting-queue' not to be counted in::
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('time-not-counting-queue',)
Staff Ticket Creation Settings Staff Ticket Creation Settings
------------------------------ ------------------------------

View File

@ -9,7 +9,7 @@ lib.py - Common functions (eg multipart e-mail)
from datetime import date, datetime, time from datetime import date, datetime, time
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
import logging import logging
@ -173,11 +173,10 @@ def format_time_spent(time_spent):
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in """Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
all graphical outputs all graphical outputs
""" """
if time_spent: if time_spent:
time_spent = "{0:02d}h:{1:02d}m".format( time_spent = "{0:02d}h:{1:02d}m".format(
time_spent.seconds // 3600, int(time_spent.total_seconds()) // 3600,
time_spent.seconds // 60 int(time_spent.total_seconds()) % 3600 // 60
) )
else: else:
time_spent = "" time_spent = ""
@ -194,3 +193,45 @@ def convert_value(value):
return value.strftime(CUSTOMFIELD_TIME_FORMAT) return value.strftime(CUSTOMFIELD_TIME_FORMAT)
else: else:
return value return value
def daily_time_spent_calculation(earliest, latest, open_hours):
"""Returns the number of seconds for a single day time interval according to open hours."""
# avoid rendering day in different locale
weekday = ('monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday')[earliest.weekday()]
# enforce correct settings
MIDNIGHT = 23.9999
start, end = open_hours.get(weekday, (0, MIDNIGHT))
if not 0 <= start <= end <= MIDNIGHT:
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
f" setting for {weekday} out of (0, 23.9999) boundary")
# transform decimals to minutes and seconds
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60)
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60)
# translate time for delta calculation
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
latest_f = latest.hour + latest.minute / 60 + latest.second / 3600
if earliest_f < start:
earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second)
elif earliest_f >= end:
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
if latest_f < start:
latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second)
elif latest_f >= end:
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
day_delta = latest - earliest
# returns up to 86399 seconds, add one second if full day
time_spent_seconds = day_delta.seconds
if time_spent_seconds == 86399:
time_spent_seconds += 1
return time_spent_seconds

View File

@ -8,7 +8,7 @@ models.py - Model (and hence database) definitions. This is the core of the
""" """
from .lib import convert_value from .lib import format_time_spent, convert_value, daily_time_spent_calculation
from .templated_email import send_templated_mail from .templated_email import send_templated_mail
from .validators import validate_file_extension from .validators import validate_file_extension
from .webhooks import send_new_ticket_webhook from .webhooks import send_new_ticket_webhook
@ -33,17 +33,6 @@ from rest_framework import serializers
import uuid import uuid
def format_time_spent(time_spent):
if time_spent:
time_spent = "{0:02d}h:{1:02d}m".format(
time_spent.seconds // 3600,
time_spent.seconds % 3600 // 60
)
else:
time_spent = ""
return time_spent
class EscapeHtml(Extension): class EscapeHtml(Extension):
def extendMarkdown(self, md): def extendMarkdown(self, md):
md.preprocessors.deregister('html_block') md.preprocessors.deregister('html_block')
@ -1000,9 +989,12 @@ class FollowUp(models.Model):
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id) return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
t = self.ticket self.ticket.modified = timezone.now()
t.modified = timezone.now() self.ticket.save()
t.save()
if helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO and not self.time_spent:
self.time_spent = self.time_spent_calculation()
super(FollowUp, self).save(*args, **kwargs) super(FollowUp, self).save(*args, **kwargs)
def get_markdown(self): def get_markdown(self):
@ -1012,6 +1004,62 @@ class FollowUp(models.Model):
def time_spent_formated(self): def time_spent_formated(self):
return format_time_spent(self.time_spent) return format_time_spent(self.time_spent)
def time_spent_calculation(self):
"Returns timedelta according to rules settings."
# 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):
if day == 0:
start_day_time = earliest
if days == 0:
# close single day case
end_day_time = latest
else:
end_day_time = earliest.replace(hour=23, minute=59, second=59)
elif day == days:
start_day_time = latest.replace(hour=0, minute=0, second=0)
end_day_time = latest
else:
middle_day_time = earliest + datetime.timedelta(days=day)
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)
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):
time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours)
return datetime.timedelta(seconds=time_spent_seconds)
class TicketChange(models.Model): class TicketChange(models.Model):
""" """

View File

@ -151,6 +151,36 @@ TICKET_PRIORITY_CHOICES = getattr(settings,
'HELPDESK_TICKET_PRIORITY_CHOICES', 'HELPDESK_TICKET_PRIORITY_CHOICES',
DEFAULT_TICKET_PRIORITY_CHOICES) DEFAULT_TICKET_PRIORITY_CHOICES)
#########################
# time tracking options #
#########################
# Follow-ups automatic time_spent calculation
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings,
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
False)
# Calculate time_spent according to open hours
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings,
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS',
{})
# Holidays don't count for time_spent calculation
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings,
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS',
())
# Time doesn't count for listed ticket statuses
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings,
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES',
())
# Time doesn't count for listed queues slugs
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings,
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES',
())
############################ ############################
# options for public pages # # options for public pages #
############################ ############################