oncall-engine/engine/apps/api/views/escalation_chain.py
Vadim Stepanov 696aca2d80
Make it clear alert groups can't be searched (#4713)
# What this PR does

* Make the filter input say `Filter results...` instead of `Search or
filter results...` on the alert group page; disallow custom input there
so it's only possible to choose among existing filters
* Remove outdated `<page>/filters?search=` functionality from internal
API

## Which issue(s) this PR closes

Related to https://github.com/grafana/oncall-private/issues/2679

<!--
*Note*: If you want the issue to be auto-closed once the PR is merged,
change "Related to" to "Closes" in the line above.
If you have more than one GitHub issue that this PR closes, be sure to
preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## 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.
2024-07-22 10:30:28 +00:00

198 lines
7.3 KiB
Python

from django.db.models import Count, Q
from django_filters import rest_framework as filters
from emoji import emojize
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.alerts.models import EscalationChain
from apps.api.permissions import RBACPermission
from apps.api.serializers.escalation_chain import (
EscalationChainListSerializer,
EscalationChainSerializer,
FilterEscalationChainSerializer,
)
from apps.auth_token.auth import PluginAuthentication
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.user_management.models import Team
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import (
NO_TEAM_VALUE,
ByTeamModelFieldFilterMixin,
ModelFieldFilterMixin,
TeamModelMultipleChoiceFilter,
)
from common.api_helpers.mixins import (
FilterSerializerMixin,
ListSerializerMixin,
PublicPrimaryKeyMixin,
TeamFilteringMixin,
)
from common.insight_log import EntityEvent, write_resource_insight_log
class EscalationChainFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
team = TeamModelMultipleChoiceFilter()
class EscalationChainViewSet(
TeamFilteringMixin,
PublicPrimaryKeyMixin[EscalationChain],
FilterSerializerMixin,
ListSerializerMixin,
viewsets.ModelViewSet,
):
authentication_classes = (
MobileAppAuthTokenAuthentication,
PluginAuthentication,
)
permission_classes = (IsAuthenticated, RBACPermission)
rbac_permissions = {
"metadata": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"details": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"copy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"filters": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
}
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
search_fields = ("name",)
filterset_class = EscalationChainFilter
serializer_class = EscalationChainSerializer
list_serializer_class = EscalationChainListSerializer
filter_serializer_class = FilterEscalationChainSerializer
def get_queryset(self, ignore_filtering_by_available_teams=False):
is_filters_request = self.request.query_params.get("filters", "false") == "true"
queryset = EscalationChain.objects.filter(
organization=self.request.auth.organization,
)
if not ignore_filtering_by_available_teams:
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
if is_filters_request:
# Do not annotate num_integrations and num_routes for filters request,
# only fetch public_primary_key and name fields needed by FilterEscalationChainSerializer
return queryset.only("public_primary_key", "name")
queryset = queryset.annotate(
num_integrations=Count(
"channel_filters__alert_receive_channel",
distinct=True,
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
)
).annotate(
num_routes=Count(
"channel_filters",
distinct=True,
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
)
)
return queryset
def perform_create(self, serializer):
serializer.save()
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)
def perform_destroy(self, instance):
write_resource_insight_log(
instance=instance,
author=self.request.user,
event=EntityEvent.DELETED,
)
instance.delete()
def perform_update(self, serializer):
prev_state = serializer.instance.insight_logs_serialized
serializer.save()
new_state = serializer.instance.insight_logs_serialized
write_resource_insight_log(
instance=serializer.instance,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)
@action(methods=["post"], detail=True)
def copy(self, request, pk):
obj = self.get_object()
name = request.data.get("name")
team_id = request.data.get("team")
if team_id == NO_TEAM_VALUE:
team_id = None
if not name:
raise BadRequest(detail={"name": ["This field may not be null."]})
else:
if EscalationChain.objects.filter(organization=request.auth.organization, name=name).exists():
raise BadRequest(detail={"name": ["Escalation chain with this name already exists."]})
try:
team = request.user.available_teams.get(public_primary_key=team_id) if team_id else None
except Team.DoesNotExist:
return Response(data={"error_code": "wrong_team"}, status=status.HTTP_403_FORBIDDEN)
copy = obj.make_copy(name, team)
serializer = self.get_serializer(copy)
write_resource_insight_log(
instance=copy,
author=self.request.user,
event=EntityEvent.CREATED,
)
return Response(serializer.data)
@action(methods=["get"], detail=True)
def details(self, request, pk):
obj = self.get_object()
channel_filters = obj.channel_filters.filter(alert_receive_channel__deleted_at__isnull=True).values(
"public_primary_key",
"filtering_term",
"is_default",
"alert_receive_channel__public_primary_key",
"alert_receive_channel__verbal_name",
)
data = {}
for channel_filter in channel_filters:
channel_filter_data = {
"display_name": "Default Route" if channel_filter["is_default"] else channel_filter["filtering_term"],
"id": channel_filter["public_primary_key"],
}
data.setdefault(
channel_filter["alert_receive_channel__public_primary_key"],
{
"id": channel_filter["alert_receive_channel__public_primary_key"],
"display_name": emojize(channel_filter["alert_receive_channel__verbal_name"], language="alias"),
"channel_filters": [],
},
)["channel_filters"].append(channel_filter_data)
return Response(data.values())
@action(methods=["get"], detail=False)
def filters(self, request):
api_root = "/api/internal/v1/"
filter_options = [
{"name": "search", "type": "search"},
{
"name": "team",
"type": "team_select",
"href": api_root + "teams/",
"global": True,
},
]
return Response(filter_options)