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:
parent
bf197b09c2
commit
2179e7a1c9
9 changed files with 130 additions and 10 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Discard old pending network requests in the UI (Users/Schedules) [#3172](https://github.com/grafana/oncall/pull/3172)
|
- 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)
|
## v1.3.45 (2023-10-19)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -120,8 +120,9 @@ class ResolutionNote(models.Model):
|
||||||
objects_with_deleted = models.Manager()
|
objects_with_deleted = models.Manager()
|
||||||
|
|
||||||
class Source(models.IntegerChoices):
|
class Source(models.IntegerChoices):
|
||||||
SLACK = 0, "slack"
|
SLACK = 0, "Slack"
|
||||||
WEB = 1, "web"
|
WEB = 1, "Web"
|
||||||
|
MOBILE_APP = 2, "Mobile App"
|
||||||
|
|
||||||
public_primary_key = models.CharField(
|
public_primary_key = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from apps.alerts.models import AlertGroup, ResolutionNote
|
from apps.alerts.models import AlertGroup, ResolutionNote
|
||||||
from apps.api.serializers.user import FastUserSerializer
|
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.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
|
||||||
from common.api_helpers.exceptions import BadRequest
|
from common.api_helpers.exceptions import BadRequest
|
||||||
from common.api_helpers.mixins import EagerLoadingMixin
|
from common.api_helpers.mixins import EagerLoadingMixin
|
||||||
|
|
@ -36,7 +37,13 @@ class ResolutionNoteSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data["author"] = self.context["request"].user
|
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)
|
created_instance = super().create(validated_data)
|
||||||
return created_instance
|
return created_instance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from apps.alerts.constants import ActionSource
|
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.alerts.tasks import wipe
|
||||||
from apps.api.errors import AlertGroupAPIError
|
from apps.api.errors import AlertGroupAPIError
|
||||||
from apps.api.permissions import LegacyAccessControlRole
|
from apps.api.permissions import LegacyAccessControlRole
|
||||||
|
|
@ -1883,6 +1883,68 @@ def test_alert_group_resolve_resolution_note(
|
||||||
assert mock_signal.called
|
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
|
@pytest.mark.django_db
|
||||||
def test_timeline_api_action(
|
def test_timeline_api_action(
|
||||||
make_organization_and_user_with_plugin_token,
|
make_organization_and_user_with_plugin_token,
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ def test_create_resolution_note(
|
||||||
"id": resolution_note.public_primary_key,
|
"id": resolution_note.public_primary_key,
|
||||||
"alert_group": alert_group.public_primary_key,
|
"alert_group": alert_group.public_primary_key,
|
||||||
"source": {
|
"source": {
|
||||||
"id": resolution_note.source,
|
"id": ResolutionNote.Source.WEB.value,
|
||||||
"display_name": resolution_note.get_source_display(),
|
"display_name": ResolutionNote.Source.WEB.label,
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"pk": user.public_primary_key,
|
"pk": user.public_primary_key,
|
||||||
|
|
@ -50,6 +50,31 @@ def test_create_resolution_note(
|
||||||
assert response.data == result
|
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
|
@pytest.mark.django_db
|
||||||
def test_create_resolution_note_invalid_text(
|
def test_create_resolution_note_invalid_text(
|
||||||
make_organization_and_user_with_plugin_token,
|
make_organization_and_user_with_plugin_token,
|
||||||
|
|
@ -516,7 +516,11 @@ class AlertGroupView(
|
||||||
rn = ResolutionNote.objects.create(
|
rn = ResolutionNote.objects.create(
|
||||||
alert_group=alert_group,
|
alert_group=alert_group,
|
||||||
author=self.request.user,
|
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
|
message_text=resolution_note_text[:3000], # trim text to fit in the db field
|
||||||
)
|
)
|
||||||
send_update_resolution_note_signal.apply_async(
|
send_update_resolution_note_signal.apply_async(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type ResolutionNoteSourceTypesOptions = {
|
||||||
[key: number]: string;
|
[key: number]: string;
|
||||||
};
|
};
|
||||||
export const ResolutionNoteSourceTypesToDisplayName: ResolutionNoteSourceTypesOptions = {
|
export const ResolutionNoteSourceTypesToDisplayName: ResolutionNoteSourceTypesOptions = {
|
||||||
0: 'slack',
|
0: 'Slack',
|
||||||
1: 'web',
|
1: 'Web',
|
||||||
|
2: 'Mobile App',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,8 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
<VerticalGroup spacing="none">
|
<VerticalGroup spacing="none">
|
||||||
{item.realm === TimeLineRealm.ResolutionNote && (
|
{item.realm === TimeLineRealm.ResolutionNote && (
|
||||||
<Text type="secondary" size="small">
|
<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>
|
||||||
)}
|
)}
|
||||||
<Text type="primary">
|
<Text type="primary">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue