diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f79c2a7ba..329e58ac31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Discard old pending network requests in the UI (Users/Schedules) [#3172](https://github.com/grafana/oncall/pull/3172) +- Fix resolution note source for mobile app by @vadimkerr ([#3174](https://github.com/grafana/oncall/pull/3174)) ## v1.3.45 (2023-10-19) diff --git a/engine/apps/alerts/migrations/0034_alter_resolutionnote_source.py b/engine/apps/alerts/migrations/0034_alter_resolutionnote_source.py new file mode 100644 index 0000000000..d9b70961c4 --- /dev/null +++ b/engine/apps/alerts/migrations/0034_alter_resolutionnote_source.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-10-20 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0033_alertgrouplogrecord_action_source'), + ] + + operations = [ + migrations.AlterField( + model_name='resolutionnote', + name='source', + field=models.IntegerField(choices=[(0, 'Slack'), (1, 'Web'), (2, 'Mobile App')], default=None, null=True), + ), + ] diff --git a/engine/apps/alerts/models/resolution_note.py b/engine/apps/alerts/models/resolution_note.py index af1317c924..b60f70d8fd 100644 --- a/engine/apps/alerts/models/resolution_note.py +++ b/engine/apps/alerts/models/resolution_note.py @@ -120,8 +120,9 @@ class ResolutionNote(models.Model): objects_with_deleted = models.Manager() class Source(models.IntegerChoices): - SLACK = 0, "slack" - WEB = 1, "web" + SLACK = 0, "Slack" + WEB = 1, "Web" + MOBILE_APP = 2, "Mobile App" public_primary_key = models.CharField( max_length=20, diff --git a/engine/apps/api/serializers/resolution_note.py b/engine/apps/api/serializers/resolution_note.py index 5cce7a51b6..528d5e6308 100644 --- a/engine/apps/api/serializers/resolution_note.py +++ b/engine/apps/api/serializers/resolution_note.py @@ -2,6 +2,7 @@ from apps.alerts.models import AlertGroup, ResolutionNote from apps.api.serializers.user import FastUserSerializer +from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import EagerLoadingMixin @@ -36,7 +37,13 @@ class Meta: def create(self, validated_data): validated_data["author"] = self.context["request"].user - validated_data["source"] = ResolutionNote.Source.WEB + + if isinstance(self.context["request"].successful_authenticator, MobileAppAuthTokenAuthentication): + source = ResolutionNote.Source.MOBILE_APP + else: + source = ResolutionNote.Source.WEB + validated_data["source"] = source + created_instance = super().create(validated_data) return created_instance diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index ee52d202c4..5b810973ff 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -10,7 +10,7 @@ from rest_framework.test import APIClient from apps.alerts.constants import ActionSource -from apps.alerts.models import AlertGroup, AlertGroupLogRecord +from apps.alerts.models import AlertGroup, AlertGroupLogRecord, ResolutionNote from apps.alerts.tasks import wipe from apps.api.errors import AlertGroupAPIError from apps.api.permissions import LegacyAccessControlRole @@ -1883,6 +1883,68 @@ def test_alert_group_resolve_resolution_note( assert mock_signal.called +@pytest.mark.django_db +def test_alert_group_resolve_resolution_note_mobile_app( + make_organization_and_user, + make_mobile_app_auth_token_for_user, + make_alert_receive_channel, + make_channel_filter, + make_alert_group, + make_alert, + make_user_auth_headers, +): + organization, user = make_organization_and_user() + organization.is_resolution_note_required = True + organization.save() + _, token = make_mobile_app_auth_token_for_user(user, organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + client = APIClient() + url = reverse("api-internal:alertgroup-resolve", kwargs={"pk": alert_group.public_primary_key}) + response = client.post(url, format="json", data={"resolution_note": "hi"}, HTTP_AUTHORIZATION=token) + + assert response.status_code == status.HTTP_200_OK + assert alert_group.resolution_notes.get().source == ResolutionNote.Source.MOBILE_APP + + +@pytest.mark.parametrize("source", ResolutionNote.Source) +@pytest.mark.django_db +def test_timeline_resolution_note_source( + make_organization_and_user_with_plugin_token, + make_alert_receive_channel, + make_channel_filter, + make_alert_group, + make_alert, + make_resolution_note_slack_message, + make_resolution_note, + make_user_auth_headers, + source, +): + """The 'type' field in timeline items should hold the source of the resolution note""" + 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) + alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter) + make_alert(alert_group=alert_group, raw_request_data=alert_raw_request_data) + + # Create resolution note + resolution_note_slack_message = make_resolution_note_slack_message( + alert_group=alert_group, user=user, added_by_user=user, text="resolution note" + ) + make_resolution_note( + alert_group=alert_group, author=user, resolution_note_slack_message=resolution_note_slack_message, source=source + ) + + client = APIClient() + url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key}) + response = client.get(url, **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + assert response.json()["render_after_resolve_report_json"][0]["type"] == source.value + + @pytest.mark.django_db def test_timeline_api_action( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/tests/test_postmortem_messages.py b/engine/apps/api/tests/test_resolution_note.py similarity index 92% rename from engine/apps/api/tests/test_postmortem_messages.py rename to engine/apps/api/tests/test_resolution_note.py index a467a6a9ba..b815c7d97e 100644 --- a/engine/apps/api/tests/test_postmortem_messages.py +++ b/engine/apps/api/tests/test_resolution_note.py @@ -35,8 +35,8 @@ def test_create_resolution_note( "id": resolution_note.public_primary_key, "alert_group": alert_group.public_primary_key, "source": { - "id": resolution_note.source, - "display_name": resolution_note.get_source_display(), + "id": ResolutionNote.Source.WEB.value, + "display_name": ResolutionNote.Source.WEB.label, }, "author": { "pk": user.public_primary_key, @@ -50,6 +50,31 @@ def test_create_resolution_note( assert response.data == result +@pytest.mark.django_db +def test_create_resolution_note_mobile_app( + make_organization_and_user, make_mobile_app_auth_token_for_user, make_alert_receive_channel, make_alert_group +): + organization, user = make_organization_and_user() + _, token = make_mobile_app_auth_token_for_user(user, organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + client = APIClient() + url = reverse("api-internal:resolution_note-list") + data = { + "alert_group": alert_group.public_primary_key, + "text": "Test Message", + } + + response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=token) + assert response.status_code == status.HTTP_201_CREATED + assert response.data["source"] == { + "id": ResolutionNote.Source.MOBILE_APP.value, + "display_name": ResolutionNote.Source.MOBILE_APP.label, + } + + @pytest.mark.django_db def test_create_resolution_note_invalid_text( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 14b1bf4113..97ef641e51 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -516,7 +516,11 @@ def resolve(self, request, pk): rn = ResolutionNote.objects.create( alert_group=alert_group, author=self.request.user, - source=ResolutionNote.Source.WEB, + source=( + ResolutionNote.Source.MOBILE_APP + if isinstance(self.request.successful_authenticator, MobileAppAuthTokenAuthentication) + else ResolutionNote.Source.WEB + ), message_text=resolution_note_text[:3000], # trim text to fit in the db field ) send_update_resolution_note_signal.apply_async( diff --git a/grafana-plugin/src/models/resolution_note/resolution_note.types.ts b/grafana-plugin/src/models/resolution_note/resolution_note.types.ts index 0bb5f1b93d..2b5c37794b 100644 --- a/grafana-plugin/src/models/resolution_note/resolution_note.types.ts +++ b/grafana-plugin/src/models/resolution_note/resolution_note.types.ts @@ -19,6 +19,7 @@ type ResolutionNoteSourceTypesOptions = { [key: number]: string; }; export const ResolutionNoteSourceTypesToDisplayName: ResolutionNoteSourceTypesOptions = { - 0: 'slack', - 1: 'web', + 0: 'Slack', + 1: 'Web', + 2: 'Mobile App', }; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 60cb731edb..4c24336fed 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -520,7 +520,8 @@ class IncidentPage extends React.Component {item.realm === TimeLineRealm.ResolutionNote && ( - {item.author && item.author.username} via {ResolutionNoteSourceTypesToDisplayName[item.type]} + {item.author && item.author.username} via{' '} + {ResolutionNoteSourceTypesToDisplayName[item.type] || 'Web'} )}