diff --git a/engine/apps/api/errors.py b/engine/apps/api/errors.py new file mode 100644 index 00000000..3c43686a --- /dev/null +++ b/engine/apps/api/errors.py @@ -0,0 +1,18 @@ +"""errors contains business-logic error codes for internal api. + +It's expected that error codes will use 1000-9999 codes range, where first two digits are for entity: +11xx - AlertGroup, 12xx - AlertReceiveChannel, etc. +10xx are saved for non-entity related errors. +""" +# TODO: this package is WIP. It requires validation of code ranges. +from enum import Enum, unique + + +@unique +class AlertGroupAPIError(Enum): + """ + Error codes for alert group. + Range is 1100-1199 + """ + + RESOLUTION_NOTE_REQUIRED = 1101 diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index e8a8e2ac..ea815f28 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import AlertGroup, AlertGroupLogRecord, AlertReceiveChannel +from apps.api.errors import AlertGroupAPIError from apps.api.permissions import LegacyAccessControlRole from apps.base.models import UserNotificationPolicyLogRecord @@ -1805,3 +1806,42 @@ def test_direct_paging_integration_treated_as_deleted( response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.json()["alert_receive_channel"]["deleted"] is True + + +@pytest.mark.django_db +def test_alert_group_resolve_resolution_note( + make_organization_and_user_with_plugin_token, + make_alert_receive_channel, + make_channel_filter, + make_alert_group, + make_alert, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token() + alert_receive_channel = make_alert_receive_channel(organization) + channel_filter = make_channel_filter(alert_receive_channel, is_default=True) + new_alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter) + make_alert(alert_group=new_alert_group, raw_request_data=alert_raw_request_data) + + organization.is_resolution_note_required = True + organization.save() + + client = APIClient() + url = reverse("api-internal:alertgroup-resolve", kwargs={"pk": new_alert_group.public_primary_key}) + + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + # check that resolution note is required + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["code"] == AlertGroupAPIError.RESOLUTION_NOTE_REQUIRED.value + + with patch( + "apps.alerts.tasks.send_update_resolution_note_signal.send_update_resolution_note_signal.apply_async" + ) as mock_signal: + url = reverse("api-internal:alertgroup-resolve", kwargs={"pk": new_alert_group.public_primary_key}) + response = client.post( + url, format="json", data={"resolution_note": "hi"}, **make_user_auth_headers(user, token) + ) + assert response.status_code == status.HTTP_200_OK + + assert new_alert_group.has_resolution_notes + assert mock_signal.called diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 4442e8dd..bcae5306 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -13,8 +13,10 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from apps.alerts.constants import ActionSource -from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel, EscalationChain +from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel, EscalationChain, ResolutionNote from apps.alerts.paging import unpage_user +from apps.alerts.tasks import send_update_resolution_note_signal +from apps.api.errors import AlertGroupAPIError from apps.api.permissions import RBACPermission from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer from apps.api.serializers.team import TeamSerializer @@ -456,11 +458,30 @@ class AlertGroupView( if alert_group.is_maintenance_incident: alert_group.stop_maintenance(self.request.user) else: - if organization.is_resolution_note_required and not alert_group.has_resolution_notes: - return Response( - data="Alert group without resolution note cannot be resolved due to organization settings.", - status=status.HTTP_400_BAD_REQUEST, + resolution_note_text = request.data.get("resolution_note") + if resolution_note_text: + rn = ResolutionNote.objects.create( + alert_group=alert_group, + author=self.request.user, + source=ResolutionNote.Source.WEB, + message_text=resolution_note_text[:3000], # trim text to fit in the db field ) + send_update_resolution_note_signal.apply_async( + kwargs={ + "alert_group_pk": alert_group.pk, + "resolution_note_pk": rn.pk, + } + ) + else: + # Check resolution note required setting only if resolution_note_text was not provided. + if organization.is_resolution_note_required and not alert_group.has_resolution_notes: + return Response( + data={ + "code": AlertGroupAPIError.RESOLUTION_NOTE_REQUIRED.value, + "detail": "Alert group without resolution note cannot be resolved due to organization settings", + }, + status=status.HTTP_400_BAD_REQUEST, + ) alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)