From ccae9d86b3f0db44cc655781f470e06ad4e56137 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 19 Jan 2023 18:51:57 +0000 Subject: [PATCH] Add an ability to use an escalation chain for direct paging (#1161) # What this PR does Adds an ability to page an escalation chain for a newly created direct paging alert group using the internal API. Also [adds a forgotten migration](https://github.com/grafana/oncall/pull/1161/commits/32fc44e744a0208f07e4f6bc7c1489c273de56d4) related to the direct paging backend. Related to https://github.com/grafana/oncall/issues/823 ## Checklist - [x] Tests updated - [ ] Documentation added (N/A) - [ ] `CHANGELOG.md` updated (N/A) --- .../0008_alter_alertgrouplogrecord_type.py | 18 +++++++ engine/apps/api/serializers/paging.py | 35 +++++++++++--- engine/apps/api/tests/test_paging.py | 48 ++++++++++++++++++- engine/apps/api/views/paging.py | 1 + 4 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 engine/apps/alerts/migrations/0008_alter_alertgrouplogrecord_type.py diff --git a/engine/apps/alerts/migrations/0008_alter_alertgrouplogrecord_type.py b/engine/apps/alerts/migrations/0008_alter_alertgrouplogrecord_type.py new file mode 100644 index 00000000..f805f599 --- /dev/null +++ b/engine/apps/alerts/migrations/0008_alter_alertgrouplogrecord_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-19 18:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0007_populate_web_title_cache'), + ] + + operations = [ + migrations.AlterField( + model_name='alertgrouplogrecord', + name='type', + field=models.IntegerField(choices=[(0, 'Acknowledged'), (1, 'Unacknowledged'), (2, 'Invite'), (3, 'Stop invitation'), (4, 'Re-invite'), (5, 'Escalation triggered'), (6, 'Invitation triggered'), (16, 'Escalation finished'), (7, 'Silenced'), (15, 'Unsilenced'), (8, 'Attached'), (9, 'Unattached'), (10, 'Custom button triggered'), (11, 'Unacknowledged by timeout'), (12, 'Failed attachment'), (13, 'Incident resolved'), (14, 'Incident unresolved'), (17, 'Escalation failed'), (18, 'Acknowledge reminder triggered'), (19, 'Wiped'), (20, 'Deleted'), (21, 'Incident registered'), (22, 'A route is assigned to the incident'), (23, 'Trigger direct paging escalation'), (24, 'Unpage a user')]), + ), + ] diff --git a/engine/apps/api/serializers/paging.py b/engine/apps/api/serializers/paging.py index 99631618..452f4eb4 100644 --- a/engine/apps/api/serializers/paging.py +++ b/engine/apps/api/serializers/paging.py @@ -40,6 +40,9 @@ class DirectPagingSerializer(serializers.Serializer): users = UserReferenceSerializer(many=True, required=False, default=list) schedules = ScheduleReferenceSerializer(many=True, required=False, default=list) + escalation_chain_id = serializers.CharField(required=False, default=None) + escalation_chain = serializers.HiddenField(default=None) # set in DirectPagingSerializer.validate + alert_group_id = serializers.CharField(required=False, default=None) alert_group = serializers.HiddenField(default=None) # set in DirectPagingSerializer.validate @@ -47,19 +50,37 @@ class DirectPagingSerializer(serializers.Serializer): message = serializers.CharField(required=False, default=None) def validate(self, attrs): - if len(attrs["users"]) == 0 and len(attrs["schedules"]) == 0: - raise serializers.ValidationError("Provide at least one user or schedule") + organization = self.context["organization"] - if attrs["alert_group_id"] and (attrs["title"] or attrs["message"]): + users = attrs["users"] + schedules = attrs["schedules"] + escalation_chain_id = attrs["escalation_chain_id"] + + alert_group_id = attrs["alert_group_id"] + title = attrs["title"] + message = attrs["message"] + + if len(users) == 0 and len(schedules) == 0 and not escalation_chain_id: + raise serializers.ValidationError("Provide users, schedules, or an escalation chain") + + if alert_group_id and (title or message): raise serializers.ValidationError("alert_group_id and (title, message) are mutually exclusive") - if attrs["alert_group_id"]: - organization = self.context["organization"] + if alert_group_id and escalation_chain_id: + raise serializers.ValidationError("escalation_chain_id is not supported for existing alert groups") + + if alert_group_id: try: attrs["alert_group"] = AlertGroup.unarchived_objects.get( - public_primary_key=attrs["alert_group_id"], channel__organization=organization + public_primary_key=alert_group_id, channel__organization=organization ) except ObjectDoesNotExist: - raise serializers.ValidationError("Alert group {} does not exist".format(attrs["alert_group_id"])) + raise serializers.ValidationError("Alert group {} does not exist".format(alert_group_id)) + + if escalation_chain_id: + try: + attrs["escalation_chain"] = organization.escalation_chains.get(public_primary_key=escalation_chain_id) + except ObjectDoesNotExist: + raise serializers.ValidationError("Escalation chain {} does not exist".format(escalation_chain_id)) return attrs diff --git a/engine/apps/api/tests/test_paging.py b/engine/apps/api/tests/test_paging.py index a76f5c60..33b66c09 100644 --- a/engine/apps/api/tests/test_paging.py +++ b/engine/apps/api/tests/test_paging.py @@ -9,7 +9,11 @@ from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal @pytest.mark.django_db def test_direct_paging_new_alert_group( - make_organization_and_user_with_plugin_token, make_user, make_schedule, make_user_auth_headers + make_organization_and_user_with_plugin_token, + make_user, + make_schedule, + make_escalation_chain, + make_user_auth_headers, ): organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) @@ -32,6 +36,8 @@ def test_direct_paging_new_alert_group( }, ] + escalation_chain_to_page = make_escalation_chain(organization) + title = "Test Alert Group" message = "Testing direct paging with new alert group" @@ -40,7 +46,13 @@ def test_direct_paging_new_alert_group( response = client.post( url, - data={"users": users_to_page, "schedules": schedules_to_page, "title": title, "message": message}, + data={ + "users": users_to_page, + "schedules": schedules_to_page, + "escalation_chain_id": escalation_chain_to_page.public_primary_key, + "title": title, + "message": message, + }, format="json", **make_user_auth_headers(user, token), ) @@ -94,6 +106,38 @@ def test_direct_paging_existing_alert_group( assert response.status_code == status.HTTP_200_OK +@pytest.mark.django_db +def test_direct_paging_existing_alert_group_and_escalation_chain( + make_organization_and_user_with_plugin_token, + make_user, + make_schedule, + make_escalation_chain, + make_alert_receive_channel, + make_alert_group, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + escalation_chain_to_page = make_escalation_chain(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + client = APIClient() + url = reverse("api-internal:direct_paging") + + response = client.post( + url, + data={ + "escalation_chain_id": escalation_chain_to_page.public_primary_key, + "alert_group_id": alert_group.public_primary_key, + }, + format="json", + **make_user_auth_headers(user, token), + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db def test_direct_paging_no_title( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/views/paging.py b/engine/apps/api/views/paging.py index f1e07d67..b0fcfd1d 100644 --- a/engine/apps/api/views/paging.py +++ b/engine/apps/api/views/paging.py @@ -38,6 +38,7 @@ class DirectPagingAPIView(APIView): message=serializer.validated_data["message"], users=users, schedules=schedules, + escalation_chain=serializer.validated_data["escalation_chain"], alert_group=serializer.validated_data["alert_group"], )