From e6274dc99227940e7db8e63628aa639f31d205f4 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Tue, 26 Jul 2022 15:14:15 -0300 Subject: [PATCH] Add schedule next shifts per user endpoint to internal API --- engine/apps/api/tests/test_schedules.py | 76 +++++++++++++++++++++++++ engine/apps/api/views/schedule.py | 21 +++++++ 2 files changed, 97 insertions(+) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 7e49a987..bac9b3ec 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -432,6 +432,7 @@ def test_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, + "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, @@ -497,6 +498,7 @@ def test_filter_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, + "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, @@ -512,6 +514,7 @@ def test_filter_events_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, + "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, @@ -594,6 +597,7 @@ def test_filter_events_range_calendar( "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, + "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, @@ -675,6 +679,7 @@ def test_filter_events_overrides( "calendar_type": OnCallSchedule.OVERRIDES, "is_empty": False, "is_gap": False, + "is_override": True, "shift": { "pk": override.public_primary_key, }, @@ -772,6 +777,7 @@ def test_filter_events_final_schedule( "calendar_type": 1 if is_override else None if is_gap else 0, "end": start_date + timezone.timedelta(hours=start + duration), "is_gap": is_gap, + "is_override": is_override, "priority_level": priority, "start": start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0), "user": user, @@ -783,6 +789,7 @@ def test_filter_events_final_schedule( "calendar_type": e["calendar_type"], "end": e["end"], "is_gap": e["is_gap"], + "is_override": e["is_override"], "priority_level": e["priority_level"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, @@ -792,6 +799,75 @@ def test_filter_events_final_schedule( assert returned_events == expected_events +@pytest.mark.django_db +def test_next_shifts_per_user( + 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", + ) + + tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=1) + user_a, user_b, user_c = (make_user_for_organization(organization, username=i) for i in "ABC") + + shifts = ( + # user, priority, start time (h), duration (hs) + (user_a, 1, 8, 2), # r1-1: 8-10 / A + (user_a, 1, 15, 2), # r1-2: 15-17 / A + (user_b, 2, 7, 5), # r2-1: 7-12 / B + (user_b, 2, 16, 2), # r2-2: 16-18 / B + (user_c, 2, 18, 2), # r2-3: 18-20 / C + ) + for user, priority, start_h, duration in shifts: + data = { + "start": tomorrow + timezone.timedelta(hours=start_h), + "rotation_start": tomorrow, + "duration": timezone.timedelta(hours=duration), + "priority_level": priority, + "frequency": CustomOnCallShift.FREQUENCY_DAILY, + "schedule": schedule, + } + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data + ) + on_call_shift.users.add(user) + + # override: 17-18 / C + override_data = { + "start": tomorrow + timezone.timedelta(hours=17), + "rotation_start": tomorrow, + "duration": timezone.timedelta(hours=1), + "schedule": schedule, + } + override = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data + ) + override.add_rolling_users([[user_c]]) + + # final schedule: 7-12: B, 15-16: A, 16-17: B, 17-18: C (override), 18-20: C + + url = reverse("api-internal:schedule-next-shifts-per-user", kwargs={"pk": schedule.public_primary_key}) + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + expected = { + user_a.public_primary_key: (tomorrow + timezone.timedelta(hours=15), tomorrow + timezone.timedelta(hours=16)), + user_b.public_primary_key: (tomorrow + timezone.timedelta(hours=7), tomorrow + timezone.timedelta(hours=12)), + user_c.public_primary_key: (tomorrow + timezone.timedelta(hours=17), tomorrow + timezone.timedelta(hours=18)), + } + returned_data = {u: (ev["start"], ev["end"]) for u, ev in response.data["users"].items()} + assert returned_data == expected + + @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 f7d25ab5..25c35f10 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -58,6 +58,7 @@ class ScheduleView( *READ_ACTIONS, "events", "filter_events", + "next_shifts_per_user", "notify_empty_oncall_options", "notify_oncall_shift_freq_options", "mention_options", @@ -222,6 +223,7 @@ class ScheduleView( "calendar_type": shift["calendar_type"], "is_empty": len(shift["users"]) == 0 and not is_gap, "is_gap": is_gap, + "is_override": shift["calendar_type"] == OnCallSchedule.TYPE_ICAL_OVERRIDES, "shift": { "pk": shift["shift_pk"], }, @@ -395,6 +397,25 @@ class ScheduleView( resolved.sort(key=lambda e: e["start"]) return resolved + @action(detail=True, methods=["get"]) + def next_shifts_per_user(self, request, pk): + """Return next shift for users in schedule.""" + user_tz, _ = self.get_request_timezone() + now = timezone.now() + starting_date = now.date() + schedule = self.original_get_object() + shift_events = self._filter_events(schedule, user_tz, starting_date, days=30, with_empty=False, with_gap=False) + events = self._resolve_schedule(shift_events) + + users = {} + for e in events: + user = e["users"][0]["pk"] if e["users"] else None + if user is not None and user not in users and e["end"] > now: + users[user] = e + + result = {"users": users} + return Response(result, status=status.HTTP_200_OK) + @action(detail=False, methods=["get"]) def type_options(self, request): # TODO: check if it needed