commit
1fb06bf21d
68 changed files with 947 additions and 339 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.3.45 (2023-10-19)
|
||||
|
||||
### Added
|
||||
|
||||
- Use shift data from event object
|
||||
- Update shifts public API to improve web shifts support ([#3165](https://github.com/grafana/oncall/pull/3165))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update ical schedule creation/update to trigger final schedule refresh ([#3156](https://github.com/grafana/oncall/pull/3156))
|
||||
- Handle None role when syncing users from Grafana ([#3147](https://github.com/grafana/oncall/pull/3147))
|
||||
- Polish "Build 'When I am on-call' for web UI" [#2915](https://github.com/grafana/oncall/issues/2915)
|
||||
- Fix iCal schedule incorrect view [#2001](https://github.com/grafana/oncall-private/issues/2001)
|
||||
- Fix rotation name rendering issue [#2324](https://github.com/grafana/oncall/issues/2324)
|
||||
|
||||
### Changed
|
||||
|
||||
- Add user TZ information to next shifts per user endpoint ([#3157](https://github.com/grafana/oncall/pull/3157))
|
||||
|
||||
## v1.3.44 (2023-10-16)
|
||||
|
||||
### Added
|
||||
|
|
@ -24,6 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- Improve alert group deletion API by @vadimkerr ([#3124](https://github.com/grafana/oncall/pull/3124))
|
||||
- Removed Integrations Name max characters limit
|
||||
([#3123](https://github.com/grafana/oncall/pull/3123))
|
||||
- Truncate long table rows (Integration Name/Alert Group) and show tooltip for the truncated content
|
||||
([#3123](https://github.com/grafana/oncall/pull/3123))
|
||||
|
||||
## v1.3.42 (2023-10-04)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ class LegacyAccessControlRole(enum.IntEnum):
|
|||
ADMIN = 0
|
||||
EDITOR = 1
|
||||
VIEWER = 2
|
||||
NONE = 3
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
|
|
@ -99,9 +100,9 @@ RBACObjectPermissionsAttribute = typing.Dict[permissions.BasePermission, typing.
|
|||
|
||||
def get_most_authorized_role(permissions: LegacyAccessControlCompatiblePermissions) -> LegacyAccessControlRole:
|
||||
if not permissions:
|
||||
return LegacyAccessControlRole.VIEWER
|
||||
return LegacyAccessControlRole.NONE
|
||||
|
||||
# ex. Admin is 0, Viewer is 2, thereby min makes sense here
|
||||
# ex. Admin is 0, None is 3, thereby min makes sense here
|
||||
return min({p.fallback_role for p in permissions}, key=lambda r: r.value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from apps.api.serializers.schedule_base import ScheduleBaseSerializer
|
||||
from apps.schedules.models import OnCallScheduleICal
|
||||
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
|
||||
from apps.schedules.tasks import (
|
||||
refresh_ical_final_schedule,
|
||||
schedule_notify_about_empty_shifts_in_schedule,
|
||||
schedule_notify_about_gaps_in_schedule,
|
||||
)
|
||||
from apps.slack.models import SlackChannel, SlackUserGroup
|
||||
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
|
||||
from common.api_helpers.utils import validate_ical_url
|
||||
|
|
@ -37,6 +41,12 @@ class ScheduleICalCreateSerializer(ScheduleICalSerializer):
|
|||
allow_null=True,
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
created_schedule = super().create(validated_data)
|
||||
# for iCal-based schedules we need to refresh final schedule information
|
||||
refresh_ical_final_schedule.apply_async((created_schedule.pk,))
|
||||
return created_schedule
|
||||
|
||||
class Meta:
|
||||
model = OnCallScheduleICal
|
||||
fields = [
|
||||
|
|
@ -80,4 +90,6 @@ class ScheduleICalUpdateSerializer(ScheduleICalCreateSerializer):
|
|||
updated_schedule.check_gaps_for_next_week()
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
|
||||
# for iCal-based schedules we need to refresh final schedule information
|
||||
refresh_ical_final_schedule.apply_async((instance.pk,))
|
||||
return updated_schedule
|
||||
|
|
|
|||
|
|
@ -848,6 +848,7 @@ def test_get_filter_escalation_chain(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_acknowledge_permissions(
|
||||
|
|
@ -883,6 +884,7 @@ def test_alert_group_acknowledge_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_unacknowledge_permissions(
|
||||
|
|
@ -917,6 +919,7 @@ def test_alert_group_unacknowledge_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_resolve_permissions(
|
||||
|
|
@ -951,6 +954,7 @@ def test_alert_group_resolve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_unresolve_permissions(
|
||||
|
|
@ -985,6 +989,7 @@ def test_alert_group_unresolve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_silence_permissions(
|
||||
|
|
@ -1019,6 +1024,7 @@ def test_alert_group_silence_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_unsilence_permissions(
|
||||
|
|
@ -1053,6 +1059,7 @@ def test_alert_group_unsilence_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_attach_permissions(
|
||||
|
|
@ -1087,6 +1094,7 @@ def test_alert_group_attach_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_unattach_permissions(
|
||||
|
|
@ -1121,6 +1129,7 @@ def test_alert_group_unattach_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_list_permissions(
|
||||
|
|
@ -1155,6 +1164,7 @@ def test_alert_group_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_stats_permissions(
|
||||
|
|
@ -1189,6 +1199,7 @@ def test_alert_group_stats_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_bulk_action_permissions(
|
||||
|
|
@ -1221,6 +1232,7 @@ def test_alert_group_bulk_action_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_filters_permissions(
|
||||
|
|
@ -1255,6 +1267,7 @@ def test_alert_group_filters_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_detail_permissions(
|
||||
|
|
@ -1678,6 +1691,7 @@ def test_alert_group_status_field(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_group_preview_template_permissions(
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ def test_integration_search(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_create_permissions(
|
||||
|
|
@ -294,6 +295,7 @@ def test_alert_receive_channel_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_update_permissions(
|
||||
|
|
@ -331,6 +333,7 @@ def test_alert_receive_channel_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_delete_permissions(
|
||||
|
|
@ -363,6 +366,7 @@ def test_alert_receive_channel_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_list_permissions(
|
||||
|
|
@ -394,6 +398,7 @@ def test_alert_receive_channel_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_detail_permissions(
|
||||
|
|
@ -427,6 +432,7 @@ def test_alert_receive_channel_detail_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_send_demo_alert_permissions(
|
||||
|
|
@ -462,6 +468,7 @@ def test_alert_receive_channel_send_demo_alert_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_integration_options_permissions(
|
||||
|
|
@ -493,6 +500,7 @@ def test_alert_receive_channel_integration_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_preview_template_permissions(
|
||||
|
|
@ -606,6 +614,7 @@ def test_alert_receive_channel_preview_template_dynamic_payload(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_change_team_permissions(
|
||||
|
|
@ -669,6 +678,7 @@ def test_alert_receive_channel_change_team(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_counters_permissions(
|
||||
|
|
@ -702,6 +712,7 @@ def test_alert_receive_channel_counters_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_counters_per_integration_permissions(
|
||||
|
|
@ -928,6 +939,7 @@ def test_alert_receive_channel_send_demo_alert_not_enabled(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_get_connected_contact_points_permissions(
|
||||
|
|
@ -965,6 +977,7 @@ def test_alert_receive_channel_get_connected_contact_points_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_get_contact_points_permissions(
|
||||
|
|
@ -998,6 +1011,7 @@ def test_alert_receive_channel_get_contact_points_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_connect_contact_point_permissions(
|
||||
|
|
@ -1035,6 +1049,7 @@ def test_alert_receive_channel_connect_contact_point_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_create_contact_point_permissions(
|
||||
|
|
@ -1072,6 +1087,7 @@ def test_alert_receive_channel_create_contact_point_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_disconnect_contact_point_permissions(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from apps.base.tests.messaging_backend import TestOnlyBackend
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_template_update_permissions(
|
||||
|
|
@ -53,6 +54,7 @@ def test_alert_receive_channel_template_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_template_detail_permissions(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_create_permissions(
|
||||
|
|
@ -48,6 +49,7 @@ def test_channel_filter_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_update_permissions(
|
||||
|
|
@ -87,6 +89,7 @@ def test_channel_filter_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_list_permissions(
|
||||
|
|
@ -122,6 +125,7 @@ def test_channel_filter_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_retrieve_permissions(
|
||||
|
|
@ -157,6 +161,7 @@ def test_channel_filter_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_delete_permissions(
|
||||
|
|
@ -192,6 +197,7 @@ def test_channel_filter_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_move_to_position_permissions(
|
||||
|
|
@ -487,6 +493,7 @@ def test_channel_filter_update_invalid_notification_backends(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_channel_filter_convert_from_regex_to_jinja2(
|
||||
|
|
@ -521,6 +528,9 @@ def test_channel_filter_convert_from_regex_to_jinja2(
|
|||
url = reverse("api-internal:channel_filter-detail", kwargs={"pk": regex_channel_filter.public_primary_key})
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
if role == LegacyAccessControlRole.NONE:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
return
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Check if preview of the filtering term migration is correct
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ def test_delete_custom_button(custom_button_internal_api_setup, make_user_auth_h
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_create_permissions(
|
||||
|
|
@ -311,6 +312,7 @@ def test_custom_button_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_update_permissions(
|
||||
|
|
@ -348,6 +350,7 @@ def test_custom_button_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_list_permissions(
|
||||
|
|
@ -381,6 +384,7 @@ def test_custom_button_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_retrieve_permissions(
|
||||
|
|
@ -414,6 +418,7 @@ def test_custom_button_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_delete_permissions(
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ def test_move_to_position_invalid_index(escalation_policy_internal_api_setup, ma
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_create_permissions(
|
||||
|
|
@ -178,6 +179,7 @@ def test_escalation_policy_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_update_permissions(
|
||||
|
|
@ -219,6 +221,7 @@ def test_escalation_policy_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_list_permissions(
|
||||
|
|
@ -256,6 +259,7 @@ def test_escalation_policy_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_retrieve_permissions(
|
||||
|
|
@ -293,6 +297,7 @@ def test_escalation_policy_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_delete_permissions(
|
||||
|
|
@ -330,6 +335,7 @@ def test_escalation_policy_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_escalation_options_permissions(
|
||||
|
|
@ -367,6 +373,7 @@ def test_escalation_policy_escalation_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_delay_options_permissions(
|
||||
|
|
@ -405,6 +412,7 @@ def test_escalation_policy_delay_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_escalation_policy_move_to_position_permissions(
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ def test_update_integration_heartbeat(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_integration_heartbeat_create_permissions(
|
||||
|
|
@ -218,6 +219,7 @@ def test_integration_heartbeat_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_integration_heartbeat_update_permissions(
|
||||
|
|
@ -257,6 +259,7 @@ def test_integration_heartbeat_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_integration_heartbeat_list_permissions(
|
||||
|
|
@ -292,6 +295,7 @@ def test_integration_heartbeat_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_integration_heartbeat_timeout_options_permissions(
|
||||
|
|
@ -323,6 +327,7 @@ def test_integration_heartbeat_timeout_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_integration_heartbeat_retrieve_permissions(
|
||||
|
|
|
|||
|
|
@ -1213,6 +1213,7 @@ def test_create_on_call_shift_override_in_past(on_call_shift_internal_api_setup,
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_create_permissions(
|
||||
|
|
@ -1245,6 +1246,7 @@ def test_on_call_shift_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_update_permissions(
|
||||
|
|
@ -1292,6 +1294,7 @@ def test_on_call_shift_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_list_permissions(
|
||||
|
|
@ -1323,6 +1326,7 @@ def test_on_call_shift_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_retrieve_permissions(
|
||||
|
|
@ -1366,6 +1370,7 @@ def test_on_call_shift_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_delete_permissions(
|
||||
|
|
@ -1409,6 +1414,7 @@ def test_on_call_shift_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_frequency_options_permissions(
|
||||
|
|
@ -1440,6 +1446,7 @@ def test_on_call_shift_frequency_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_days_options_permissions(
|
||||
|
|
@ -1471,6 +1478,7 @@ def test_on_call_shift_days_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_on_call_shift_preview_permissions(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -10,12 +11,11 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rbac_enabled", [True, False])
|
||||
def test_get_organization_rbac_enabled(
|
||||
make_organization_and_user_with_plugin_token, make_user_auth_headers, rbac_enabled
|
||||
):
|
||||
def test_get_organization_rbac_enabled(make_organization_and_user_with_plugin_token, make_user_auth_headers):
|
||||
is_rbac_enabled = os.getenv("ONCALL_TESTING_RBAC_ENABLED", "True") == "True"
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
organization.is_rbac_permissions_enabled = rbac_enabled
|
||||
# set rbac enabled based on env variable (factories use this value)
|
||||
organization.is_rbac_permissions_enabled = is_rbac_enabled
|
||||
organization.save()
|
||||
|
||||
client = APIClient()
|
||||
|
|
@ -23,7 +23,7 @@ def test_get_organization_rbac_enabled(
|
|||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["rbac_enabled"] == rbac_enabled
|
||||
assert response.json()["rbac_enabled"] == organization.is_rbac_permissions_enabled
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -49,6 +49,7 @@ def test_update_organization_settings(make_organization_and_user_with_plugin_tok
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_organization_retrieve_permissions(
|
||||
|
|
@ -79,6 +80,7 @@ def test_organization_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_organization_update_permissions(
|
||||
|
|
@ -110,6 +112,7 @@ def test_organization_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_organization_get_telegram_verification_code_permissions(
|
||||
|
|
@ -134,6 +137,7 @@ def test_organization_get_telegram_verification_code_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_organization_get_channel_verification_code_permissions(
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ def test_delete_resolution_note(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_resolution_note_create_permissions(
|
||||
|
|
@ -248,6 +249,7 @@ def test_resolution_note_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_resolution_note_update_permissions(
|
||||
|
|
@ -292,6 +294,7 @@ def test_resolution_note_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_resolution_note_delete_permissions(
|
||||
|
|
@ -334,6 +337,7 @@ def test_resolution_note_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_resolution_note_list_permissions(
|
||||
|
|
@ -366,6 +370,7 @@ def test_resolution_note_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_resolution_note_detail_permissions(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_public_api_tokens_retrieve_permissions(
|
||||
|
|
@ -39,6 +40,7 @@ def test_public_api_tokens_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_public_api_tokens_list_permissions(
|
||||
|
|
@ -65,6 +67,7 @@ def test_public_api_tokens_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_public_api_tokens_create_permissions(
|
||||
|
|
@ -96,6 +99,7 @@ def test_public_api_tokens_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_public_api_tokens_delete_permissions(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_schedule_export_token(
|
||||
|
|
@ -52,6 +53,7 @@ def test_get_schedule_export_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_export_token_not_found(
|
||||
|
|
@ -85,6 +87,7 @@ def test_schedule_export_token_not_found(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_create_export_token(
|
||||
|
|
@ -118,6 +121,7 @@ def test_schedule_create_export_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_delete_export_token(
|
||||
|
|
|
|||
|
|
@ -601,25 +601,25 @@ def test_create_ical_schedule(schedule_internal_api_setup, make_user_auth_header
|
|||
user, token, _, _, _, _ = schedule_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:schedule-list")
|
||||
data = {
|
||||
"ical_url_primary": ICAL_URL,
|
||||
"ical_url_overrides": None,
|
||||
"name": "created_ical_schedule",
|
||||
"type": 1,
|
||||
"slack_channel_id": None,
|
||||
"user_group": None,
|
||||
"team": None,
|
||||
"warnings": [],
|
||||
"on_call_now": [],
|
||||
"has_gaps": False,
|
||||
"mention_oncall_next": False,
|
||||
"mention_oncall_start": True,
|
||||
"notify_empty_oncall": 0,
|
||||
"notify_oncall_shift_freq": 1,
|
||||
}
|
||||
with patch(
|
||||
"apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary", return_value=ICAL_URL
|
||||
):
|
||||
data = {
|
||||
"ical_url_primary": ICAL_URL,
|
||||
"ical_url_overrides": None,
|
||||
"name": "created_ical_schedule",
|
||||
"type": 1,
|
||||
"slack_channel_id": None,
|
||||
"user_group": None,
|
||||
"team": None,
|
||||
"warnings": [],
|
||||
"on_call_now": [],
|
||||
"has_gaps": False,
|
||||
"mention_oncall_next": False,
|
||||
"mention_oncall_start": True,
|
||||
"notify_empty_oncall": 0,
|
||||
"notify_oncall_shift_freq": 1,
|
||||
}
|
||||
), patch("apps.schedules.tasks.refresh_ical_final_schedule.apply_async") as mock_refresh_final:
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
# modify initial data by adding id and None for optional fields
|
||||
schedule = OnCallSchedule.objects.get(public_primary_key=response.data["id"])
|
||||
|
|
@ -628,6 +628,8 @@ def test_create_ical_schedule(schedule_internal_api_setup, make_user_auth_header
|
|||
data["enable_web_overrides"] = False
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data == data
|
||||
# check final schedule refresh triggered
|
||||
mock_refresh_final.assert_called_once_with((schedule.pk,))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -736,12 +738,40 @@ def test_update_ical_schedule(schedule_internal_api_setup, make_user_auth_header
|
|||
"type": 1,
|
||||
"team": None,
|
||||
}
|
||||
response = client.put(
|
||||
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
||||
)
|
||||
with patch("apps.schedules.tasks.refresh_ical_final_schedule.apply_async") as mock_refresh_final:
|
||||
response = client.put(
|
||||
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
||||
)
|
||||
updated_instance = OnCallSchedule.objects.get(public_primary_key=ical_schedule.public_primary_key)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert updated_instance.name == "updated_ical_schedule"
|
||||
# check refresh final is not triggered (url unchanged)
|
||||
assert not mock_refresh_final.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_ical_schedule_url(schedule_internal_api_setup, make_user_auth_headers):
|
||||
user, token, _, ical_schedule, _, _ = schedule_internal_api_setup
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:schedule-detail", kwargs={"pk": ical_schedule.public_primary_key})
|
||||
|
||||
updated_url = "another-url"
|
||||
data = {
|
||||
"name": ical_schedule.name,
|
||||
"type": 1,
|
||||
"ical_url_primary": updated_url,
|
||||
}
|
||||
with patch(
|
||||
"apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary", return_value=updated_url
|
||||
), patch("apps.schedules.tasks.refresh_ical_final_schedule.apply_async") as mock_refresh_final:
|
||||
response = client.put(
|
||||
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
||||
)
|
||||
updated_instance = OnCallSchedule.objects.get(public_primary_key=ical_schedule.public_primary_key)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# check refresh final triggered (changing url)
|
||||
mock_refresh_final.assert_called_once_with((updated_instance.pk,))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -1368,7 +1398,15 @@ def test_next_shifts_per_user(
|
|||
)
|
||||
|
||||
tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=1)
|
||||
user_a, user_b, user_c, user_d = (make_user_for_organization(organization, username=i) for i in "ABCD")
|
||||
users = (
|
||||
("A", "Europe/London"),
|
||||
("B", "UTC"),
|
||||
("C", None),
|
||||
("D", "America/Montevideo"),
|
||||
)
|
||||
user_a, user_b, user_c, user_d = (
|
||||
make_user_for_organization(organization, username=i, _timezone=tz) for i, tz in users
|
||||
)
|
||||
# clear users pks <-> organization cache (persisting between tests)
|
||||
memoized_users_in_ical.cache_clear()
|
||||
|
||||
|
|
@ -1426,13 +1464,25 @@ def test_next_shifts_per_user(
|
|||
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)),
|
||||
user_d.public_primary_key: None,
|
||||
user_a.public_primary_key: (
|
||||
tomorrow + timezone.timedelta(hours=15),
|
||||
tomorrow + timezone.timedelta(hours=16),
|
||||
user_a.timezone,
|
||||
),
|
||||
user_b.public_primary_key: (
|
||||
tomorrow + timezone.timedelta(hours=7),
|
||||
tomorrow + timezone.timedelta(hours=12),
|
||||
user_b.timezone,
|
||||
),
|
||||
user_c.public_primary_key: (
|
||||
tomorrow + timezone.timedelta(hours=17),
|
||||
tomorrow + timezone.timedelta(hours=18),
|
||||
user_c.timezone,
|
||||
),
|
||||
user_d.public_primary_key: (None, None, user_d.timezone),
|
||||
}
|
||||
returned_data = {
|
||||
u: (ev["start"], ev["end"]) if ev is not None else None for u, ev in response.data["users"].items()
|
||||
u: (ev.get("start"), ev.get("end"), ev.get("user_timezone")) for u, ev in response.data["users"].items()
|
||||
}
|
||||
assert returned_data == expected
|
||||
|
||||
|
|
@ -1643,6 +1693,7 @@ def test_filter_events_invalid_type(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_create_permissions(
|
||||
|
|
@ -1681,6 +1732,7 @@ def test_schedule_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_update_permissions(
|
||||
|
|
@ -1723,6 +1775,7 @@ def test_schedule_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_list_permissions(
|
||||
|
|
@ -1761,6 +1814,7 @@ def test_schedule_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_retrieve_permissions(
|
||||
|
|
@ -1799,6 +1853,7 @@ def test_schedule_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_delete_permissions(
|
||||
|
|
@ -1837,6 +1892,7 @@ def test_schedule_delete_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_events_permissions(
|
||||
|
|
@ -1875,6 +1931,7 @@ def test_events_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_filter_shift_swaps_permissions(
|
||||
|
|
@ -1913,6 +1970,7 @@ def test_filter_shift_swaps_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_reload_ical_permissions(
|
||||
|
|
@ -1951,6 +2009,7 @@ def test_reload_ical_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_notify_oncall_shift_freq_options_permissions(
|
||||
|
|
@ -1975,6 +2034,7 @@ def test_schedule_notify_oncall_shift_freq_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_notify_empty_oncall_options_permissions(
|
||||
|
|
@ -1999,6 +2059,7 @@ def test_schedule_notify_empty_oncall_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_schedule_mention_options_permissions(
|
||||
|
|
@ -2023,6 +2084,7 @@ def test_schedule_mention_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_current_user_events_permissions(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_set_general_log_channel_permissions(
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ def test_list(ssr_setup, make_user_auth_headers, expand_users):
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_list_permissions(
|
||||
|
|
@ -157,6 +158,7 @@ def test_retrieve(ssr_setup, make_user_auth_headers, expand_users):
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_retrieve_permissions(
|
||||
|
|
@ -277,6 +279,7 @@ def test_create_swap_start_and_swap_end_must_include_time_zone(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_create_permissions(
|
||||
|
|
@ -398,6 +401,7 @@ def test_update_swap_start_and_swap_end_must_include_time_zone(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_update_own_ssr_permissions(ssr_setup, make_user_auth_headers, role, expected_status):
|
||||
|
|
@ -551,6 +555,7 @@ def test_related_shifts(ssr_setup, make_on_call_shift, make_user_auth_headers):
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_partial_update_own_ssr_permissions(ssr_setup, make_user_auth_headers, role, expected_status):
|
||||
|
|
@ -670,6 +675,7 @@ def test_delete(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_delete_own_ssr_permissions(ssr_setup, make_user_auth_headers, role, expected_status):
|
||||
|
|
@ -778,6 +784,7 @@ def test_take_deleted_ssr(ssr_setup, make_user_auth_headers):
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_take_permissions(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_slack_channels_list_permissions(
|
||||
|
|
@ -46,6 +47,7 @@ def test_slack_channels_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_slack_channels_detail_permissions(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from apps.api.permissions import LegacyAccessControlRole
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_slack_settings_permissions(
|
||||
|
|
@ -46,6 +47,7 @@ def test_get_slack_settings_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_update_slack_settings_permissions(
|
||||
|
|
@ -76,6 +78,7 @@ def test_update_slack_settings_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_acknowledge_remind_options_permissions(
|
||||
|
|
@ -106,6 +109,7 @@ def test_get_acknowledge_remind_options_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_unacknowledge_timeout_options_permissions(
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ def test_list_teams_for_non_member(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_list_teams_permissions(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def test_not_authorized(make_organization_and_user_with_plugin_token, make_teleg
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_list_telegram_channels_permissions(
|
||||
|
|
@ -61,6 +62,7 @@ def test_list_telegram_channels_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_telegram_channels_permissions(
|
||||
|
|
@ -87,6 +89,7 @@ def test_get_telegram_channels_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_delete_telegram_channels_permissions(
|
||||
|
|
@ -114,6 +117,7 @@ def test_delete_telegram_channels_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_set_default_telegram_channels_permissions(
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ def test_notification_chain_verbal(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_update_self_permissions(
|
||||
|
|
@ -356,6 +357,7 @@ def test_user_update_self_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_update_other_permissions(
|
||||
|
|
@ -384,6 +386,7 @@ def test_user_update_other_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_list_permissions(
|
||||
|
|
@ -414,6 +417,7 @@ def test_user_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_detail_self_permissions(
|
||||
|
|
@ -444,6 +448,7 @@ def test_user_detail_self_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_detail_other_permissions(
|
||||
|
|
@ -470,6 +475,7 @@ def test_user_detail_other_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_get_own_verification_code(
|
||||
|
|
@ -500,6 +506,7 @@ def test_user_get_own_verification_code(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_get_other_verification_code(
|
||||
|
|
@ -572,6 +579,7 @@ def test_verification_code_provider_exception(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_verify_own_phone(
|
||||
|
|
@ -607,6 +615,7 @@ Tests below are outdated
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_verify_another_phone(
|
||||
|
|
@ -635,6 +644,7 @@ def test_user_verify_another_phone(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_get_own_telegram_verification_code(
|
||||
|
|
@ -659,6 +669,7 @@ def test_user_get_own_telegram_verification_code(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_get_another_telegram_verification_code(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ def test_usergroup_list_without_slack_installed(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_usergroup_permissions(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_get_user_schedule_export_token(
|
||||
|
|
@ -47,6 +48,7 @@ def test_get_user_schedule_export_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_schedule_export_token_not_found(
|
||||
|
|
@ -73,6 +75,7 @@ def test_user_schedule_export_token_not_found(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_schedule_create_export_token(
|
||||
|
|
@ -99,6 +102,7 @@ def test_user_schedule_create_export_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_409_CONFLICT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_409_CONFLICT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_schedule_create_multiple_export_tokens_fails(
|
||||
|
|
@ -130,6 +134,7 @@ def test_user_schedule_create_multiple_export_tokens_fails(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_schedule_delete_export_token(
|
||||
|
|
@ -166,6 +171,7 @@ def test_user_schedule_delete_export_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_cannot_get_another_users_schedule_token(
|
||||
|
|
@ -198,6 +204,7 @@ def test_user_cannot_get_another_users_schedule_token(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_user_cannot_delete_another_users_schedule_token(
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ def test_delete_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_webhook_create_permissions(
|
||||
|
|
@ -322,6 +323,7 @@ def test_webhook_create_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_webhook_update_permissions(
|
||||
|
|
@ -359,6 +361,7 @@ def test_webhook_update_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_webhook_list_permissions(
|
||||
|
|
@ -392,6 +395,7 @@ def test_webhook_list_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_webhook_retrieve_permissions(
|
||||
|
|
@ -425,6 +429,7 @@ def test_webhook_retrieve_permissions(
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_webhook_delete_permissions(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class CurrentOrganizationView(APIView):
|
|||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
rbac_permissions = {
|
||||
"get": [],
|
||||
"get": [RBACPermission.Permissions.OTHER_SETTINGS_READ],
|
||||
"put": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -375,11 +375,14 @@ class ScheduleView(
|
|||
|
||||
events = schedule.final_events(now, datetime_end)
|
||||
|
||||
users = {u.public_primary_key: None for u in schedule.related_users()}
|
||||
# include user TZ information for every user
|
||||
users = {u.public_primary_key: {"user_timezone": u.timezone} for u in schedule.related_users()}
|
||||
added_users = set()
|
||||
for e in events:
|
||||
user = e["users"][0]["pk"] if e["users"] else None
|
||||
if user is not None and users.get(user) is None and e["end"] > now:
|
||||
users[user] = e
|
||||
if user is not None and user not in added_users and e["end"] > now:
|
||||
users[user].update(e)
|
||||
added_users.add(user)
|
||||
|
||||
result = {"users": users}
|
||||
return Response(result, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from rest_framework.filters import SearchFilter
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.api.serializers.slack_channel import SlackChannelSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.slack.models import SlackChannel
|
||||
|
|
@ -12,7 +13,7 @@ from common.api_helpers.paginators import HundredPageSizePaginator
|
|||
|
||||
class SlackChannelView(PublicPrimaryKeyMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
pagination_class = HundredPageSizePaginator
|
||||
|
||||
|
|
@ -21,6 +22,11 @@ class SlackChannelView(PublicPrimaryKeyMixin, mixins.ListModelMixin, mixins.Retr
|
|||
serializer_class = SlackChannelSerializer
|
||||
search_fields = ["name"]
|
||||
|
||||
rbac_permissions = {
|
||||
"list": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
"retrieve": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
organization = self.request.auth.organization
|
||||
slack_team_identity = organization.slack_team_identity
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ class SlackTeamSettingsAPIView(views.APIView):
|
|||
|
||||
class AcknowledgeReminderOptionsAPIView(views.APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
rbac_permissions = {
|
||||
"get": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
choices = []
|
||||
|
|
@ -57,7 +61,11 @@ class AcknowledgeReminderOptionsAPIView(views.APIView):
|
|||
|
||||
class UnAcknowledgeTimeoutOptionsAPIView(views.APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
rbac_permissions = {
|
||||
"get": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
choices = []
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from rest_framework import mixins, viewsets
|
|||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.api.serializers.user_group import UserGroupSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.slack.models import SlackUserGroup
|
||||
|
|
@ -9,9 +10,14 @@ from apps.slack.models import SlackUserGroup
|
|||
|
||||
class UserGroupViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
serializer_class = UserGroupSerializer
|
||||
|
||||
rbac_permissions = {
|
||||
"list": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
"retrieve": [RBACPermission.Permissions.CHATOPS_READ],
|
||||
}
|
||||
|
||||
filter_backends = (SearchFilter,)
|
||||
search_fields = ("name", "handle")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from rest_framework import fields, serializers
|
|||
from apps.schedules.models import CustomOnCallShift
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.custom_fields import (
|
||||
OrganizationFilteredPrimaryKeyRelatedField,
|
||||
RollingUsersField,
|
||||
TeamPrimaryKeyRelatedField,
|
||||
TimeZoneField,
|
||||
|
|
@ -70,6 +71,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
||||
team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team")
|
||||
schedule = OrganizationFilteredPrimaryKeyRelatedField(read_only=True)
|
||||
type = CustomOnCallShiftTypeField()
|
||||
time_zone = TimeZoneField(required=False, allow_null=True)
|
||||
users = UsersFilteredByOrganizationField(queryset=User.objects, required=False)
|
||||
|
|
@ -92,6 +94,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
"id",
|
||||
"organization",
|
||||
"team_id",
|
||||
"schedule",
|
||||
"name",
|
||||
"type",
|
||||
"time_zone",
|
||||
|
|
@ -116,7 +119,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
"source": {"required": False, "write_only": True},
|
||||
}
|
||||
|
||||
PREFETCH_RELATED = ["users"]
|
||||
SELECT_RELATED = ["schedule"]
|
||||
PREFETCH_RELATED = ["schedules", "users"]
|
||||
|
||||
def create(self, validated_data):
|
||||
self._validate_frequency_and_week_start(
|
||||
|
|
@ -244,6 +248,9 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
|
||||
def to_representation(self, instance):
|
||||
result = super().to_representation(instance)
|
||||
if result["schedule"] is None:
|
||||
related_schedules = instance.schedules.all()
|
||||
result["schedule"] = related_schedules[0].public_primary_key if related_schedules else None
|
||||
result["duration"] = int(instance.duration.total_seconds())
|
||||
result["start"] = instance.start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
result["rotation_start"] = instance.rotation_start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
|
@ -377,4 +384,7 @@ class CustomOnCallShiftUpdateSerializer(CustomOnCallShiftSerializer):
|
|||
result = super().update(instance, validated_data)
|
||||
for schedule in instance.schedules.all():
|
||||
instance.start_drop_ical_and_check_schedule_tasks(schedule)
|
||||
if instance.schedule:
|
||||
# web-schedule shifts use FK instead
|
||||
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from apps.public_api.serializers.schedules_base import ScheduleBaseSerializer
|
|||
from apps.schedules.models import OnCallScheduleICal
|
||||
from apps.schedules.tasks import (
|
||||
drop_cached_ical_task,
|
||||
refresh_ical_final_schedule,
|
||||
schedule_notify_about_empty_shifts_in_schedule,
|
||||
schedule_notify_about_gaps_in_schedule,
|
||||
)
|
||||
|
|
@ -32,6 +33,12 @@ class ScheduleICalSerializer(ScheduleBaseSerializer):
|
|||
def validate_ical_url_overrides(self, url):
|
||||
return validate_ical_url(url)
|
||||
|
||||
def create(self, validated_data):
|
||||
created_schedule = super().create(validated_data)
|
||||
# for iCal-based schedules we need to refresh final schedule information
|
||||
refresh_ical_final_schedule.apply_async((created_schedule.pk,))
|
||||
return created_schedule
|
||||
|
||||
|
||||
class ScheduleICalUpdateSerializer(ScheduleICalSerializer):
|
||||
team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team")
|
||||
|
|
@ -70,4 +77,5 @@ class ScheduleICalUpdateSerializer(ScheduleICalSerializer):
|
|||
)
|
||||
schedule_notify_about_empty_shifts_in_schedule.apply_async((instance.pk,))
|
||||
schedule_notify_about_gaps_in_schedule.apply_async((instance.pk,))
|
||||
refresh_ical_final_schedule.apply_async((instance.pk,))
|
||||
return super().update(instance, validated_data)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
|
@ -48,6 +50,43 @@ invalid_field_data_10 = {
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_filter_on_call_shift_schedule(make_organization_and_user_with_token, make_on_call_shift, make_schedule):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
schedule_1 = make_schedule(organization, schedule_class=OnCallScheduleWeb)
|
||||
schedule_2 = make_schedule(organization, schedule_class=OnCallScheduleWeb)
|
||||
start_date = timezone.now().replace(microsecond=0)
|
||||
shifts = []
|
||||
for schedule in (schedule_1, schedule_2):
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=7200),
|
||||
"schedule": schedule,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, **data
|
||||
)
|
||||
on_call_shift.users.add(user)
|
||||
shifts.append(on_call_shift)
|
||||
|
||||
url = reverse("api-public:on_call_shifts-list")
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
expected = sorted([s.public_primary_key for s in shifts])
|
||||
returned = sorted([s["id"] for s in response.json()["results"]])
|
||||
assert returned == expected
|
||||
|
||||
url += f"?schedule_id={schedule_1.public_primary_key}"
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
expected = [shifts[0].public_primary_key]
|
||||
returned = [s["id"] for s in response.json()["results"]]
|
||||
assert returned == expected
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_on_call_shift(make_organization_and_user_with_token, make_on_call_shift, make_schedule):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
|
|
@ -73,6 +112,7 @@ def test_get_on_call_shift(make_organization_and_user_with_token, make_on_call_s
|
|||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": schedule.public_primary_key,
|
||||
"name": on_call_shift.name,
|
||||
"type": "single_event",
|
||||
"time_zone": None,
|
||||
|
|
@ -111,6 +151,7 @@ def test_get_override_on_call_shift(make_organization_and_user_with_token, make_
|
|||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": schedule.public_primary_key,
|
||||
"name": on_call_shift.name,
|
||||
"type": "override",
|
||||
"time_zone": None,
|
||||
|
|
@ -155,6 +196,7 @@ def test_create_on_call_shift(make_organization_and_user_with_token):
|
|||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": None,
|
||||
"name": data["name"],
|
||||
"type": "recurrent_event",
|
||||
"time_zone": None,
|
||||
|
|
@ -206,6 +248,7 @@ def test_create_on_call_shift_using_default_interval(make_organization_and_user_
|
|||
expected = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": None,
|
||||
"name": data["name"],
|
||||
"type": "recurrent_event",
|
||||
"time_zone": None,
|
||||
|
|
@ -282,6 +325,7 @@ def test_create_override_on_call_shift(make_organization_and_user_with_token):
|
|||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": None,
|
||||
"name": data["name"],
|
||||
"type": "override",
|
||||
"time_zone": None,
|
||||
|
|
@ -360,11 +404,13 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal
|
|||
assert on_call_shift.by_day != data_to_update["by_day"]
|
||||
assert len(on_call_shift.users.filter(public_primary_key=user.public_primary_key)) == 0
|
||||
|
||||
response = client.put(url, data=data_to_update, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
with patch("apps.schedules.models.CustomOnCallShift.start_drop_ical_and_check_schedule_tasks") as mock_drop_ical:
|
||||
response = client.put(url, data=data_to_update, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": schedule.public_primary_key,
|
||||
"name": on_call_shift.name,
|
||||
"type": "recurrent_event",
|
||||
"time_zone": None,
|
||||
|
|
@ -389,6 +435,69 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal
|
|||
assert on_call_shift.by_day == data_to_update["by_day"]
|
||||
assert len(on_call_shift.users.filter(public_primary_key=user.public_primary_key)) == 1
|
||||
assert response.data == result
|
||||
mock_drop_ical.assert_called_once_with(schedule)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_on_call_shift_web_schedule(make_organization_and_user_with_token, make_on_call_shift, make_schedule):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
start_date = timezone.now().replace(microsecond=0)
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=7200),
|
||||
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,
|
||||
"interval": 2,
|
||||
"by_day": ["MO", "FR"],
|
||||
"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]])
|
||||
|
||||
url = reverse("api-public:on_call_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key})
|
||||
data_to_update = {
|
||||
"duration": 14400,
|
||||
"by_day": ["MO", "WE", "FR"],
|
||||
}
|
||||
assert int(on_call_shift.duration.total_seconds()) != data_to_update["duration"]
|
||||
assert on_call_shift.by_day != data_to_update["by_day"]
|
||||
|
||||
with patch("apps.schedules.models.CustomOnCallShift.start_drop_ical_and_check_schedule_tasks") as mock_drop_ical:
|
||||
response = client.put(url, data=data_to_update, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
result = {
|
||||
"id": on_call_shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": schedule.public_primary_key,
|
||||
"name": on_call_shift.name,
|
||||
"type": "rolling_users",
|
||||
"time_zone": None,
|
||||
"level": 0,
|
||||
"start": on_call_shift.start.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"rotation_start": on_call_shift.rotation_start.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"duration": data_to_update["duration"],
|
||||
"frequency": "weekly",
|
||||
"interval": on_call_shift.interval,
|
||||
"until": None,
|
||||
"week_start": "SU",
|
||||
"by_day": data_to_update["by_day"],
|
||||
"rolling_users": [[user.public_primary_key]],
|
||||
"start_rotation_from_user_index": None,
|
||||
"by_month": None,
|
||||
"by_monthday": None,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
on_call_shift.refresh_from_db()
|
||||
assert int(on_call_shift.duration.total_seconds()) == data_to_update["duration"]
|
||||
assert on_call_shift.by_day == data_to_update["by_day"]
|
||||
assert response.data == result
|
||||
mock_drop_ical.assert_called_once_with(schedule)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -482,6 +591,7 @@ def test_create_web_override(make_organization_and_user_with_token, make_on_call
|
|||
expected_response = {
|
||||
"id": shift.public_primary_key,
|
||||
"team_id": None,
|
||||
"schedule": None,
|
||||
"name": "test web override",
|
||||
"type": "override",
|
||||
"start": start_str,
|
||||
|
|
|
|||
|
|
@ -393,24 +393,24 @@ def test_update_ical_url_overrides_calendar_schedule(
|
|||
with patch("common.api_helpers.utils.validate_ical_url", return_value=ICAL_URL):
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
result = {
|
||||
"id": schedule.public_primary_key,
|
||||
"team_id": None,
|
||||
"name": schedule.name,
|
||||
"type": "calendar",
|
||||
"time_zone": schedule.time_zone,
|
||||
"on_call_now": [],
|
||||
"shifts": [],
|
||||
"slack": {
|
||||
"channel_id": "SLACKCHANNELID",
|
||||
"user_group_id": None,
|
||||
},
|
||||
"ical_url_overrides": ICAL_URL,
|
||||
"enable_web_overrides": False,
|
||||
}
|
||||
result = {
|
||||
"id": schedule.public_primary_key,
|
||||
"team_id": None,
|
||||
"name": schedule.name,
|
||||
"type": "calendar",
|
||||
"time_zone": schedule.time_zone,
|
||||
"on_call_now": [],
|
||||
"shifts": [],
|
||||
"slack": {
|
||||
"channel_id": "SLACKCHANNELID",
|
||||
"user_group_id": None,
|
||||
},
|
||||
"ical_url_overrides": ICAL_URL,
|
||||
"enable_web_overrides": False,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == result
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -633,7 +633,7 @@ def test_create_ical_schedule(make_organization_and_user_with_token):
|
|||
with patch(
|
||||
"apps.public_api.serializers.schedules_ical.ScheduleICalSerializer.validate_ical_url_primary",
|
||||
return_value=ICAL_URL,
|
||||
):
|
||||
), patch("apps.schedules.tasks.refresh_ical_final_schedule.apply_async") as mock_refresh_final:
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
schedule = OnCallSchedule.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
|
|
@ -653,6 +653,7 @@ def test_create_ical_schedule(make_organization_and_user_with_token):
|
|||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == result
|
||||
mock_refresh_final.assert_called_once_with((schedule.pk,))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -680,7 +681,8 @@ def test_update_ical_schedule(
|
|||
|
||||
assert schedule.name != data["name"]
|
||||
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
with patch("apps.schedules.tasks.refresh_ical_final_schedule.apply_async") as mock_refresh_final:
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
result = {
|
||||
"id": schedule.public_primary_key,
|
||||
|
|
@ -700,6 +702,7 @@ def test_update_ical_schedule(
|
|||
schedule.refresh_from_db()
|
||||
assert schedule.name == data["name"]
|
||||
assert response.json() == result
|
||||
assert not mock_refresh_final.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from django.db.models import Q
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
|
@ -35,7 +36,9 @@ class CustomOnCallShiftView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelV
|
|||
queryset = CustomOnCallShift.objects.filter(organization=self.request.auth.organization)
|
||||
|
||||
if schedule_id:
|
||||
queryset = queryset.filter(schedules__public_primary_key=schedule_id)
|
||||
queryset = queryset.filter(
|
||||
Q(schedules__public_primary_key=schedule_id) | Q(schedule__public_primary_key=schedule_id)
|
||||
)
|
||||
if name:
|
||||
queryset = queryset.filter(name=name)
|
||||
return queryset.order_by("schedules")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from apps.user_management.models import User
|
|||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_reset_slack_integration_permissions(
|
||||
|
|
|
|||
|
|
@ -83,12 +83,13 @@ def _get_payload(action_type="button", **kwargs):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("step_class", ALERT_GROUP_ACTIONS_STEPS)
|
||||
@pytest.mark.parametrize("role", (LegacyAccessControlRole.VIEWER, LegacyAccessControlRole.NONE))
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_actions_unauthorized(
|
||||
step_class, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
step_class, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group, role
|
||||
):
|
||||
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities(
|
||||
role=LegacyAccessControlRole.VIEWER
|
||||
role=role
|
||||
)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.20 on 2023-10-18 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user_management', '0015_auto_20230926_2203'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'ADMIN'), (1, 'EDITOR'), (2, 'VIEWER'), (3, 'NONE')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -88,7 +88,7 @@ class UserManager(models.Manager["User"]):
|
|||
email=user["email"],
|
||||
name=user["name"],
|
||||
username=user["login"],
|
||||
role=LegacyAccessControlRole[user["role"].upper()],
|
||||
role=getattr(LegacyAccessControlRole, user["role"].upper(), LegacyAccessControlRole.NONE),
|
||||
avatar_url=user["avatarUrl"],
|
||||
permissions=user["permissions"],
|
||||
)
|
||||
|
|
@ -120,7 +120,7 @@ class UserManager(models.Manager["User"]):
|
|||
users_to_update = []
|
||||
for user in organization.users.filter(user_id__in=existing_user_ids):
|
||||
grafana_user = grafana_users[user.user_id]
|
||||
g_user_role = LegacyAccessControlRole[grafana_user["role"].upper()]
|
||||
g_user_role = getattr(LegacyAccessControlRole, grafana_user["role"].upper(), LegacyAccessControlRole.NONE)
|
||||
|
||||
if (
|
||||
user.email != grafana_user["email"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||
from django.test import override_settings
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.grafana_plugin.helpers.client import GcomAPIClient, GrafanaAPIClient
|
||||
from apps.user_management.models import Team, User
|
||||
from apps.user_management.sync import check_grafana_incident_is_enabled, cleanup_organization, sync_organization
|
||||
|
|
@ -62,6 +63,43 @@ def test_sync_users_for_organization(make_organization, make_user_for_organizati
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sync_users_for_organization_role_none(make_organization, make_user_for_organization):
|
||||
organization = make_organization(grafana_url="https://test.test")
|
||||
users = tuple(make_user_for_organization(organization, user_id=user_id) for user_id in (1, 2))
|
||||
|
||||
api_users = tuple(
|
||||
{
|
||||
"userId": user_id,
|
||||
"email": "test@test.test",
|
||||
"name": "Test",
|
||||
"login": "test",
|
||||
"role": "None",
|
||||
"avatarUrl": "/test/1234",
|
||||
"permissions": [],
|
||||
}
|
||||
for user_id in (2, 3)
|
||||
)
|
||||
|
||||
User.objects.sync_for_organization(organization, api_users=api_users)
|
||||
|
||||
assert organization.users.count() == 2
|
||||
|
||||
# check that excess users are deleted
|
||||
assert not organization.users.filter(pk=users[0].pk).exists()
|
||||
|
||||
# check that existing users are updated
|
||||
updated_user = organization.users.filter(pk=users[1].pk).first()
|
||||
assert updated_user is not None
|
||||
assert updated_user.role == LegacyAccessControlRole.NONE
|
||||
|
||||
# check that missing users are created
|
||||
created_user = organization.users.filter(user_id=api_users[1]["userId"]).first()
|
||||
assert created_user is not None
|
||||
assert created_user.user_id == api_users[1]["userId"]
|
||||
assert created_user.role == LegacyAccessControlRole.NONE
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sync_teams_for_organization(make_organization, make_team):
|
||||
organization = make_organization()
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ def get_user_permission_role_mapping_from_frontend_plugin_json() -> RoleMapping:
|
|||
plugin_json: PluginJSON = json.load(fp)
|
||||
|
||||
role_mapping: RoleMapping = {
|
||||
LegacyAccessControlRole.NONE: [],
|
||||
LegacyAccessControlRole.VIEWER: [],
|
||||
LegacyAccessControlRole.EDITOR: [],
|
||||
LegacyAccessControlRole.ADMIN: [],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
.rc-table-cell {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
/* works better than break-all, especially for table headers */
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.grecaptcha-badge {
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Make sure if you chage max-width here
|
||||
You also change it in consts.ts
|
||||
*/
|
||||
@media screen and (max-width: 1500px) {
|
||||
.table__email-column {
|
||||
max-width: 175px;
|
||||
}
|
||||
|
||||
.table__email-content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.incident__title-column {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.table__wrap-column {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
.u-flex {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
|
@ -84,10 +84,6 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.u-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.u-break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
@ -129,3 +125,28 @@
|
|||
margin-bottom: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* -----
|
||||
* Overflow
|
||||
*/
|
||||
|
||||
.u-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.overflow-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
white-space: initial;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
interface MatchMediaTooltipProps {
|
||||
placement: 'top' | 'bottom' | 'right' | 'left';
|
||||
content: string;
|
||||
children: JSX.Element;
|
||||
|
||||
maxWidth?: number;
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
const DEBOUNCE_MS = 200;
|
||||
|
||||
export const MatchMediaTooltip: FC<MatchMediaTooltipProps> = ({ minWidth, maxWidth, placement, content, children }) => {
|
||||
const [match, setMatch] = useState<MediaQueryList>(getMatch());
|
||||
|
||||
useEffect(() => {
|
||||
const debouncedResize = debounce(DEBOUNCE_MS, onWindowResize);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (match?.matches) {
|
||||
return (
|
||||
<Tooltip placement={placement} content={content}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
function onWindowResize() {
|
||||
setMatch(getMatch());
|
||||
}
|
||||
|
||||
function getMatch() {
|
||||
if (minWidth && maxWidth) {
|
||||
return window.matchMedia(`(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`);
|
||||
} else if (minWidth) {
|
||||
return window.matchMedia(`(min-width: ${minWidth}px)`);
|
||||
} else if (maxWidth) {
|
||||
return window.matchMedia(`(max-width: ${maxWidth}px)`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import styles from 'assets/style/utils.css';
|
||||
import { TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface TextEllipsisTooltipProps {
|
||||
content: string;
|
||||
queryClassName?: string;
|
||||
placement?: string;
|
||||
className?: string;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
const TextEllipsisTooltip: React.FC<TextEllipsisTooltipProps> = ({
|
||||
queryClassName = TEXT_ELLIPSIS_CLASS,
|
||||
className,
|
||||
content: textContent,
|
||||
placement,
|
||||
children,
|
||||
}) => {
|
||||
const [isEllipsis, setIsEllipsis] = useState(true);
|
||||
const elContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setEllipsis();
|
||||
}, []);
|
||||
|
||||
const elContent = (
|
||||
<div className={cx(className)} ref={elContentRef} onMouseOver={setEllipsis}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isEllipsis) {
|
||||
return (
|
||||
<Tooltip content={textContent} placement={placement as any}>
|
||||
{/* The wrapping div is needed, otherwise the attached ref will be lost when <Tooltip /> mounts */}
|
||||
<div>{elContent}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return elContent;
|
||||
|
||||
function setEllipsis() {
|
||||
const el = elContentRef?.current?.querySelector<HTMLElement>(`.${queryClassName}`);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEllipsis(el.offsetHeight < el.scrollHeight);
|
||||
}
|
||||
};
|
||||
|
||||
export default TextEllipsisTooltip;
|
||||
|
|
@ -17,6 +17,7 @@ interface TooltipBadgeProps {
|
|||
icon?: IconName;
|
||||
customIcon?: React.ReactNode;
|
||||
addPadding?: boolean;
|
||||
placement?;
|
||||
|
||||
onHover?: () => void;
|
||||
}
|
||||
|
|
@ -24,14 +25,25 @@ interface TooltipBadgeProps {
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
||||
const { borderType, text, tooltipTitle, tooltipContent, onHover, addPadding, icon, customIcon, className, ...rest } =
|
||||
props;
|
||||
const {
|
||||
borderType,
|
||||
text,
|
||||
tooltipTitle,
|
||||
tooltipContent,
|
||||
placement,
|
||||
onHover,
|
||||
addPadding,
|
||||
icon,
|
||||
customIcon,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const testId = rest['data-testid'];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="bottom-start"
|
||||
placement={placement || 'bottom-start'}
|
||||
interactive
|
||||
content={
|
||||
<div className={cx('tooltip')}>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import hash from 'object-hash';
|
|||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
|
||||
import { Event, RotationFormLiveParams, Shift, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Event, RotationFormLiveParams, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
|
||||
import RotationTutorial from './RotationTutorial';
|
||||
|
|
@ -34,7 +34,7 @@ interface RotationProps {
|
|||
tutorialParams?: RotationFormLiveParams;
|
||||
simplified?: boolean;
|
||||
filters?: ScheduleFiltersType;
|
||||
getColor?: (shiftId: Shift['id']) => string;
|
||||
getColor?: (event: Event) => string;
|
||||
onSlotClick?: (event: Event) => void;
|
||||
emptyText?: string;
|
||||
showScheduleNameAsSlotTitle?: boolean;
|
||||
|
|
@ -156,7 +156,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
event={event}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
color={propsColor || getColor(event.shift?.pk)}
|
||||
color={propsColor || getColor(event)}
|
||||
handleAddOverride={getAddOverrideClickHandler(event)}
|
||||
handleAddShiftSwap={getAddShiftSwapClickHandler(event)}
|
||||
handleOpenSchedule={getOpenScheduleClickHandler(event)}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
getOverridesFromStore,
|
||||
getShiftsFromStore,
|
||||
} from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift, ShiftSwap, Event } from 'models/schedule/schedule.types';
|
||||
import { Schedule, ShiftSwap, Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -59,7 +59,7 @@ class ScheduleFinal extends Component<ScheduleFinalProps> {
|
|||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (shiftId: Shift['id']) => findColor(shiftId, layers, overrides);
|
||||
const getColor = (event: Event) => findColor(event.shift?.pk, layers, overrides);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ interface ScheduleOverridesProps extends WithStoreProps {
|
|||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
disabled: boolean;
|
||||
disableShiftSwaps: boolean;
|
||||
filters: ScheduleFiltersType;
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
store,
|
||||
shiftIdToShowRotationForm,
|
||||
disabled,
|
||||
disableShiftSwaps,
|
||||
shiftStartToShowOverrideForm: propsShiftStartToShowOverrideForm,
|
||||
shiftEndToShowOverrideForm: propsShiftEndToShowOverrideForm,
|
||||
onShowShiftSwapForm,
|
||||
|
|
@ -112,7 +114,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<HorizontalGroup>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
disabled={disableShiftSwaps}
|
||||
onClick={() => {
|
||||
const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers);
|
||||
const swapStart = closestEvent
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Badge, HorizontalGroup } from '@grafana/ui';
|
||||
import { Badge, Button, HorizontalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -12,9 +12,10 @@ import Text from 'components/Text/Text';
|
|||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import { getColorForSchedule, getPersonalShiftsFromStore } from 'models/schedule/schedule.helpers';
|
||||
import { Shift, Event } from 'models/schedule/schedule.types';
|
||||
import { Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
|
@ -32,24 +33,69 @@ interface SchedulePersonalProps extends WithStoreProps, RouteComponentProps {
|
|||
onSlotClick?: (event: Event) => void;
|
||||
}
|
||||
|
||||
@observer
|
||||
class SchedulePersonal extends Component<SchedulePersonalProps> {
|
||||
componentDidMount() {
|
||||
const { store, startMoment } = this.props;
|
||||
interface SchedulePersonalState {
|
||||
startMoment?: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
@observer
|
||||
class SchedulePersonal extends Component<SchedulePersonalProps, SchedulePersonalState> {
|
||||
state: SchedulePersonalState = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
startMoment: props.startMoment,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<SchedulePersonalProps>): void {
|
||||
const { store, startMoment } = this.props;
|
||||
componentDidMount() {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
if (prevProps.startMoment !== this.props.startMoment) {
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<SchedulePersonalProps>, prevState: Readonly<SchedulePersonalState>): void {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
if (prevProps.currentTimezone !== this.props.currentTimezone) {
|
||||
const oldTimezone = prevProps.currentTimezone;
|
||||
|
||||
this.setState((oldState) => {
|
||||
const wDiff = oldState.startMoment.diff(getStartOfWeek(oldTimezone), 'weeks');
|
||||
|
||||
return { ...oldState, startMoment: getStartOfWeek(this.props.currentTimezone).add(wDiff, 'weeks') };
|
||||
});
|
||||
}
|
||||
|
||||
if (prevState.startMoment !== startMoment) {
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
}
|
||||
}
|
||||
|
||||
handleTodayClick = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.setState({ startMoment: getStartOfWeek(store.currentTimezone) });
|
||||
};
|
||||
|
||||
handleLeftClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(-7, 'day') });
|
||||
};
|
||||
|
||||
handleRightClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(7, 'day') });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userPk, startMoment, currentTimezone, store, onSlotClick } = this.props;
|
||||
const { userPk, currentTimezone, store, onSlotClick } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
|
|
@ -60,18 +106,7 @@ class SchedulePersonal extends Component<SchedulePersonalProps> {
|
|||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (shiftId: Shift['id']) => {
|
||||
const shift = store.scheduleStore.shifts[shiftId];
|
||||
|
||||
if (!shift) {
|
||||
if (shiftId) {
|
||||
store.scheduleStore.updateOncallShift(shiftId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return getColorForSchedule(shift.schedule);
|
||||
};
|
||||
const getColor = (event: Event) => getColorForSchedule(event.schedule?.id);
|
||||
|
||||
const isOncall = store.scheduleStore.onCallNow[userPk];
|
||||
|
||||
|
|
@ -82,12 +117,37 @@ class SchedulePersonal extends Component<SchedulePersonalProps> {
|
|||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={cx('title')}>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {store.userStore.currentUser.name}
|
||||
</Text>
|
||||
{/* @ts-ignore */}
|
||||
{isOncall ? <Badge text="On-call now" color="green" /> : <Badge text="Not on-call now" color="gray" />}
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
|
||||
</Text>
|
||||
|
||||
{isOncall ? (
|
||||
<Badge text="On-call now" color="green" />
|
||||
) : (
|
||||
/* @ts-ignore */
|
||||
<Badge text="Not on-call now" color="gray" />
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text>
|
||||
<Button variant="secondary" size="sm" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" size="sm" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
const base = 60 * 60 * 24 * 7;
|
||||
|
||||
const width = duration / base;
|
||||
const width = Math.max(duration / base, 0);
|
||||
|
||||
const currentMoment = useMemo(() => dayjs(), []);
|
||||
|
||||
|
|
@ -172,6 +172,7 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
content={
|
||||
<ScheduleSlotDetails
|
||||
isShiftSwap
|
||||
title="Shift swap"
|
||||
beneficiaryName={beneficiary?.display_name}
|
||||
user={benefactorStoreUser || beneficiaryStoreUser}
|
||||
benefactorName={benefactor?.display_name}
|
||||
|
|
@ -237,13 +238,17 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
{users.map(({ display_name, pk: userPk, swap_request }) => {
|
||||
const storeUser = store.userStore.items[userPk];
|
||||
|
||||
const { schedule, shift } = event;
|
||||
|
||||
const isCurrentUserSlot = userPk === store.userStore.currentUserPk;
|
||||
const inactive = filters && filters.users.length && !filters.users.includes(userPk);
|
||||
|
||||
const userTitle = storeUser ? getTitle(storeUser) : display_name;
|
||||
const userTitle = showScheduleNameAsSlotTitle ? schedule?.name : storeUser ? getTitle(storeUser) : display_name;
|
||||
|
||||
const isShiftSwap = Boolean(swap_request);
|
||||
|
||||
const title = isShiftSwap ? 'Shift swap' : showScheduleNameAsSlotTitle ? schedule?.name : getShiftName(shift);
|
||||
|
||||
let backgroundColor = color;
|
||||
if (isShiftSwap) {
|
||||
backgroundColor = SHIFT_SWAP_COLOR;
|
||||
|
|
@ -282,7 +287,7 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
key={userPk}
|
||||
content={
|
||||
<ScheduleSlotDetails
|
||||
showScheduleNameAsSlotTitle={showScheduleNameAsSlotTitle}
|
||||
title={title}
|
||||
isShiftSwap={isShiftSwap}
|
||||
beneficiaryName={
|
||||
isShiftSwap ? (swap_request.user ? swap_request.user.display_name : display_name) : undefined
|
||||
|
|
@ -328,7 +333,7 @@ interface ScheduleSlotDetailsProps {
|
|||
beneficiaryName?: string;
|
||||
benefactorName?: string;
|
||||
currentMoment: dayjs.Dayjs;
|
||||
showScheduleNameAsSlotTitle?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
||||
|
|
@ -344,7 +349,7 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
beneficiaryName,
|
||||
benefactorName,
|
||||
currentMoment,
|
||||
showScheduleNameAsSlotTitle,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const { scheduleStore } = useStore();
|
||||
|
|
@ -368,8 +373,6 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
}
|
||||
}, [shift]);
|
||||
|
||||
const title = isShiftSwap ? 'Shift swap' : showScheduleNameAsSlotTitle ? schedule?.name : getShiftName(shift);
|
||||
|
||||
// const onCallNow = schedule?.on_call_now;
|
||||
// const isOncall = Boolean(storeUser && onCallNow && onCallNow.some((onCallUser) => storeUser.pk === onCallUser.pk));
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface TeamNameProps {
|
||||
team: GrafanaTeam;
|
||||
className?: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
const TeamName = observer((props: TeamNameProps) => {
|
||||
const { team, size = 'medium' } = props;
|
||||
const { team, size = 'medium', className } = props;
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -26,7 +27,7 @@ const TeamName = observer((props: TeamNameProps) => {
|
|||
return <Badge text={team.name} color={'blue'} tooltip={'Resource is not assigned to any team (ex General team)'} />;
|
||||
}
|
||||
return (
|
||||
<Text type="secondary" size={size}>
|
||||
<Text type="secondary" size={size} className={className}>
|
||||
<Avatar size="small" src={team.avatar_url} className={cx('avatar')} />
|
||||
<Tooltip placement="top" content={'Resource is assigned to ' + team.name}>
|
||||
<Text type="primary">{team.name}</Text>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const fillGaps = (events: Event[]) => {
|
|||
return newEvents;
|
||||
};
|
||||
|
||||
export const splitToShiftsAndFillGaps = (events: Event[]) => {
|
||||
export const splitToShifts = (events: Event[]) => {
|
||||
const shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }> = [];
|
||||
|
||||
for (const [_i, event] of events.entries()) {
|
||||
|
|
@ -77,13 +77,20 @@ export const splitToShiftsAndFillGaps = (events: Event[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
shifts.forEach((shift) => {
|
||||
shift.events = fillGaps(shift.events);
|
||||
});
|
||||
|
||||
return shifts;
|
||||
};
|
||||
|
||||
export const fillGapsInShifts = (shifts: ShiftEvents[]) => {
|
||||
return shifts.map((shift) => ({
|
||||
...shift,
|
||||
events: fillGaps(shift.events),
|
||||
}));
|
||||
};
|
||||
|
||||
export const enrichEventsWithScheduleData = (events: Event[], schedule: Partial<Schedule>) => {
|
||||
return events.map((event) => ({ ...event, schedule }));
|
||||
};
|
||||
|
||||
export const getPersonalShiftsFromStore = (
|
||||
store: RootStore,
|
||||
userPk: User['pk'],
|
||||
|
|
@ -102,6 +109,44 @@ export const getShiftsFromStore = (
|
|||
: (store.scheduleStore.events[scheduleId]?.['final']?.[getFromString(startMoment)] as any);
|
||||
};
|
||||
|
||||
export const unFlattenShiftEvents = (shifts: ShiftEvents[]) => {
|
||||
for (let i = 0; i < shifts.length; i++) {
|
||||
const shift = shifts[i];
|
||||
|
||||
for (let j = 0; j < shift.events.length - 1; j++) {
|
||||
for (let k = j + 1; k < shift.events.length; k++) {
|
||||
const event1 = shift.events[j];
|
||||
const event2 = shift.events[k];
|
||||
|
||||
const event1Start = dayjs(event1.start);
|
||||
const event1End = dayjs(event1.end);
|
||||
|
||||
const event2Start = dayjs(event2.start);
|
||||
const event2End = dayjs(event2.end);
|
||||
|
||||
if (
|
||||
(event1Start.isBefore(event2Start) && event1End.isAfter(event2Start)) ||
|
||||
(event1End.isAfter(event2End) && event1Start.isBefore(event2End))
|
||||
) {
|
||||
const firstEvent = event1Start.isBefore(event2Start) ? event1 : event2;
|
||||
const secondEvent = firstEvent === event1 ? event2 : event1;
|
||||
|
||||
const oldShift = { ...shift, events: shift.events.filter((event) => event !== secondEvent) };
|
||||
|
||||
const newShift = { ...shift, events: [secondEvent] };
|
||||
|
||||
shifts[i] = oldShift;
|
||||
shifts.push(newShift);
|
||||
|
||||
return unFlattenShiftEvents(shifts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shifts;
|
||||
};
|
||||
|
||||
export const flattenShiftEvents = (shifts: ShiftEvents[]) => {
|
||||
if (!shifts) {
|
||||
return undefined;
|
||||
|
|
@ -241,9 +286,7 @@ export const getOverridesFromStore = (
|
|||
: (store.scheduleStore.events[scheduleId]?.['override']?.[getFromString(startMoment)] as ShiftEvents[]);
|
||||
};
|
||||
|
||||
export const splitToLayers = (
|
||||
shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }>
|
||||
) => {
|
||||
export const splitToLayers = (shifts: ShiftEvents[]) => {
|
||||
return shifts
|
||||
.reduce((memo, shift) => {
|
||||
let layer = memo.find((level) => level.priority === shift.priority);
|
||||
|
|
@ -395,7 +438,7 @@ export const getOverrideColor = (rotationIndex: number) => {
|
|||
return OVERRIDE_COLORS[normalizedRotationIndex];
|
||||
};
|
||||
|
||||
export const getShiftName = (shift: Shift) => {
|
||||
export const getShiftName = (shift: Partial<Shift>) => {
|
||||
if (!shift) {
|
||||
return '';
|
||||
}
|
||||
|
|
@ -408,5 +451,5 @@ export const getShiftName = (shift: Shift) => {
|
|||
return 'Override';
|
||||
}
|
||||
|
||||
return `[L${shift.priority_level}] Rotation`;
|
||||
return 'Rotation';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ import { SelectOption } from 'state/types';
|
|||
|
||||
import {
|
||||
createShiftSwapEventFromShiftSwap,
|
||||
enrichEventsWithScheduleData,
|
||||
enrichLayers,
|
||||
enrichOverrides,
|
||||
fillGapsInShifts,
|
||||
flattenShiftEvents,
|
||||
getFromString,
|
||||
splitToLayers,
|
||||
splitToShiftsAndFillGaps,
|
||||
splitToShifts,
|
||||
unFlattenShiftEvents,
|
||||
} from './schedule.helpers';
|
||||
import {
|
||||
Rotation,
|
||||
|
|
@ -34,7 +37,7 @@ import {
|
|||
|
||||
export class ScheduleStore extends BaseStore {
|
||||
@observable
|
||||
searchResult: { count?: number; results?: Array<Schedule['id']> } = {};
|
||||
searchResult: { page_size?: number; count?: number; results?: Array<Schedule['id']> } = {};
|
||||
|
||||
@observable.shallow
|
||||
items: { [id: string]: Schedule } = {};
|
||||
|
|
@ -137,7 +140,7 @@ export class ScheduleStore extends BaseStore {
|
|||
shouldUpdateFn: () => boolean = undefined
|
||||
) {
|
||||
const filters = typeof f === 'string' ? { search: f } : f;
|
||||
const { count, results } = await makeRequest(this.path, {
|
||||
const { page_size, count, results } = await makeRequest(this.path, {
|
||||
method: 'GET',
|
||||
params: { ...filters, page },
|
||||
});
|
||||
|
|
@ -157,6 +160,7 @@ export class ScheduleStore extends BaseStore {
|
|||
),
|
||||
};
|
||||
this.searchResult = {
|
||||
page_size,
|
||||
count,
|
||||
results: results.map((item: Schedule) => item.id),
|
||||
};
|
||||
|
|
@ -193,6 +197,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return undefined;
|
||||
}
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
count: this.searchResult.count,
|
||||
results: this.searchResult.results?.map((scheduleId: Schedule['id']) => this.items[scheduleId]),
|
||||
};
|
||||
|
|
@ -287,7 +292,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.rotationPreview = { ...this.rotationPreview, [fromString]: layers };
|
||||
}
|
||||
|
||||
this.finalPreview = { ...this.finalPreview, [fromString]: splitToShiftsAndFillGaps(response.final) };
|
||||
this.finalPreview = { ...this.finalPreview, [fromString]: fillGapsInShifts(splitToShifts(response.final)) };
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -450,7 +455,9 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
|
||||
const fromString = getFromString(startMoment);
|
||||
const shifts = splitToShiftsAndFillGaps(response.events);
|
||||
const shiftsRaw = splitToShifts(response.events);
|
||||
const shiftsUnflattened = unFlattenShiftEvents(shiftsRaw);
|
||||
const shifts = fillGapsInShifts(shiftsUnflattened);
|
||||
const layers = type === 'rotation' ? splitToLayers(shifts) : undefined;
|
||||
|
||||
this.events = {
|
||||
|
|
@ -535,7 +542,7 @@ export class ScheduleStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9) {
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
const dayBefore = startMoment.subtract(1, 'day');
|
||||
|
|
@ -548,8 +555,8 @@ export class ScheduleStore extends BaseStore {
|
|||
},
|
||||
});
|
||||
|
||||
const shiftEventsList = schedules.reduce((acc, schedule) => {
|
||||
return [...acc, ...splitToShiftsAndFillGaps(schedule.events)];
|
||||
const shiftEventsList = schedules.reduce((acc, { events, id, name }) => {
|
||||
return [...acc, ...fillGapsInShifts(splitToShifts(enrichEventsWithScheduleData(events, { id, name })))];
|
||||
}, []);
|
||||
|
||||
const shiftEventsListFlattened = flattenShiftEvents(shiftEventsList);
|
||||
|
|
@ -562,9 +569,12 @@ export class ScheduleStore extends BaseStore {
|
|||
},
|
||||
};
|
||||
|
||||
this.onCallNow = {
|
||||
...this.onCallNow,
|
||||
[userPk]: is_oncall,
|
||||
};
|
||||
if (isUpdateOnCallNow) {
|
||||
// since current endpoint works incorrectly we are waiting for https://github.com/grafana/oncall/issues/3164
|
||||
this.onCallNow = {
|
||||
...this.onCallNow,
|
||||
[userPk]: is_oncall,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export interface Event {
|
|||
is_gap: boolean;
|
||||
missing_users: Array<{ display_name: User['username']; pk: User['pk'] }>;
|
||||
priority_level: number;
|
||||
shift: { pk: Shift['id'] | null };
|
||||
shift: Pick<Shift, 'name' | 'type'> & { pk: string };
|
||||
source: string;
|
||||
start: string;
|
||||
users: Array<{
|
||||
|
|
@ -104,6 +104,7 @@ export interface Event {
|
|||
}>;
|
||||
is_override: boolean;
|
||||
|
||||
schedule?: Partial<Schedule>; // populated by frontend for personal schedule to display schedule name instead of user name
|
||||
shiftSwapId?: ShiftSwap['id']; // if event is acually shift swap request (filled out by frontend)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { Button, HorizontalGroup, IconButton, Tooltip, VerticalGroup } from '@gr
|
|||
import cn from 'classnames/bind';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import { MatchMediaTooltip } from 'components/MatchMediaTooltip/MatchMediaTooltip';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Alert as AlertType, Alert, IncidentStatus } from 'models/alertgroup/alertgroup.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
|
|
@ -15,7 +15,7 @@ import { SilenceButtonCascader } from 'pages/incidents/parts/SilenceButtonCascad
|
|||
import { move } from 'state/helpers';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { TABLE_COLUMN_MAX_WIDTH } from 'utils/consts';
|
||||
import { TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Incident.module.scss';
|
||||
|
||||
|
|
@ -78,14 +78,13 @@ export function renderRelatedUsers(incident: Alert, isFull = false) {
|
|||
}
|
||||
|
||||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }} wrap={false} className="table__email-content">
|
||||
<Text type="secondary">
|
||||
<Avatar size="small" src={user.avatar} />{' '}
|
||||
<MatchMediaTooltip placement="top" content={user.username} maxWidth={TABLE_COLUMN_MAX_WIDTH}>
|
||||
<span>{user.username}</span>
|
||||
</MatchMediaTooltip>{' '}
|
||||
{badge}
|
||||
</Text>
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }} wrap={false}>
|
||||
<TextEllipsisTooltip placement="top" content={user.username}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
|
||||
<Avatar size="small" src={user.avatar} /> <span className={cx('break-word')}>{user.username}</span>
|
||||
<span className={cx('user-badge')}>{badge}</span>
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</PluginLink>
|
||||
);
|
||||
}
|
||||
|
|
@ -117,32 +116,30 @@ export function renderRelatedUsers(incident: Alert, isFull = false) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={'table__email-column'}>
|
||||
<VerticalGroup spacing="xs">
|
||||
{visibleUsers.map(renderUser)}
|
||||
{Boolean(otherUsers.length) && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
<>
|
||||
{otherUsers.map((user, index) => (
|
||||
<>
|
||||
{index ? ', ' : ''}
|
||||
{renderUser(user)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Text type="secondary" underline size="small">
|
||||
+{otherUsers.length} user{otherUsers.length > 1 ? 's' : ''}
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
<VerticalGroup spacing="xs">
|
||||
{visibleUsers.map(renderUser)}
|
||||
{Boolean(otherUsers.length) && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
<>
|
||||
{otherUsers.map((user, index) => (
|
||||
<>
|
||||
{index ? ', ' : ''}
|
||||
{renderUser(user)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Text type="secondary" underline size="small">
|
||||
+{otherUsers.length} user{otherUsers.length > 1 ? 's' : ''}
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,3 +196,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { ReactElement, SyntheticEvent } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -15,6 +15,7 @@ import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
|||
import ManualAlertGroup from 'components/ManualAlertGroup/ManualAlertGroup';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import Tutorial from 'components/Tutorial/Tutorial';
|
||||
import { TutorialStep } from 'components/Tutorial/Tutorial.types';
|
||||
import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilters.types';
|
||||
|
|
@ -27,7 +28,7 @@ import { PageProps, WithStoreProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Incidents.module.scss';
|
||||
import { IncidentDropdown } from './parts/IncidentDropdown';
|
||||
|
|
@ -463,7 +464,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
|
||||
const columns = [
|
||||
{
|
||||
width: '5%',
|
||||
width: '140px',
|
||||
title: 'Status',
|
||||
key: 'time',
|
||||
render: withSkeleton(this.renderStatus),
|
||||
|
|
@ -553,7 +554,13 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
};
|
||||
|
||||
renderId(record: AlertType) {
|
||||
return <Text type="secondary">#{record.inside_organization_number}</Text>;
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={`#${record.inside_organization_number}`}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
|
||||
#{record.inside_organization_number}
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle = (record: AlertType) => {
|
||||
|
|
@ -565,25 +572,25 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
const { incidentsItemsPerPage, incidentsCursor } = store.alertGroupStore;
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="none" justify="center">
|
||||
<div className={'table__wrap-column'}>
|
||||
<PluginLink
|
||||
query={{
|
||||
page: 'alert-groups',
|
||||
id: record.pk,
|
||||
cursor: incidentsCursor,
|
||||
perpage: incidentsItemsPerPage,
|
||||
start,
|
||||
...query,
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="top" content={record.render_for_web.title}>
|
||||
<span>{record.render_for_web.title}</span>
|
||||
</Tooltip>
|
||||
</PluginLink>
|
||||
{Boolean(record.dependent_alert_groups.length) && ` + ${record.dependent_alert_groups.length} attached`}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
<div>
|
||||
<TextEllipsisTooltip placement="top" content={record.render_for_web.title}>
|
||||
<Text type="link" size="medium" className={cx('overflow-parent')}>
|
||||
<PluginLink
|
||||
query={{
|
||||
page: 'alert-groups',
|
||||
id: record.pk,
|
||||
cursor: incidentsCursor,
|
||||
perpage: incidentsItemsPerPage,
|
||||
start,
|
||||
...query,
|
||||
}}
|
||||
>
|
||||
<Text className={cx(TEXT_ELLIPSIS_CLASS)}>{record.render_for_web.title}</Text>
|
||||
</PluginLink>
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
{Boolean(record.dependent_alert_groups.length) && ` + ${record.dependent_alert_groups.length} attached`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -598,10 +605,14 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
const integration = alertReceiveChannelStore.getIntegration(record.alert_receive_channel);
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="sm">
|
||||
<TextEllipsisTooltip
|
||||
className={cx('u-flex', 'u-flex-gap-xs', 'overflow-parent')}
|
||||
placement="top"
|
||||
content={record?.alert_receive_channel?.verbal_name || ''}
|
||||
>
|
||||
<IntegrationLogo integration={integration} scale={0.1} />
|
||||
<Emoji text={record.alert_receive_channel?.verbal_name || ''} />
|
||||
</HorizontalGroup>
|
||||
<Emoji className={cx(TEXT_ELLIPSIS_CLASS)} text={record.alert_receive_channel?.verbal_name || ''} />
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -631,7 +642,11 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
}
|
||||
|
||||
renderTeam(record: AlertType, teams: any) {
|
||||
return <TeamName team={teams[record.team]} />;
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={teams[record.team]?.name}>
|
||||
<TeamName className={TEXT_ELLIPSIS_CLASS} team={teams[record.team]} />
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
getOnActionButtonClick = (incidentId: string, action: AlertAction): ((e: SyntheticEvent) => Promise<void>) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
width: 180px;
|
||||
}
|
||||
|
||||
.title {
|
||||
.heartbeat-badge {
|
||||
padding: 4px 10px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.integrations-header {
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
}
|
||||
|
|
@ -15,11 +20,6 @@
|
|||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.heartbeat-badge {
|
||||
padding: 4px 10px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.integrations-actionsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -44,4 +44,4 @@
|
|||
&:hover {
|
||||
background: var(--cards-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
|
||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||
|
|
@ -34,14 +35,13 @@ import { withMobXProviderContext } from 'state/withStore';
|
|||
import { openNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PAGE } from 'utils/consts';
|
||||
import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Integrations.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
const MAX_LINE_LENGTH = 40;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: Filters;
|
||||
|
|
@ -227,16 +227,11 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
...query,
|
||||
}}
|
||||
>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji
|
||||
className={cx('title')}
|
||||
text={
|
||||
item.verbal_name?.length > MAX_LINE_LENGTH
|
||||
? item.verbal_name?.substring(0, MAX_LINE_LENGTH) + '...'
|
||||
: item.verbal_name
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<TextEllipsisTooltip placement="top" content={item.verbal_name}>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji className={cx('title', TEXT_ELLIPSIS_CLASS)} text={item.verbal_name} />
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</PluginLink>
|
||||
);
|
||||
};
|
||||
|
|
@ -278,6 +273,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<PluginLink query={{ page: 'incidents', integration: item.id }} className={cx('alertsInfoText')}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
placement="top"
|
||||
text={alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count}
|
||||
tooltipTitle=""
|
||||
tooltipContent={
|
||||
|
|
@ -298,6 +294,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
icon="link"
|
||||
text={`${connectedEscalationsChainsCount}/${routesCounter}`}
|
||||
tooltipContent={undefined}
|
||||
placement="top"
|
||||
tooltipTitle={
|
||||
connectedEscalationsChainsCount +
|
||||
' connected escalation chain' +
|
||||
|
|
@ -328,6 +325,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<TooltipBadge
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
placement="top"
|
||||
borderType={heartbeatStatus ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal}`}
|
||||
|
|
@ -347,6 +345,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<TooltipBadge
|
||||
borderType="primary"
|
||||
icon="pause"
|
||||
placement="top"
|
||||
text={IntegrationHelper.getMaintenanceText(item.maintenance_till)}
|
||||
tooltipTitle={IntegrationHelper.getMaintenanceText(item.maintenance_till, maintenanceMode)}
|
||||
tooltipContent={undefined}
|
||||
|
|
@ -359,7 +358,11 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
}
|
||||
|
||||
renderTeam(item: AlertReceiveChannel, teams: any) {
|
||||
return <TeamName team={teams[item.team]} />;
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={teams[item.team]?.name}>
|
||||
<TeamName className={TEXT_ELLIPSIS_CLASS} team={teams[item.team]} />
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtons = (item: AlertReceiveChannel) => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm';
|
||||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
|
|
@ -36,7 +37,7 @@ import { PageProps, WithStoreProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openErrorNotification, openNotification } from 'utils';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './OutgoingWebhooks.module.scss';
|
||||
import { WebhookFormActionType } from './OutgoingWebhooks.types';
|
||||
|
|
@ -246,7 +247,11 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
};
|
||||
|
||||
renderTeam(record: OutgoingWebhook, teams: any) {
|
||||
return <TeamName team={teams[record.team]} />;
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={teams[record.team]?.name}>
|
||||
<TeamName className={TEXT_ELLIPSIS_CLASS} team={teams[record.team]} />
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionButtons = (record: OutgoingWebhook) => {
|
||||
|
|
@ -342,9 +347,13 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
|
||||
renderUrl(url: string) {
|
||||
return (
|
||||
<div className="u-break-word">
|
||||
<span>{url}</span>
|
||||
</div>
|
||||
<TextEllipsisTooltip content={url} placement="top">
|
||||
<CopyToClipboard text={url} onCopy={() => openNotification('URL has been copied')}>
|
||||
<Text type="link" className={cx(TEXT_ELLIPSIS_CLASS, 'line-clamp-3')}>
|
||||
{url}
|
||||
</Text>
|
||||
</CopyToClipboard>
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftIdToShowRotationForm ||
|
||||
shiftSwapIdToShowForm;
|
||||
|
||||
const disabledShiftSwaps =
|
||||
!isUserActionAllowed(UserActions.SchedulesWrite) ||
|
||||
!!shiftIdToShowOverridesForm ||
|
||||
shiftIdToShowRotationForm ||
|
||||
shiftSwapIdToShowForm;
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="schedule" pageName="schedules">
|
||||
{() => (
|
||||
|
|
@ -314,6 +320,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabledOverrideForm}
|
||||
disableShiftSwaps={disabledShiftSwaps}
|
||||
shiftStartToShowOverrideForm={shiftStartToShowOverrideForm}
|
||||
shiftEndToShowOverrideForm={shiftEndToShowOverrideForm}
|
||||
onShowShiftSwapForm={!shiftSwapIdToShowForm ? this.handleShowShiftSwapForm : undefined}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,3 @@
|
|||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.schedules__user-on-call {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import qs from 'query-string';
|
|||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import { MatchMediaTooltip } from 'components/MatchMediaTooltip/MatchMediaTooltip';
|
||||
import NewScheduleSelector from 'components/NewScheduleSelector/NewScheduleSelector';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Table from 'components/Table/Table';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
|
||||
|
|
@ -25,7 +25,7 @@ import SchedulePersonal from 'containers/Rotations/SchedulePersonal';
|
|||
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
||||
import { Schedule } from 'models/schedule/schedule.types';
|
||||
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
||||
|
|
@ -33,13 +33,13 @@ import { WithStoreProps, PageProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PAGE, PLUGIN_ROOT, TABLE_COLUMN_MAX_WIDTH } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Schedules.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
const PAGE_SIZE_DEFAULT = 15;
|
||||
|
||||
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const { grafanaTeamStore } = store;
|
||||
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit, page, startMoment } = this.state;
|
||||
|
||||
const { results, count } = store.scheduleStore.getSearchResult();
|
||||
const { results, count, page_size } = store.scheduleStore.getSearchResult();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -162,9 +162,6 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
userPk={store.userStore.currentUserPk}
|
||||
currentTimezone={store.currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onSlotClick={(...rest) => {
|
||||
console.log(rest);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('schedules__filters-container')}>
|
||||
|
|
@ -179,7 +176,11 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
columns={columns}
|
||||
data={results}
|
||||
loading={!results}
|
||||
pagination={{ page, total: Math.ceil((count || 0) / ITEMS_PER_PAGE), onChange: this.handlePageChange }}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / (page_size || PAGE_SIZE_DEFAULT)),
|
||||
onChange: this.handlePageChange,
|
||||
}}
|
||||
rowKey="id"
|
||||
expandable={{
|
||||
expandedRowKeys: expandedRowKeys,
|
||||
|
|
@ -234,9 +235,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
handleCreateSchedule = (data: Schedule) => {
|
||||
const { history, query } = this.props;
|
||||
|
||||
if (data.type === ScheduleType.API) {
|
||||
history.push(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`);
|
||||
}
|
||||
history.push(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`);
|
||||
};
|
||||
|
||||
handleExpandRow = (expanded: boolean, data: Schedule) => {
|
||||
|
|
@ -369,14 +368,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
{item.on_call_now.map((user) => {
|
||||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }} className="table__email-content">
|
||||
<div className={cx('schedules__user-on-call')}>
|
||||
<div>
|
||||
<Avatar size="medium" src={user.avatar} />
|
||||
</div>
|
||||
<MatchMediaTooltip placement="top" content={user.username} maxWidth={TABLE_COLUMN_MAX_WIDTH}>
|
||||
<span className="table__email-content">{user.username}</span>
|
||||
</MatchMediaTooltip>
|
||||
</div>
|
||||
<HorizontalGroup>
|
||||
<TextEllipsisTooltip placement="top" content={user.username}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
|
||||
<Avatar size="small" src={user.avatar} />{' '}
|
||||
<span className={cx('break-word')}>{user.username}</span>
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</HorizontalGroup>
|
||||
</PluginLink>
|
||||
);
|
||||
})}
|
||||
|
|
@ -455,7 +454,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const { store } = this.props;
|
||||
const { page, startMoment } = this.state;
|
||||
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);
|
||||
|
||||
// For removal we need to check if count is 1
|
||||
// which means we should change the page to the previous one
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ dayjs.extend(customParseFormat);
|
|||
import 'assets/style/vars.css';
|
||||
import 'assets/style/global.css';
|
||||
import 'assets/style/utils.css';
|
||||
import 'assets/style/responsive.css';
|
||||
|
||||
import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers';
|
||||
import PluginSetup from './PluginSetup';
|
||||
|
|
|
|||
|
|
@ -41,9 +41,6 @@ export const FARO_ENDPOINT_PROD =
|
|||
export const DOCS_SLACK_SETUP = 'https://grafana.com/docs/oncall/latest/open-source/#slack-setup';
|
||||
export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/notify/telegram/';
|
||||
|
||||
// Make sure if you chage max-width here you also change it in responsive.css
|
||||
export const TABLE_COLUMN_MAX_WIDTH = 1500;
|
||||
|
||||
export const generateAssignToTeamInputDescription = (objectName: string): string =>
|
||||
`Assigning to a team allows you to filter ${objectName} and configure their visibility. Go to OnCall -> Settings -> Team and Access Settings for more details.`;
|
||||
|
||||
|
|
@ -54,3 +51,5 @@ export enum PAGE {
|
|||
Webhooks = 'webhooks',
|
||||
Schedules = 'schedules',
|
||||
}
|
||||
|
||||
export const TEXT_ELLIPSIS_CLASS = 'overflow-child';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue