From 4149c266bbd3175295939313615972cd30784aec Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Tue, 18 Apr 2023 10:16:36 -0300 Subject: [PATCH] Add mine filter to schedules listing (#1743) Related to #1741 . --- CHANGELOG.md | 4 ++ engine/apps/api/tests/test_schedules.py | 45 ++++++++++++- engine/apps/api/views/schedule.py | 10 +++ .../apps/schedules/models/on_call_schedule.py | 7 +- .../schedules/tests/test_on_call_schedule.py | 67 ++++++++++++++++++- .../SchedulesFilters/SchedulesFilters.tsx | 25 +++++++ .../SchedulesFilters.types.ts | 1 + .../src/models/schedule/schedule.ts | 2 +- .../src/pages/schedules/Schedules.tsx | 2 +- 9 files changed, 156 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35579982..098385de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## v1.2.10 (2023-04-13) +### Added + +- Added mine filter to schedules listing + ### Fixed - Fixed a bug in GForm's RemoteSelect where the value for Dropdown could not change diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 3c03545e..a7bb151f 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -27,7 +27,6 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano @pytest.fixture() def schedule_internal_api_setup( make_organization_and_user_with_plugin_token, - make_user_auth_headers, make_slack_channel, make_schedule, ): @@ -377,6 +376,50 @@ def test_get_list_schedules_by_used( assert set(schedule_names) == set(expected_schedule_names) +@pytest.mark.django_db +@pytest.mark.parametrize( + "query_param, expected_schedule_names", + [ + ("?mine=true", ["test_web_schedule"]), + ("?mine=false", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), + ("?mine=null", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), + ("", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), + ], +) +def test_get_list_schedules_by_mine( + schedule_internal_api_setup, + make_user_auth_headers, + make_on_call_shift, + query_param, + expected_schedule_names, +): + user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel = schedule_internal_api_setup + client = APIClient() + + today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + # setup user shift in web schedule + override_data = { + "start": today + timezone.timedelta(hours=22), + "rotation_start": today + timezone.timedelta(hours=22), + "duration": timezone.timedelta(hours=1), + "schedule": web_schedule, + } + override = make_on_call_shift( + organization=user.organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data + ) + override.add_rolling_users([[user]]) + web_schedule.refresh_ical_file() + + url = reverse("api-internal:schedule-list") + query_param + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + assert response.json()["count"] == len(expected_schedule_names) + + schedule_names = [schedule["name"] for schedule in response.json()["results"]] + assert set(schedule_names) == set(expected_schedule_names) + + @pytest.mark.django_db def test_get_list_schedules_pagination_respects_search( schedule_internal_api_setup, diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 76d8d6ac..9916c9d1 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -162,6 +162,7 @@ class ScheduleView( def get_queryset(self, ignore_filtering_by_available_teams=False): is_short_request = self.request.query_params.get("short", "false") == "true" filter_by_type = self.request.query_params.get("type") + mine = BooleanField(allow_null=True).to_internal_value(data=self.request.query_params.get("mine")) used = BooleanField(allow_null=True).to_internal_value(data=self.request.query_params.get("used")) organization = self.request.auth.organization queryset = OnCallSchedule.objects.filter(organization=organization).defer( @@ -178,6 +179,9 @@ class ScheduleView( queryset = queryset.filter().instance_of(SCHEDULE_TYPE_TO_CLASS[filter_by_type]) if used is not None: queryset = queryset.filter(escalation_policies__isnull=not used).distinct() + if mine: + user = self.request.user + queryset = queryset.related_to_user(user) queryset = queryset.order_by("pk") return queryset @@ -475,6 +479,12 @@ class ScheduleView( "href": api_root + "teams/", "global": True, }, + { + "name": "mine", + "type": "boolean", + "display_name": "Mine", + "default": "true", + }, { "name": "used", "type": "boolean", diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 8896811f..cf03c647 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -74,11 +74,12 @@ class OnCallScheduleQuerySet(PolymorphicQuerySet): return get_oncall_users_for_multiple_schedules(self, events_datetime) def related_to_user(self, user): + username_regex = r"SUMMARY:(\[L[0-9]+\] )?{}".format(user.username) return self.filter( - Q(cached_ical_file_primary__contains=user.username) + Q(cached_ical_file_primary__regex=username_regex) | Q(cached_ical_file_primary__contains=user.email) - | Q(cached_ical_file_overrides__contains=user.username) - | Q(cached_ical_file_overrides__contains=user.username), + | Q(cached_ical_file_overrides__regex=username_regex) + | Q(cached_ical_file_overrides__contains=user.email), organization=user.organization, ) diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 83e886a3..00a9b817 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -1199,13 +1199,78 @@ def test_user_related_schedules( override.add_rolling_users([[admin]]) schedule2.refresh_ical_file() - # schedule2 + # schedule3 make_schedule(organization, schedule_class=OnCallScheduleWeb) schedules = OnCallSchedule.objects.related_to_user(admin) assert list(schedules) == [schedule1, schedule2] +@pytest.mark.django_db +def test_user_related_schedules_only_username( + make_organization, + make_user_for_organization, + make_schedule, + make_on_call_shift, +): + organization = make_organization() + # oncall is used as keyword in the ical calendar definition, + # shouldn't be associated to the user + user = make_user_for_organization(organization, username="oncall") + other_user = make_user_for_organization(organization, username="other") + + today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + schedule1 = make_schedule(organization, schedule_class=OnCallScheduleWeb) + shifts = ( + # user, priority, start time (h), duration (seconds) + (user, 1, 0, (24 * 60 * 60) - 1), # r1-1: 0-23:59:59 + ) + for user, priority, start_h, duration in shifts: + data = { + "start": today + timezone.timedelta(hours=start_h), + "rotation_start": today + timezone.timedelta(hours=start_h), + "duration": timezone.timedelta(seconds=duration), + "priority_level": priority, + "frequency": CustomOnCallShift.FREQUENCY_DAILY, + "schedule": schedule1, + } + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users([[user]]) + schedule1.refresh_ical_file() + + schedule2 = make_schedule(organization, schedule_class=OnCallScheduleWeb) + override_data = { + "start": today + timezone.timedelta(hours=22), + "rotation_start": today + timezone.timedelta(hours=22), + "duration": timezone.timedelta(hours=1), + "schedule": schedule2, + } + override = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data + ) + override.add_rolling_users([[user]]) + schedule2.refresh_ical_file() + + # schedule3 + schedule3 = make_schedule(organization, schedule_class=OnCallScheduleWeb) + override_data = { + "start": today + timezone.timedelta(hours=22), + "rotation_start": today + timezone.timedelta(hours=22), + "duration": timezone.timedelta(hours=1), + "schedule": schedule3, + } + override = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data + ) + override.add_rolling_users([[other_user]]) + schedule3.refresh_ical_file() + + schedules = OnCallSchedule.objects.related_to_user(user) + assert list(schedules) == [schedule1, schedule2] + + @pytest.mark.django_db def test_upcoming_shift_for_user( make_organization, diff --git a/grafana-plugin/src/components/SchedulesFilters/SchedulesFilters.tsx b/grafana-plugin/src/components/SchedulesFilters/SchedulesFilters.tsx index f62b3e4b..36b5a388 100644 --- a/grafana-plugin/src/components/SchedulesFilters/SchedulesFilters.tsx +++ b/grafana-plugin/src/components/SchedulesFilters/SchedulesFilters.tsx @@ -24,6 +24,14 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => { }, [value] ); + + const handleMineChange = useCallback( + (mine) => { + onChange({ ...value, mine }); + }, + [value] + ); + const handleStatusChange = useCallback( (used) => { onChange({ ...value, used }); @@ -53,6 +61,23 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => {
+ + + boolean = undefined ) { diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 42edf46e..fce6645a 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -59,7 +59,7 @@ class SchedulesPage extends React.Component