Skip to content

Commit

Permalink
Resolution note source mobile app (#3174)
Browse files Browse the repository at this point in the history
# What this PR does

Fixes #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)
  • Loading branch information
vstpme authored Oct 20, 2023
1 parent bf197b0 commit 2179e7a
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 18 additions & 0 deletions engine/apps/alerts/migrations/0034_alter_resolutionnote_source.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
5 changes: 3 additions & 2 deletions engine/apps/alerts/models/resolution_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion engine/apps/api/serializers/resolution_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
64 changes: 63 additions & 1 deletion engine/apps/api/tests/test_alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion engine/apps/api/views/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ResolutionNoteSourceTypesOptions = {
[key: number]: string;
};
export const ResolutionNoteSourceTypesToDisplayName: ResolutionNoteSourceTypesOptions = {
0: 'slack',
1: 'web',
0: 'Slack',
1: 'Web',
2: 'Mobile App',
};
3 changes: 2 additions & 1 deletion grafana-plugin/src/pages/incident/Incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down

0 comments on commit 2179e7a

Please sign in to comment.