From 1a1c8efa96b02db58e682893a0b05bc7a0c7123a Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 1 Aug 2022 16:37:14 -0300 Subject: [PATCH] Update schedule filter_events to combine overlapping same-shift events --- engine/apps/api/tests/test_schedules.py | 103 +++++++++++++++++- engine/apps/api/views/schedule.py | 22 ++++ .../apps/schedules/models/on_call_schedule.py | 2 +- 3 files changed, 120 insertions(+), 7 deletions(-) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index bac9b3ec..31f4d7ef 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -557,9 +557,9 @@ def test_filter_events_range_calendar( "schedule": schedule, } on_call_shift = make_on_call_shift( - organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) - on_call_shift.users.add(user) + on_call_shift.add_rolling_users([[user]]) # add override shift override_start = request_date + timezone.timedelta(seconds=3600) @@ -640,9 +640,9 @@ def test_filter_events_overrides( "schedule": schedule, } on_call_shift = make_on_call_shift( - organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) - on_call_shift.users.add(user) + on_call_shift.add_rolling_users([[user]]) # add override shift override_start = request_date + timezone.timedelta(seconds=3600) @@ -735,9 +735,9 @@ def test_filter_events_final_schedule( "schedule": schedule, } on_call_shift = make_on_call_shift( - organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) - on_call_shift.users.add(user) + on_call_shift.add_rolling_users([[user]]) # override: 22-23 / E override_data = { @@ -868,6 +868,97 @@ def test_next_shifts_per_user( assert returned_data == expected +@pytest.mark.django_db +def test_merging_same_shift_events( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, + make_schedule, + make_on_call_shift, +): + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + + schedule = make_schedule( + organization, + schedule_class=OnCallScheduleWeb, + name="test_web_schedule", + ) + now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_date = now - timezone.timedelta(days=7) + request_date = start_date + + user_a = make_user_for_organization(organization) + user_b = make_user_for_organization(organization) + user_c = make_user_for_organization(organization, role=Role.VIEWER) + + data = { + "start": start_date + timezone.timedelta(hours=10), + "rotation_start": start_date, + "duration": timezone.timedelta(hours=2), + "priority_level": 1, + "frequency": CustomOnCallShift.FREQUENCY_DAILY, + "schedule": schedule, + } + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users([[user_a, user_c, user_b]]) + + expected_events = [ + { + "calendar_type": 0, + "end": start_date + timezone.timedelta(hours=12), + "is_gap": False, + "priority_level": 1, + "start": start_date + timezone.timedelta(hours=10), + "users": [user_a.username, user_b.username], + "missing_users": [user_c.username], + } + ] + + # final schedule + url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) + url += "?date={}&days=1".format(request_date.strftime("%Y-%m-%d")) + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + returned_events = [ + { + "calendar_type": e["calendar_type"], + "end": e["end"], + "is_gap": e["is_gap"], + "priority_level": e["priority_level"], + "start": e["start"], + "users": [u["display_name"] for u in e["users"]] if e["users"] else None, + "missing_users": e["missing_users"], + } + for e in response.data["events"] + if not e["is_gap"] + ] + assert returned_events == expected_events + + # rotations + url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) + url += "?date={}&days=1&type=rotation".format(request_date.strftime("%Y-%m-%d")) + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + returned_events = [ + { + "calendar_type": e["calendar_type"], + "end": e["end"], + "is_gap": e["is_gap"], + "priority_level": e["priority_level"], + "start": e["start"], + "users": [u["display_name"] for u in e["users"]] if e["users"] else None, + "missing_users": e["missing_users"], + } + for e in response.data["events"] + if not e["is_gap"] + ] + assert returned_events == expected_events + + @pytest.mark.django_db def test_filter_events_invalid_type( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 4af90abf..78f9f837 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -252,6 +252,9 @@ class ScheduleView( else: # return final schedule events = schedule.final_events(user_tz, starting_date, days) + # combine multiple-users same-shift events into one + events = self._merge_events(events) + result = { "id": schedule.public_primary_key, "name": schedule.name, @@ -260,6 +263,25 @@ class ScheduleView( } return Response(result, status=status.HTTP_200_OK) + def _merge_events(self, events): + """Merge user groups same-shift events.""" + if events: + merged = [events[0]] + current = merged[0] + for next_event in events[1:]: + if ( + current["start"] == next_event["start"] + and current["shift"]["pk"] is not None + and current["shift"]["pk"] == next_event["shift"]["pk"] + ): + current["users"] += next_event["users"] + current["missing_users"] += next_event["missing_users"] + else: + merged.append(next_event) + current = next_event + events = merged + return events + @action(detail=True, methods=["get"]) def next_shifts_per_user(self, request, pk): """Return next shift for users in schedule.""" diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 1589757b..52d4782f 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -364,7 +364,7 @@ class OnCallSchedule(PolymorphicModel): # event starts after the current interval, move to next interval and go through it current_interval_idx += 1 - resolved.sort(key=lambda e: e["start"]) + resolved.sort(key=lambda e: (e["start"], e["shift"]["pk"])) return resolved