add POST /escalation public API endpoint + add public API docs for teams/organization endpoints (#4815)

# What this PR does

- Adds a `POST /escalation` public endpoint (equivalent to the internal
direct paging API endpoint)
- Adds public API documentation for teams and organization endpoints

<img width="1140" alt="Screenshot 2024-08-15 at 12 49 40"
src="https://github.com/user-attachments/assets/e0e8d6bb-f3ac-4f9e-bdf7-e8926949cc3b">

## Which issue(s) this PR closes

Closes https://github.com/grafana/oncall-private/issues/2859
Closes https://github.com/grafana/oncall/issues/2448

## 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] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
This commit is contained in:
Joey Orlando 2024-08-15 14:31:35 -04:00 committed by GitHub
parent 64bf1e5096
commit 67fc52d56a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 752 additions and 6 deletions

View file

@ -0,0 +1,190 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation/
title: Escalation HTTP API
weight: 1200
refs:
users:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/users
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/users
teams:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/teams
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/teams
manual-paging:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/configure/integrations/references/manual
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/configure/integrations/references/manual
---
# Escalation HTTP API
See [Manual paging integration](ref:manual-paging) for more background on how escalating to a team or user(s) works.
## Escalate to a set of users
For more details about how to fetch a user's Grafana OnCall ID, refer to the [Users](ref:users) public API documentation.
```shell
curl "{{API_URL}}/api/v1/escalation/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"title": "We are seeing a network outage in the datacenter",
"message": "I need help investigating, can you join the investigation?",
"source_url": "https://github.com/myorg/myrepo/issues/123",
"users": [
{
"id": "U281SN24AVVJX",
"important": false
},
{
"id": "U5AKCVNDEDUE7",
"important": true
}
]
}'
```
The above command returns JSON structured in the following way:
```json
{
"id": "IZHCC4GTNPZ93",
"integration_id": "CC3GZYZNIIEH5",
"route_id": "RDN8LITALJXCJ",
"alerts_count": 1,
"state": "firing",
"created_at": "2024-08-15T18:05:36.801215Z",
"resolved_at": null,
"resolved_by": null,
"acknowledged_at": null,
"acknowledged_by": null,
"title": "We're seeing a network outage in the datacenter",
"permalinks": {
"slack": null,
"slack_app": null,
"telegram": null,
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
},
"silenced_at": null
}
```
## Escalate to a team
For more details about how to fetch a team's Grafana OnCall ID, refer to the [Teams](ref:teams) public API documentation.
```shell
curl "{{API_URL}}/api/v1/escalation/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"title": "We are seeing a network outage in the datacenter",
"message": "I need help investigating, can you join the investigation?",
"source_url": "https://github.com/myorg/myrepo/issues/123",
"team": "TI73TDU19W48J"
}'
```
The above command returns JSON structured in the following way:
```json
{
"id": "IZHCC4GTNPZ93",
"integration_id": "CC3GZYZNIIEH5",
"route_id": "RDN8LITALJXCJ",
"alerts_count": 1,
"state": "firing",
"created_at": "2024-08-15T18:05:36.801215Z",
"resolved_at": null,
"resolved_by": null,
"acknowledged_at": null,
"acknowledged_by": null,
"title": "We're seeing a network outage in the datacenter",
"permalinks": {
"slack": null,
"slack_app": null,
"telegram": null,
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
},
"silenced_at": null
}
```
## Escalate to a set of user(s) for an existing Alert Group
The following shows how you can escalate to a set of user(s) for an existing Alert Group.
```shell
curl "{{API_URL}}/api/v1/escalation/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"alert_group_id": "IZMRNNY8RFS94",
"users": [
{
"id": "U281SN24AVVJX",
"important": false
},
{
"id": "U5AKCVNDEDUE7",
"important": true
}
]
}'
```
The above command returns JSON structured in the following way:
```json
{
"id": "IZHCC4GTNPZ93",
"integration_id": "CC3GZYZNIIEH5",
"route_id": "RDN8LITALJXCJ",
"alerts_count": 1,
"state": "firing",
"created_at": "2024-08-15T18:05:36.801215Z",
"resolved_at": null,
"resolved_by": null,
"acknowledged_at": null,
"acknowledged_by": null,
"title": "We're seeing a network outage in the datacenter",
"permalinks": {
"slack": null,
"slack_app": null,
"telegram": null,
"web": "http://<my_grafana_url>/a/grafana-oncall-app/alert-groups/I5LAZ2MXGPUAH"
},
"silenced_at": null
}
```
| Parameter | Unique | Required | Description |
| -------------------- | :----: | :--------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | No | No | Name of the Alert Group that will be created |
| `message` | No | No | Content of the Alert Group that will be created |
| `source_url` | No | No | Value that will be added in the Alert's payload as `oncall.permalink`. This can be useful to have the source URL/button autopopulated with a URL of interest. |
| `team` | No | Yes (see [Things to Note](#things-to-note)) | Grafana OnCall team ID. If specified, will use the "Direct Paging" Integration associated with this Grafana OnCall team, to create the Alert Group. |
| `users` | No | Yes (see [Things to Note](#things-to-note)) | List of user(s) to escalate to. See above request example for object schema. `id` represents the Grafana OnCall user's ID. `important` is a boolean representing whether to escalate the Alert Group using this user's default or important personal notification policy. |
| `alert_group_id` | No | No | If specified, will escalate the specified users for this Alert Group. |
## Things to note
- `team` and `users` are mutually exclusive in the request payload. If you would like to escalate to a team AND user(s),
first escalate to a team, then using the Alert Group ID returned in the response payload, add the required users to the
existing Alert Group
- `alert_group_id` is mutually exclusive with `title`, `message`, and `source_url`. Practically speaking this means that
if you are trying to escalate to a set of users on an existing Alert Group, you cannot update the `title`, `message`, or
`source_url` of that Alert Group
- If escalating to a set of users for an existing Alert Group, the Alert Group cannot be in a resolved state
**HTTP request**
`POST {{API_URL}}/api/v1/escalation/`

View file

@ -0,0 +1,73 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/organizations/
title: Grafana OnCall organizations HTTP API
weight: 1500
refs:
pagination:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/#pagination
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/#pagination
---
# Grafana OnCall organizations HTTP API
## Get an organization
This endpoint retrieves the organization object.
```shell
curl "{{API_URL}}/api/v1/organizations/O53AAGWFBPE5W/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
````
The above command returns JSON structured in the following way:
```json
{
"id": "O53AAGWFBPE5W"
}
```
**HTTP request**
`GET {{API_URL}}/api/v1/organizations/<ORGANIZATION_ID>/`
| Parameter | Unique | Description |
| ---------- | :-----: | :----------------------------------------------------------------- |
| `id` | Yes | Organization ID |
## List Organizations
```shell
curl "{{API_URL}}/api/v1/organizations/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```
The above command returns JSON structured in the following way:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "O53AAGWFBPE5W"
}
],
"page_size": 25,
"current_page_number": 1,
"total_pages": 1
}
```
> **Note**: The response is [paginated](ref:pagination). You may need to make multiple requests to get all records.
**HTTP request**
`GET {{API_URL}}/api/v1/organizations/`

View file

@ -0,0 +1,86 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/teams/
title: Grafana OnCall teams HTTP API
weight: 1500
refs:
pagination:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/#pagination
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/#pagination
---
# Grafana OnCall teams HTTP API
## Get a team
This endpoint retrieves the team object.
```shell
curl "{{API_URL}}/api/v1/teams/TI73TDU19W48J/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
````
The above command returns JSON structured in the following way:
```json
{
"id": "TI73TDU19W48J",
"name": "my test team",
"email": "",
"avatar_url": "/avatar/3f49c15916554246daa714b9bd0ee398"
}
```
**HTTP request**
`GET {{API_URL}}/api/v1/teams/<TEAM_ID>/`
| Parameter | Unique | Description |
| ---------- | :-----: | :----------------------------------------------------------------- |
| `id` | Yes/org | Team ID |
| `name` | Yes/org | Team name |
| `email` | Yes/org | Team e-mail |
| `avatar_url` | Yes | Avatar URL of the Grafana team |
## List Teams
```shell
curl "{{API_URL}}/api/v1/teams/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```
The above command returns JSON structured in the following way:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "TI73TDU19W48J",
"name": "my test team",
"email": "",
"avatar_url": "/avatar/3f49c15916554246daa714b9bd0ee398"
}
],
"page_size": 50,
"current_page_number": 1,
"total_pages": 1
}
```
> **Note**: The response is [paginated](ref:pagination). You may need to make multiple requests to get all records.
The following available filter parameter should be provided as a `GET` argument:
- `name` (Exact match)
**HTTP request**
`GET {{API_URL}}/api/v1/teams/`

View file

@ -32,9 +32,11 @@ class UserReferenceSerializer(serializers.Serializer):
return attrs
class DirectPagingSerializer(serializers.Serializer):
class BasePagingSerializer(serializers.Serializer):
context: SerializerContext
ALLOWS_GRAFANA_INCIDENT_ID = False
users = UserReferenceSerializer(many=True, required=False, default=list)
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())
@ -44,7 +46,6 @@ class DirectPagingSerializer(serializers.Serializer):
title = serializers.CharField(required=False, default=None)
message = serializers.CharField(required=False, default=None, allow_null=True)
source_url = serializers.URLField(required=False, default=None, allow_null=True)
grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)
def validate(self, attrs):
organization = self.context["organization"]
@ -52,13 +53,17 @@ class DirectPagingSerializer(serializers.Serializer):
title = attrs["title"]
message = attrs["message"]
source_url = attrs["source_url"]
grafana_incident_id = attrs["grafana_incident_id"]
grafana_incident_id = self.ALLOWS_GRAFANA_INCIDENT_ID and attrs.get("grafana_incident_id")
if alert_group_id and (title or message or source_url or grafana_incident_id):
raise serializers.ValidationError(
"alert_group_id and (title, message, source_url, grafana_incident_id) are mutually exclusive"
f"alert_group_id and (title, message, source_url{', grafana_incident_id' if self.ALLOWS_GRAFANA_INCIDENT_ID else ''}) "
"are mutually exclusive"
)
if attrs["users"] and attrs["team"]:
raise serializers.ValidationError("users and team are mutually exclusive")
if alert_group_id:
try:
attrs["alert_group"] = AlertGroup.objects.get(
@ -68,3 +73,9 @@ class DirectPagingSerializer(serializers.Serializer):
raise serializers.ValidationError("Alert group {} does not exist".format(alert_group_id))
return attrs
class DirectPagingSerializer(BasePagingSerializer):
ALLOWS_GRAFANA_INCIDENT_ID = True
grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)

View file

@ -224,6 +224,41 @@ def test_direct_paging_no_user_or_team_specified(
assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL
@pytest.mark.django_db
def test_direct_paging_both_team_and_users_specified(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_user,
make_team,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR)
team = make_team(organization=organization)
# user must be part of the team
user.teams.add(team)
client = APIClient()
url = reverse("api-internal:direct_paging")
response = client.post(
url,
data={
"team": team.public_primary_key,
"users": [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
],
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["non_field_errors"] == ["users and team are mutually exclusive"]
@pytest.mark.parametrize(
"field_name,field_value",
[

View file

@ -9,6 +9,7 @@ from .views.alert_receive_channel import AlertReceiveChannelView
from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView
from .views.alerts import AlertDetailView
from .views.channel_filter import ChannelFilterView
from .views.direct_paging import DirectPagingAPIView
from .views.escalation_chain import EscalationChainViewSet
from .views.escalation_policy import EscalationPolicyView
from .views.features import FeaturesAPIView
@ -23,7 +24,6 @@ from .views.organization import (
OrganizationConfigChecksView,
SetGeneralChannel,
)
from .views.paging import DirectPagingAPIView
from .views.preview_template_options import PreviewTemplateOptionsView
from .views.public_api_tokens import PublicApiTokenView
from .views.resolution_note import ResolutionNoteView

View file

@ -5,7 +5,7 @@ from rest_framework.views import APIView
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
from apps.api.permissions import RBACPermission
from apps.api.serializers.paging import DirectPagingSerializer
from apps.api.serializers.direct_paging import DirectPagingSerializer
from apps.auth_token.auth import PluginAuthentication
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from common.api_helpers.exceptions import BadRequest

View file

@ -1,4 +1,5 @@
from .alerts import AlertSerializer # noqa: F401
from .escalation import EscalationSerializer # noqa: F401
from .escalation_chains import EscalationChainSerializer # noqa: F401
from .escalation_policies import EscalationPolicySerializer, EscalationPolicyUpdateSerializer # noqa: F401
from .incidents import IncidentSerializer # noqa: F401

View file

@ -0,0 +1,8 @@
from apps.api.serializers.direct_paging import BasePagingSerializer
class EscalationSerializer(BasePagingSerializer):
"""
Very similar to `apps.api.serializers.direct_paging.DirectPagingSerializer` except that
there is no `grafana_incident_id` attribute
"""

View file

@ -0,0 +1,294 @@
from unittest import mock
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.alerts.models import AlertGroup
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError
title = "Custom title"
message = "Testing escalation with new alert group"
source_url = "https://www.example.com"
@pytest.mark.django_db
def test_escalation_new_alert_group(
make_organization_and_user_with_token,
make_user,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_token()
users_to_page = [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
{
"id": make_user(organization=organization).public_primary_key,
"important": True,
},
]
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"users": users_to_page,
"title": title,
"message": message,
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_200_OK
alert_groups = AlertGroup.objects.all()
assert alert_groups.count() == 1
ag = alert_groups.get()
assert response.json() == {
"id": ag.public_primary_key,
"integration_id": ag.channel.public_primary_key,
"route_id": ag.channel_filter.public_primary_key,
"alerts_count": 1,
"state": "firing",
"created_at": mock.ANY,
"resolved_at": None,
"resolved_by": None,
"acknowledged_at": None,
"acknowledged_by": None,
"title": title,
"permalinks": {
"slack": None,
"slack_app": None,
"telegram": None,
"web": f"a/grafana-oncall-app/alert-groups/{ag.public_primary_key}",
},
"silenced_at": None,
}
alert = ag.alerts.get()
assert ag.web_title_cache == title
assert alert.title == title
assert alert.message == message
@pytest.mark.django_db
def test_escalation_team(
make_organization_and_user_with_token,
make_team,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_token()
team = make_team(organization=organization)
# user must be part of the team
user.teams.add(team)
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"team": team.public_primary_key,
"message": message,
"source_url": source_url,
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_200_OK
alert_group = AlertGroup.objects.get(public_primary_key=response.json()["id"])
alert = alert_group.alerts.first()
assert alert.raw_request_data["oncall"]["permalink"] == source_url
@pytest.mark.django_db
def test_escalation_existing_alert_group(
make_organization_and_user_with_token,
make_user,
make_alert_receive_channel,
make_alert_group,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_token()
users_to_page = [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
{
"id": make_user(
organization=organization,
).public_primary_key,
"important": True,
},
]
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={"users": users_to_page, "alert_group_id": alert_group.public_primary_key},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == alert_group.public_primary_key
@pytest.mark.django_db
def test_escalation_existing_alert_group_resolved(
make_organization_and_user_with_token,
make_user,
make_alert_receive_channel,
make_alert_group,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_token()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=True)
users_to_page = [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
]
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"alert_group_id": alert_group.public_primary_key,
"users": users_to_page,
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["detail"] == DirectPagingAlertGroupResolvedError.DETAIL
@pytest.mark.django_db
def test_escalation_no_user_or_team_specified(
make_organization_and_user_with_token,
make_user_auth_headers,
):
_, user, token = make_organization_and_user_with_token()
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"team": None,
"users": [],
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL
@pytest.mark.django_db
def test_escalation_both_team_and_users_specified(
make_organization_and_user_with_token,
make_user_auth_headers,
make_user,
make_team,
):
organization, user, token = make_organization_and_user_with_token()
team = make_team(organization=organization)
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"team": team.public_primary_key,
"users": [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
],
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["non_field_errors"] == ["users and team are mutually exclusive"]
@pytest.mark.parametrize(
"field_name,field_value",
[
("title", title),
("message", message),
("source_url", source_url),
],
)
@pytest.mark.django_db
def test_escalation_alert_group_id_and_other_fields_are_mutually_exclusive(
make_organization_and_user_with_token,
make_team,
make_user_auth_headers,
make_alert_receive_channel,
make_alert_group,
field_name,
field_value,
):
error_msg = "alert_group_id and (title, message, source_url) are mutually exclusive"
organization, user, token = make_organization_and_user_with_token()
team = make_team(organization=organization)
# user must be part of the team
user.teams.add(team)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=True)
client = APIClient()
url = reverse("api-public:escalation")
response = client.post(
url,
data={
"team": team.public_primary_key,
"alert_group_id": alert_group.public_primary_key,
field_name: field_value,
},
format="json",
**make_user_auth_headers(user, token),
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["non_field_errors"] == [error_msg]

View file

@ -34,4 +34,5 @@ urlpatterns = [
optional_slash_path("info", views.InfoView.as_view(), name="info"),
optional_slash_path("make_call", views.MakeCallView.as_view(), name="make_call"),
optional_slash_path("send_sms", views.SendSMSView.as_view(), name="send_sms"),
optional_slash_path("escalation", views.EscalationView.as_view(), name="escalation"),
]

View file

@ -1,5 +1,6 @@
from .action import ActionView # noqa: F401
from .alerts import AlertView # noqa: F401
from .escalation import EscalationView # noqa: F401
from .escalation_chains import EscalationChainView # noqa: F401
from .escalation_policies import EscalationPolicyView # noqa: F401
from .incidents import IncidentView # noqa: F401

View file

@ -0,0 +1,46 @@
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import EscalationSerializer, IncidentSerializer
from apps.public_api.throttlers import UserThrottle
from common.api_helpers.exceptions import BadRequest
class EscalationView(APIView):
"""
aka "Direct Paging"
"""
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
throttle_classes = [UserThrottle]
def post(self, request):
user = request.user
organization = user.organization
serializer = EscalationSerializer(data=request.data, context={"organization": organization, "request": request})
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
try:
alert_group = direct_paging(
organization=organization,
from_user=user,
message=validated_data["message"],
title=validated_data["title"],
source_url=validated_data["source_url"],
team=validated_data["team"],
users=[(user["instance"], user["important"]) for user in validated_data["users"]],
alert_group=validated_data["alert_group"],
)
except DirectPagingAlertGroupResolvedError:
raise BadRequest(detail=DirectPagingAlertGroupResolvedError.DETAIL)
except DirectPagingUserTeamValidationError:
raise BadRequest(detail=DirectPagingUserTeamValidationError.DETAIL)
return Response(IncidentSerializer(alert_group).data, status=status.HTTP_200_OK)