2022-06-03 08:09:47 -06:00
|
|
|
from django.db.models import Count, Q
|
2023-03-22 00:57:20 +08:00
|
|
|
from django_filters import rest_framework as filters
|
2024-10-24 11:24:36 +02:00
|
|
|
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_view, inline_serializer
|
2022-06-03 08:09:47 -06:00
|
|
|
from emoji import emojize
|
2024-10-24 11:24:36 +02:00
|
|
|
from rest_framework import serializers, status, viewsets
|
2022-06-03 08:09:47 -06:00
|
|
|
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
|
2022-11-29 09:41:56 +01:00
|
|
|
from apps.api.permissions import RBACPermission
|
2023-03-14 14:38:18 +00:00
|
|
|
from apps.api.serializers.escalation_chain import (
|
|
|
|
|
EscalationChainListSerializer,
|
|
|
|
|
EscalationChainSerializer,
|
|
|
|
|
FilterEscalationChainSerializer,
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
from apps.auth_token.auth import PluginAuthentication
|
2023-09-26 16:31:26 +01:00
|
|
|
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
2023-03-30 12:43:00 +03:00
|
|
|
from apps.user_management.models import Team
|
2022-06-03 08:09:47 -06:00
|
|
|
from common.api_helpers.exceptions import BadRequest
|
2023-11-23 14:28:00 -03:00
|
|
|
from common.api_helpers.filters import (
|
|
|
|
|
NO_TEAM_VALUE,
|
|
|
|
|
ByTeamModelFieldFilterMixin,
|
|
|
|
|
ModelFieldFilterMixin,
|
|
|
|
|
TeamModelMultipleChoiceFilter,
|
|
|
|
|
)
|
2023-03-14 14:38:18 +00:00
|
|
|
from common.api_helpers.mixins import (
|
|
|
|
|
FilterSerializerMixin,
|
|
|
|
|
ListSerializerMixin,
|
|
|
|
|
PublicPrimaryKeyMixin,
|
|
|
|
|
TeamFilteringMixin,
|
|
|
|
|
)
|
2022-08-24 12:04:44 +05:00
|
|
|
from common.insight_log import EntityEvent, write_resource_insight_log
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2023-03-22 00:57:20 +08:00
|
|
|
class EscalationChainFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
|
|
|
|
|
team = TeamModelMultipleChoiceFilter()
|
|
|
|
|
|
|
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
@extend_schema_view(
|
|
|
|
|
list=extend_schema(
|
|
|
|
|
responses=PolymorphicProxySerializer(
|
|
|
|
|
component_name="EscalationChainPolymorphic",
|
|
|
|
|
serializers=[EscalationChainListSerializer, FilterEscalationChainSerializer],
|
|
|
|
|
resource_type_field_name=None,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
update=extend_schema(responses=EscalationChainSerializer),
|
|
|
|
|
partial_update=extend_schema(responses=EscalationChainSerializer),
|
|
|
|
|
)
|
2023-03-14 14:38:18 +00:00
|
|
|
class EscalationChainViewSet(
|
|
|
|
|
TeamFilteringMixin,
|
2024-01-30 13:07:19 -05:00
|
|
|
PublicPrimaryKeyMixin[EscalationChain],
|
2023-03-14 14:38:18 +00:00
|
|
|
FilterSerializerMixin,
|
|
|
|
|
ListSerializerMixin,
|
|
|
|
|
viewsets.ModelViewSet,
|
|
|
|
|
):
|
2024-10-24 11:24:36 +02:00
|
|
|
"""
|
|
|
|
|
Internal API endpoints for escalation chains.
|
|
|
|
|
"""
|
|
|
|
|
|
2023-09-26 16:31:26 +01:00
|
|
|
authentication_classes = (
|
|
|
|
|
MobileAppAuthTokenAuthentication,
|
|
|
|
|
PluginAuthentication,
|
|
|
|
|
)
|
2022-11-29 09:41:56 +01:00
|
|
|
permission_classes = (IsAuthenticated, RBACPermission)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2022-11-29 09:41:56 +01:00
|
|
|
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],
|
2023-03-22 00:57:20 +08:00
|
|
|
"filters": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
|
2022-06-03 08:09:47 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
queryset = EscalationChain.objects.none() # needed for drf-spectacular introspection
|
|
|
|
|
|
2023-03-22 00:57:20 +08:00
|
|
|
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
|
2023-03-20 15:51:39 +01:00
|
|
|
search_fields = ("name",)
|
2023-03-22 00:57:20 +08:00
|
|
|
filterset_class = EscalationChainFilter
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
serializer_class = EscalationChainSerializer
|
|
|
|
|
list_serializer_class = EscalationChainListSerializer
|
2023-03-22 00:57:20 +08:00
|
|
|
|
2023-03-14 14:38:18 +00:00
|
|
|
filter_serializer_class = FilterEscalationChainSerializer
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-03-22 00:57:20 +08:00
|
|
|
def get_queryset(self, ignore_filtering_by_available_teams=False):
|
2023-03-14 14:38:18 +00:00
|
|
|
is_filters_request = self.request.query_params.get("filters", "false") == "true"
|
|
|
|
|
|
|
|
|
|
queryset = EscalationChain.objects.filter(
|
|
|
|
|
organization=self.request.auth.organization,
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-22 00:57:20 +08:00
|
|
|
if not ignore_filtering_by_available_teams:
|
2023-03-22 15:43:32 +08:00
|
|
|
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
|
2023-03-22 00:57:20 +08:00
|
|
|
|
2023-03-14 14:38:18 +00:00
|
|
|
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),
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
2023-03-14 14:38:18 +00:00
|
|
|
).annotate(
|
|
|
|
|
num_routes=Count(
|
|
|
|
|
"channel_filters",
|
|
|
|
|
distinct=True,
|
|
|
|
|
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return queryset
|
|
|
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
|
|
|
|
serializer.save()
|
2022-08-24 12:04:44 +05:00
|
|
|
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
def perform_destroy(self, instance):
|
2022-08-24 12:04:44 +05:00
|
|
|
write_resource_insight_log(
|
|
|
|
|
instance=instance,
|
|
|
|
|
author=self.request.user,
|
|
|
|
|
event=EntityEvent.DELETED,
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
2022-08-24 12:04:44 +05:00
|
|
|
instance.delete()
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
def perform_update(self, serializer):
|
2022-08-24 12:04:44 +05:00
|
|
|
prev_state = serializer.instance.insight_logs_serialized
|
2022-06-03 08:09:47 -06:00
|
|
|
serializer.save()
|
2022-08-24 12:04:44 +05:00
|
|
|
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,
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
|
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
@extend_schema(responses=EscalationChainSerializer)
|
2022-06-03 08:09:47 -06:00
|
|
|
@action(methods=["post"], detail=True)
|
|
|
|
|
def copy(self, request, pk):
|
2023-06-07 13:10:53 +01:00
|
|
|
obj = self.get_object()
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
name = request.data.get("name")
|
2023-03-30 12:43:00 +03:00
|
|
|
team_id = request.data.get("team")
|
2023-11-23 14:28:00 -03:00
|
|
|
if team_id == NO_TEAM_VALUE:
|
2023-03-30 12:43:00 +03:00
|
|
|
team_id = None
|
|
|
|
|
|
|
|
|
|
if not name:
|
2022-06-03 08:09:47 -06:00
|
|
|
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."]})
|
|
|
|
|
|
2023-03-30 12:43:00 +03:00
|
|
|
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)
|
2022-06-03 08:09:47 -06:00
|
|
|
serializer = self.get_serializer(copy)
|
2022-08-24 12:04:44 +05:00
|
|
|
write_resource_insight_log(
|
|
|
|
|
instance=copy,
|
|
|
|
|
author=self.request.user,
|
|
|
|
|
event=EntityEvent.CREATED,
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
return Response(serializer.data)
|
|
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
@extend_schema(
|
|
|
|
|
responses=inline_serializer(
|
|
|
|
|
name="EscalationChainDetails",
|
|
|
|
|
fields={
|
|
|
|
|
"id": serializers.CharField(),
|
|
|
|
|
"display_name": serializers.CharField(),
|
|
|
|
|
"channel_filters": inline_serializer(
|
|
|
|
|
name="EscalationChainDetailsChannelFilter",
|
|
|
|
|
fields={
|
|
|
|
|
"id": serializers.CharField(),
|
|
|
|
|
"display_name": serializers.CharField(),
|
|
|
|
|
},
|
|
|
|
|
many=True,
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
many=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
@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"],
|
Fix warnings when running backend tests (#2079)
# What this PR does
- update `make test` to always use `settings.ci-test`. Right now it will
use whatever the value of `DJANGO_SETTINGS_MODULE` is in
`./dev/.env.dev`, which causes ~45 tests to fail
- Fix several Python warnings that we see when running the tests
```bash
RemovedInDjango40Warning: The providing_args argument is deprecated. As it is purely documentational, it has no replacement. If you rely on this argument as documentation, you can move the text to a code comment or docstring.
alert_create_signal = django.dispatch.Signal(
```
```bash
PytestCollectionWarning: cannot collect test class 'TestOnlyBackend' because it has a __init__ constructor (from: apps/api/tests/test_alert_receive_channel_template.py)
class TestOnlyBackend(BaseMessagingBackend):
```
```bash
DeprecationWarning: The parameter 'use_aliases' in emoji.emojize() is deprecated and will be removed in version 2.0.0. Use language='alias' instead.
To hide this warning, pin/downgrade the package to 'emoji~=1.6.3'
return emoji.emojize(self.verbal_name, use_aliases=True)
```
```bash
DateTimeField CustomOnCallShift.start received a naive datetime (2023-06-01 12:53:12) while time zone support is active.
warnings.warn("DateTimeField %s received a naive datetime (%s)"
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
/etc/app/apps/twilioapp/tests/test_phone_calls.py:173: DeprecationWarning: The 'text' argument to find()-type methods is deprecated. Use 'string' instead.
content = BeautifulSoup(content, features="html.parser").findAll(text=True)
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone
apps/twilioapp/tests/test_phone_calls.py::test_wrong_pressed_digit
/usr/local/lib/python3.11/site-packages/bs4/builder/__init__.py:545: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor.
```
```bash
apps/twilioapp/tests/test_phone_calls.py::test_forbidden_requests
/usr/local/lib/python3.11/site-packages/social_django/urls.py:15: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path().
url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
```
```bash
apps/twilioapp/tests/test_phone_calls.py: 66 warnings
/usr/local/lib/python3.11/site-packages/debug_toolbar/utils.py:255: DeprecationWarning: currentThread() is deprecated, use current_thread() instead
thread = threading.currentThread()
```
## Checklist
- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
2023-06-06 20:38:00 +02:00
|
|
|
"display_name": emojize(channel_filter["alert_receive_channel__verbal_name"], language="alias"),
|
2022-06-03 08:09:47 -06:00
|
|
|
"channel_filters": [],
|
|
|
|
|
},
|
|
|
|
|
)["channel_filters"].append(channel_filter_data)
|
|
|
|
|
return Response(data.values())
|
2023-03-22 00:57:20 +08:00
|
|
|
|
2024-10-24 11:24:36 +02:00
|
|
|
@extend_schema(
|
|
|
|
|
responses=inline_serializer(
|
|
|
|
|
name="EscalationChainFilters",
|
|
|
|
|
fields={
|
|
|
|
|
"name": serializers.CharField(),
|
|
|
|
|
"type": serializers.CharField(),
|
|
|
|
|
"href": serializers.CharField(required=False),
|
|
|
|
|
"global": serializers.BooleanField(required=False),
|
|
|
|
|
},
|
|
|
|
|
many=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-03-22 00:57:20 +08:00
|
|
|
@action(methods=["get"], detail=False)
|
|
|
|
|
def filters(self, request):
|
|
|
|
|
api_root = "/api/internal/v1/"
|
|
|
|
|
|
|
|
|
|
filter_options = [
|
2024-07-22 11:30:28 +01:00
|
|
|
{"name": "search", "type": "search"},
|
2023-03-22 00:57:20 +08:00
|
|
|
{
|
|
|
|
|
"name": "team",
|
|
|
|
|
"type": "team_select",
|
|
|
|
|
"href": api_root + "teams/",
|
|
|
|
|
"global": True,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return Response(filter_options)
|