Skip to content

Commit 09e4a4d

Browse files
authored
Add list shifts for swap request endpoint (#2697)
Example request/response: `GET /api/internal/v1/shift_swaps/SSR3FJC9H3HZCHT/shifts` ``` { "events": [ { "all_day": false, "start": "2023-08-01T00:00:00Z", "end": "2023-08-01T03:00:00Z", "users": [ { "display_name": "testing", "email": "testing", "pk": "UWJWIN8MQ1GYL", "avatar_full": "http://localhost:3000/avatar/ae2b1fca515949e5d54fb22b8ed95575", "swap_request": { "pk": "SSR3FJC9H3HZCHT" } } ], "missing_users": [], "priority_level": 1, "source": "web", "calendar_type": 0, "is_empty": false, "is_gap": false, "is_override": false, "shift": { "pk": "OK9SS5YP42XRG" } }, { "all_day": false, "start": "2023-08-01T03:00:00Z", "end": "2023-08-02T00:00:00Z", "users": [ { "display_name": "testing", "email": "testing", "pk": "UWJWIN8MQ1GYL", "avatar_full": "http://localhost:3000/avatar/ae2b1fca515949e5d54fb22b8ed95575", "swap_request": { "pk": "SSR3FJC9H3HZCHT" } } ], "missing_users": [], "priority_level": 1, "source": "web", "calendar_type": 0, "is_empty": false, "is_gap": false, "is_override": false, "shift": { "pk": "OK9SS5YP42XRG" } } ] } ```
1 parent 655ecd3 commit 09e4a4d

File tree

5 files changed

+129
-2
lines changed

5 files changed

+129
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Added
1717

1818
- Add filter_shift_swaps endpoint to schedules API ([#2684](https://github.com/grafana/oncall/pull/2684))
19+
- Add shifts endpoint to shift swap API ([#2697](https://github.com/grafana/oncall/pull/2697/))
1920

2021
### Fixed
2122

engine/apps/api/tests/test_shift_swaps.py

+73-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rest_framework.test import APIClient
1111

1212
from apps.api.permissions import LegacyAccessControlRole
13-
from apps.schedules.models import OnCallScheduleWeb, ShiftSwapRequest
13+
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest
1414
from common.api_helpers.utils import serialize_datetime_as_utc_timestamp
1515
from common.insight_log import EntityEvent
1616

@@ -466,6 +466,53 @@ def test_partial_update_time_related_fields(ssr_setup, make_user_auth_headers):
466466
assert response.json() == expected_response
467467

468468

469+
@pytest.mark.django_db
470+
def test_related_shifts(ssr_setup, make_on_call_shift, make_user_auth_headers):
471+
ssr, beneficiary, token, _ = ssr_setup()
472+
473+
schedule = ssr.schedule
474+
organization = schedule.organization
475+
user = beneficiary
476+
477+
today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
478+
start = today + timezone.timedelta(days=2)
479+
duration = timezone.timedelta(hours=8)
480+
data = {
481+
"start": start,
482+
"rotation_start": start,
483+
"duration": duration,
484+
"priority_level": 1,
485+
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
486+
"schedule": schedule,
487+
}
488+
on_call_shift = make_on_call_shift(
489+
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
490+
)
491+
on_call_shift.add_rolling_users([[user]])
492+
493+
client = APIClient()
494+
url = reverse("api-internal:shift_swap-shifts", kwargs={"pk": ssr.public_primary_key})
495+
auth_headers = make_user_auth_headers(beneficiary, token)
496+
response = client.get(url, **auth_headers)
497+
498+
assert response.status_code == status.HTTP_200_OK
499+
response_json = response.json()
500+
expected = [
501+
# start, end, user, swap request ID
502+
(
503+
start.strftime("%Y-%m-%dT%H:%M:%SZ"),
504+
(start + duration).strftime("%Y-%m-%dT%H:%M:%SZ"),
505+
user.public_primary_key,
506+
ssr.public_primary_key,
507+
),
508+
]
509+
returned_events = [
510+
(e["start"], e["end"], e["users"][0]["pk"], e["users"][0]["swap_request"]["pk"])
511+
for e in response_json["events"]
512+
]
513+
assert returned_events == expected
514+
515+
469516
@pytest.mark.django_db
470517
@pytest.mark.parametrize(
471518
"role,expected_status",
@@ -714,3 +761,28 @@ def test_take_permissions(
714761

715762
response = client.post(url, format="json", **make_user_auth_headers(benefactor, token))
716763
assert response.status_code == expected_status
764+
765+
766+
@patch("apps.api.views.shift_swap.ShiftSwapViewSet.shifts", return_value=mock_success_response)
767+
@pytest.mark.django_db
768+
@pytest.mark.parametrize(
769+
"role,expected_status",
770+
[
771+
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
772+
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
773+
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
774+
],
775+
)
776+
def test_list_shifts_permissions(
777+
mock_endpoint_handler,
778+
ssr_setup,
779+
make_user_auth_headers,
780+
role,
781+
expected_status,
782+
):
783+
ssr, beneficiary, token, _ = ssr_setup(beneficiary_role=role)
784+
client = APIClient()
785+
url = reverse("api-internal:shift_swap-shifts", kwargs={"pk": ssr.public_primary_key})
786+
787+
response = client.get(url, format="json", **make_user_auth_headers(beneficiary, token))
788+
assert response.status_code == expected_status

engine/apps/api/views/shift_swap.py

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ShiftSwapViewSet(PublicPrimaryKeyMixin, ModelViewSet):
3636
"partial_update": [RBACPermission.Permissions.SCHEDULES_WRITE],
3737
"destroy": [RBACPermission.Permissions.SCHEDULES_WRITE],
3838
"take": [RBACPermission.Permissions.SCHEDULES_WRITE],
39+
"shifts": [RBACPermission.Permissions.SCHEDULES_READ],
3940
}
4041

4142
is_beneficiary = IsOwner(ownership_field="beneficiary")
@@ -87,6 +88,13 @@ def perform_update(self, serializer) -> None:
8788

8889
update_shift_swap_request_message.apply_async((shift_swap_request.pk,))
8990

91+
@action(methods=["get"], detail=True)
92+
def shifts(self, request, pk) -> Response:
93+
shift_swap = self.get_object()
94+
result = {"events": shift_swap.shifts()}
95+
96+
return Response(result, status=status.HTTP_200_OK)
97+
9098
@action(methods=["post"], detail=True)
9199
def take(self, request, pk) -> Response:
92100
shift_swap = self.get_object()

engine/apps/schedules/models/shift_swap_request.py

+11
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ def hard_delete(self):
165165
# make sure final schedule ical representation is updated
166166
refresh_ical_final_schedule.apply_async((self.schedule.pk,))
167167

168+
def shifts(self):
169+
"""Return shifts affected by this swap request."""
170+
schedule = self.schedule.get_real_instance()
171+
events = schedule.final_events(self.swap_start, self.swap_end)
172+
related_shifts = [
173+
e
174+
for e in events
175+
if self.public_primary_key in set(u["swap_request"]["pk"] for u in e["users"] if u.get("swap_request"))
176+
]
177+
return related_shifts
178+
168179
def take(self, benefactor: "User") -> None:
169180
if benefactor == self.beneficiary:
170181
raise exceptions.BeneficiaryCannotTakeOwnShiftSwapRequest()

engine/apps/schedules/tests/test_shift_swap_request.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from unittest.mock import patch
33

44
import pytest
5+
from django.utils import timezone
56

67
from apps.schedules import exceptions
7-
from apps.schedules.models import ShiftSwapRequest
8+
from apps.schedules.models import CustomOnCallShift, ShiftSwapRequest
89

910

1011
@pytest.mark.django_db
@@ -116,3 +117,37 @@ def test_take_own_ssr(shift_swap_request_setup) -> None:
116117
ssr, beneficiary, _ = shift_swap_request_setup()
117118
with pytest.raises(exceptions.BeneficiaryCannotTakeOwnShiftSwapRequest):
118119
ssr.take(beneficiary)
120+
121+
122+
@pytest.mark.django_db
123+
def test_related_shifts(shift_swap_request_setup, make_on_call_shift) -> None:
124+
ssr, beneficiary, _ = shift_swap_request_setup()
125+
126+
schedule = ssr.schedule
127+
organization = schedule.organization
128+
user = beneficiary
129+
130+
today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
131+
start = today + timezone.timedelta(days=2)
132+
duration = timezone.timedelta(hours=8)
133+
data = {
134+
"start": start,
135+
"rotation_start": start,
136+
"duration": duration,
137+
"priority_level": 1,
138+
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
139+
"schedule": schedule,
140+
}
141+
on_call_shift = make_on_call_shift(
142+
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
143+
)
144+
on_call_shift.add_rolling_users([[user]])
145+
146+
events = ssr.shifts()
147+
148+
expected = [
149+
# start, end, user, swap request ID
150+
(start, start + duration, user.public_primary_key, ssr.public_primary_key),
151+
]
152+
returned_events = [(e["start"], e["end"], e["users"][0]["pk"], e["users"][0]["swap_request"]["pk"]) for e in events]
153+
assert returned_events == expected

0 commit comments

Comments
 (0)