diff --git a/README.rst b/README.rst index abd82169..972d8c92 100644 --- a/README.rst +++ b/README.rst @@ -110,6 +110,9 @@ From the command line you can run the tests using: `make test` See `quicktest.py` for usage details. +If you need to create tests for new features, add your tests in a test file to the `tests` module and call them in the test VENV with:: + python quicktest.py helpdesk.tests.test_my_new_features -v 2 + Upgrading from previous versions -------------------------------- diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 481d4cbb..96bb430f 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -198,6 +198,8 @@ def convert_value(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.""" + time_spent_seconds = 0 + # avoid rendering day in different locale weekday = ('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday')[earliest.weekday()] @@ -215,8 +217,12 @@ def daily_time_spent_calculation(earliest, latest, open_hours): # 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 + latest_f = latest.hour + latest.minute / 60 + latest.second / (60 * 60) + latest.microsecond / (60 * 60 * 999999) + # if latest time is midnight and close hour is midnight, add a second to the time spent + if latest_f >= MIDNIGHT and end == MIDNIGHT: + time_spent_seconds += 1 + if earliest_f < start: earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second) elif earliest_f >= end: @@ -230,8 +236,6 @@ def daily_time_spent_calculation(earliest, latest, open_hours): 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 + time_spent_seconds += day_delta.seconds return time_spent_seconds \ No newline at end of file diff --git a/helpdesk/models.py b/helpdesk/models.py index 4409d901..1781093f 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -994,7 +994,7 @@ class FollowUp(models.Model): 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) def get_markdown(self): @@ -1045,14 +1045,14 @@ class FollowUp(models.Model): # close single day case end_day_time = latest else: - end_day_time = earliest.replace(hour=23, minute=59, second=59) + end_day_time = earliest.replace(hour=23, minute=59, second=59, microsecond=999999) 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) + 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 diff --git a/helpdesk/tests/test_time_spent_auto.py b/helpdesk/tests/test_time_spent_auto.py new file mode 100644 index 00000000..d40d2800 --- /dev/null +++ b/helpdesk/tests/test_time_spent_auto.py @@ -0,0 +1,262 @@ + +from datetime import datetime, timedelta +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User +from django.test import TestCase, override_settings +from django.test.client import Client +from helpdesk.models import FollowUp, Queue, Ticket +from helpdesk import settings as helpdesk_settings +import uuid + + +@override_settings(USE_TZ=True) +class TimeSpentAutoTestCase(TestCase): + + def setUp(self): + """Creates a queue, ticket and user.""" + self.queue_public = Queue.objects.create( + title='Queue 1', + slug='q1', + allow_public_submission=True, + dedicated_time=timedelta(minutes=60) + ) + + self.ticket_data = dict(queue=self.queue_public, + title='test ticket', + description='test ticket description') + + self.client = Client() + + self.user = User.objects.create( + username='staff', + email='staff@example.com', + password=make_password('Test1234'), + is_staff=True, + is_superuser=False, + is_active=True + ) + + def test_add_two_followups_time_spent_auto(self): + """Tests automatic time_spent calculation.""" + # activate automatic calculation + helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True + + # ticket creation date, follow-up creation date, assertion value + TEST_VALUES = ( + # friday + ('2024-03-01T00:00:00+00:00', '2024-03-01T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), + ('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:58+00:00', timedelta(hours=23, minutes=59, seconds=58)), + ('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)), + ('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)), + ('2024-03-01T00:00:00+00:00', '2024-03-02T09:00:00+00:00', timedelta(hours=33)), + ('2024-03-01T00:00:00+00:00', '2024-03-03T00:00:00+00:00', timedelta(hours=48)), + ) + + for (ticket_time, fup_time, assertion_delta) in TEST_VALUES: + # create and setup test ticket time + ticket = Ticket.objects.create(**self.ticket_data) + ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") + ticket.created = ticket_time_p + ticket.modified = ticket_time_p + ticket.save() + + fup_time_p = datetime.strptime(fup_time, "%Y-%m-%dT%H:%M:%S%z") + followup1 = FollowUp.objects.create( + ticket=ticket, + date=fup_time_p, + title="Testing followup", + comment="Testing followup time spent", + public=True, + user=self.user, + new_status=1, + message_id=uuid.uuid4().hex, + time_spent=None + ) + followup1.save() + + self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) + self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) + + # adding a second follow-up at different intervals + for delta in (timedelta(seconds=1), timedelta(minutes=1), timedelta(hours=1), timedelta(days=1), timedelta(days=10)): + + followup2 = FollowUp.objects.create( + ticket=ticket, + date=followup1.date + delta, + title="Testing followup 2", + comment="Testing followup time spent 2", + public=True, + user=self.user, + new_status=1, + message_id=uuid.uuid4().hex, + time_spent=None + ) + followup2.save() + + self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds()) + self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds()) + + # delete second follow-up as we test it with many intervals + followup2.delete() + + + def test_followup_time_spent_auto_opening_hours(self): + """Tests automatic time_spent calculation with opening hours and holidays.""" + + # activate automatic calculation + helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True + helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = { + "monday": (0, 23.9999), + "tuesday": (8, 18), + "wednesday": (8.5, 18.5), + "thursday": (0, 10), + "friday": (13, 23), + "saturday": (0, 0), + "sunday": (0, 0), + } + + # adding holidays + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ( + '2024-03-18', '2024-03-19', '2024-03-20', '2024-03-21', '2024-03-22', + ) + + # ticket creation date, follow-up creation date, assertion value + TEST_VALUES = ( + # monday + ('2024-03-04T00:00:00+00:00', '2024-03-04T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), + # tuesday + ('2024-03-05T07:00:00+00:00', '2024-03-05T09:00:00+00:00', timedelta(hours=1)), + ('2024-03-05T17:50:00+00:00', '2024-03-05T17:51:00+00:00', timedelta(minutes=1)), + ('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)), + ('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)), + ('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)), + # wednesday + ('2024-03-06T08:00:00+00:00', '2024-03-06T09:01:00+00:00', timedelta(minutes=31)), + ('2024-03-06T01:00:00+00:00', '2024-03-06T19:30:10+00:00', timedelta(hours=10)), + ('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)), + # thursday + ('2024-03-07T00:00:00+00:00', '2024-03-07T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), + ('2024-03-07T09:30:00+00:00', '2024-03-07T10:30:00+00:00', timedelta(minutes=30)), + # friday + ('2024-03-08T00:00:00+00:00', '2024-03-08T23:30:10+00:00', timedelta(hours=10)), + # saturday + ('2024-03-09T00:00:00+00:00', '2024-03-09T09:30:10+00:00', timedelta(hours=0)), + # sunday + ('2024-03-10T00:00:00+00:00', '2024-03-10T09:30:10+00:00', timedelta(hours=0)), + + # monday to sunday + ('2024-03-04T04:00:00+00:00', '2024-03-10T09:00:00+00:00', timedelta(hours=60)), + + # two weeks + ('2024-03-04T04:00:00+00:00', '2024-03-17T09:00:00+00:00', timedelta(hours=124)), + + # three weeks, the third one is holidays + ('2024-03-04T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=124)), + ('2024-03-18T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=0)), + ) + + for (ticket_time, fup_time, assertion_delta) in TEST_VALUES: + # create and setup test ticket time + ticket = Ticket.objects.create(**self.ticket_data) + ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") + ticket.created = ticket_time_p + ticket.modified = ticket_time_p + ticket.save() + + fup_time_p = datetime.strptime(fup_time, "%Y-%m-%dT%H:%M:%S%z") + followup1 = FollowUp.objects.create( + ticket=ticket, + date=fup_time_p, + title="Testing followup", + comment="Testing followup time spent", + public=True, + user=self.user, + new_status=1, + message_id=uuid.uuid4().hex, + time_spent=None + ) + followup1.save() + + self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) + self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) + + # removing opening hours and holidays + helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {} + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = () + + def test_followup_time_spent_auto_exclude_statuses(self): + """Tests automatic time_spent calculation OPEN_STATUS exclusion.""" + + # activate automatic calculation + helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True + + # Follow-ups with OPEN_STATUS are excluded from time counting + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,) + + + # create and setup test ticket time + ticket = Ticket.objects.create(**self.ticket_data) + ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + ticket.created = ticket_time_p + ticket.modified = ticket_time_p + ticket.save() + + fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + followup1 = FollowUp.objects.create( + ticket=ticket, + date=fup_time_p, + title="Testing followup", + comment="Testing followup time spent", + public=True, + user=self.user, + new_status=1, + message_id=uuid.uuid4().hex, + time_spent=None + ) + followup1.save() + + # The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation + self.assertEqual(followup1.time_spent.total_seconds(), 0.0) + self.assertEqual(ticket.time_spent.total_seconds(), 0.0) + + # Remove status exclusion + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = () + + + def test_followup_time_spent_auto_exclude_queues(self): + """Tests automatic time_spent calculation queues exclusion.""" + + # activate automatic calculation + helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True + + # Follow-ups within the default queue are excluded from time counting + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('q1',) + + + # create and setup test ticket time + ticket = Ticket.objects.create(**self.ticket_data) + ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + ticket.created = ticket_time_p + ticket.modified = ticket_time_p + ticket.save() + + fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + followup1 = FollowUp.objects.create( + ticket=ticket, + date=fup_time_p, + title="Testing followup", + comment="Testing followup time spent", + public=True, + user=self.user, + new_status=1, + message_id=uuid.uuid4().hex, + time_spent=None + ) + followup1.save() + + # The Follow-up time_spent should be zero as the default queue was excluded from calculation + self.assertEqual(followup1.time_spent.total_seconds(), 0.0) + self.assertEqual(ticket.time_spent.total_seconds(), 0.0) + + # Remove queues exclusion + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = () \ No newline at end of file