Resolution note source mobile app (#3174)

# What this PR does

Fixes https://github.com/grafana/oncall/issues/2320

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Vadim Stepanov 2023-10-20 15:22:45 +01:00 committed by GitHub
parent bf197b09c2
commit 2179e7a1c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 130 additions and 10 deletions

View file

@ -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)

View file

@ -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),
),
]

View file

@ -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,

View file

@ -2,6 +2,7 @@ from rest_framework import serializers
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 ResolutionNoteSerializer(EagerLoadingMixin, serializers.ModelSerializer):
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

View file

@ -10,7 +10,7 @@ from rest_framework.response import Response
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,

View file

@ -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,

View file

@ -516,7 +516,11 @@ class AlertGroupView(
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(

View file

@ -19,6 +19,7 @@ type ResolutionNoteSourceTypesOptions = {
[key: number]: string;
};
export const ResolutionNoteSourceTypesToDisplayName: ResolutionNoteSourceTypesOptions = {
0: 'slack',
1: 'web',
0: 'Slack',
1: 'Web',
2: 'Mobile App',
};

View file

@ -520,7 +520,8 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
<VerticalGroup spacing="none">
{item.realm === TimeLineRealm.ResolutionNote && (
<Text type="secondary" size="small">
{item.author && item.author.username} via {ResolutionNoteSourceTypesToDisplayName[item.type]}
{item.author && item.author.username} via{' '}
{ResolutionNoteSourceTypesToDisplayName[item.type] || 'Web'}
</Text>
)}
<Text type="primary">