diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index 1205429e..140276d6 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -125,6 +125,7 @@ class AlertGroupListSerializer( PREFETCH_RELATED = [ "dependent_alert_groups", "log_records__author", + "labels", ] SELECT_RELATED = [ diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 5ff8cc81..d5a1d1d4 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -863,6 +863,72 @@ def test_get_filter_escalation_chain( assert len(response.data["results"]) == 2 +@pytest.mark.django_db +def test_get_filter_by_teams( + make_organization_and_user_with_plugin_token, + make_team, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_user_auth_headers, +): + client = APIClient() + organization, user, token = make_organization_and_user_with_plugin_token() + team1 = make_team(organization) + team2 = make_team(organization) + + alert_receive_channel_0 = make_alert_receive_channel(organization) + alert_receive_channel_1 = make_alert_receive_channel(organization, team=team1) + alert_receive_channel_2 = make_alert_receive_channel(organization, team=team2) + + alert_group_0 = make_alert_group(alert_receive_channel_0) + make_alert(alert_group=alert_group_0, raw_request_data=alert_raw_request_data) + + alert_group_1 = make_alert_group(alert_receive_channel_1) + make_alert(alert_group=alert_group_1, raw_request_data=alert_raw_request_data) + + alert_group_2 = make_alert_group(alert_receive_channel_2) + make_alert(alert_group=alert_group_2, raw_request_data=alert_raw_request_data) + + url = reverse("api-internal:alertgroup-list") + + # check no team is given + response = client.get(url, **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 3 + assert {ag["pk"] for ag in response.data["results"]} == { + alert_group_0.public_primary_key, + alert_group_1.public_primary_key, + alert_group_2.public_primary_key, + } + + # check the "No team" case + response = client.get(url + "?team=null", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 1 + assert {ag["pk"] for ag in response.data["results"]} == {alert_group_0.public_primary_key} + + # check the "No team" + other team case + response = client.get(url + f"?team=null&team={team2.public_primary_key}", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 2 + assert {ag["pk"] for ag in response.data["results"]} == { + alert_group_0.public_primary_key, + alert_group_2.public_primary_key, + } + + # check the multiple teams case + response = client.get( + url + f"?team={team1.public_primary_key}&team={team2.public_primary_key}", **make_user_auth_headers(user, token) + ) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 2 + assert {ag["pk"] for ag in response.data["results"]} == { + alert_group_1.public_primary_key, + alert_group_2.public_primary_key, + } + + @pytest.mark.django_db def test_get_filter_labels( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/tests/test_escalation_chain.py b/engine/apps/api/tests/test_escalation_chain.py index 9d06af88..1bf25489 100644 --- a/engine/apps/api/tests/test_escalation_chain.py +++ b/engine/apps/api/tests/test_escalation_chain.py @@ -5,6 +5,8 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from common.api_helpers.filters import NO_TEAM_VALUE + @pytest.fixture() def escalation_chain_internal_api_setup(make_organization_and_user_with_plugin_token, make_escalation_chain): @@ -103,7 +105,7 @@ def test_escalation_chain_copy( escalation_chain = make_escalation_chain(organization, team=team) data = { "name": "escalation_chain_updated", - "team": new_team.public_primary_key if new_team else "null", + "team": new_team.public_primary_key if new_team else NO_TEAM_VALUE, } client = APIClient() @@ -125,6 +127,8 @@ def test_escalation_chain_copy_empty_name( client = APIClient() url = reverse("api-internal:escalation_chain-copy", kwargs={"pk": escalation_chain.public_primary_key}) - response = client.post(url, {"name": "", "team": "null"}, format="json", **make_user_auth_headers(user, token)) + response = client.post( + url, {"name": "", "team": NO_TEAM_VALUE}, format="json", **make_user_auth_headers(user, token) + ) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/engine/apps/api/tests/test_team.py b/engine/apps/api/tests/test_team.py index f66b4436..b438ebbd 100644 --- a/engine/apps/api/tests/test_team.py +++ b/engine/apps/api/tests/test_team.py @@ -10,8 +10,9 @@ from apps.alerts.models import AlertReceiveChannel from apps.api.permissions import LegacyAccessControlRole from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleWeb from apps.user_management.models import Team +from common.api_helpers.filters import NO_TEAM_VALUE -GENERAL_TEAM = Team(public_primary_key="null", name="No team", email=None, avatar_url=None) +GENERAL_TEAM = Team(public_primary_key=NO_TEAM_VALUE, name="No team", email=None, avatar_url=None) def get_payload_from_team(team, long=False): @@ -203,7 +204,7 @@ def test_teams_number_of_users_currently_oncall_attribute_works_properly( team1.public_primary_key: 2, team2.public_primary_key: 1, team3.public_primary_key: 0, - "null": 0, # this covers the case of "No team" + NO_TEAM_VALUE: 0, # this covers the case of "No team" } for team in response.json(): diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index c5daecbd..860c7092 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -28,12 +28,7 @@ from apps.labels.utils import is_labels_feature_enabled from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from apps.user_management.models import Team, User from common.api_helpers.exceptions import BadRequest -from common.api_helpers.filters import ( - ByTeamModelFieldFilterMixin, - DateRangeFilterMixin, - ModelFieldFilterMixin, - TeamModelMultipleChoiceFilter, -) +from common.api_helpers.filters import NO_TEAM_VALUE, DateRangeFilterMixin, ModelFieldFilterMixin from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin, TeamFilteringMixin from common.api_helpers.paginators import TwentyFiveCursorPaginator @@ -84,7 +79,7 @@ class AlertGroupFilterBackend(filters.DjangoFilterBackend): return filterset -class AlertGroupFilter(DateRangeFilterMixin, ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet): +class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.FilterSet): """ Examples of possible date formats here https://docs.djangoproject.com/en/1.9/ref/settings/#datetime-input-formats """ @@ -141,7 +136,6 @@ class AlertGroupFilter(DateRangeFilterMixin, ByTeamModelFieldFilterMixin, ModelF ) with_resolution_note = filters.BooleanFilter(method="filter_with_resolution_note") mine = filters.BooleanFilter(method="filter_mine") - team = TeamModelMultipleChoiceFilter(field_name="channel__team") class Meta: model = AlertGroup @@ -337,6 +331,16 @@ class AlertGroupView( if not ignore_filtering_by_available_teams: alert_receive_channels_qs = alert_receive_channels_qs.filter(*self.available_teams_lookup_args) + # Filter by team(s). Since we really filter teams from integrations, this is not an AlertGroup model filter. + # This is based on the common.api_helpers.ByTeamModelFieldFilterMixin implementation + team_values = self.request.query_params.getlist("team", []) + if team_values: + null_team_lookup = Q(team__isnull=True) if NO_TEAM_VALUE in team_values else None + teams_lookup = Q(team__public_primary_key__in=[ppk for ppk in team_values if ppk != NO_TEAM_VALUE]) + if null_team_lookup: + teams_lookup = teams_lookup | null_team_lookup + alert_receive_channels_qs = alert_receive_channels_qs.filter(teams_lookup) + alert_receive_channels_ids = list(alert_receive_channels_qs.values_list("id", flat=True)) queryset = AlertGroup.objects.filter(channel__in=alert_receive_channels_ids) diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 950af614..9edab2dc 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -25,7 +25,7 @@ from apps.integrations.legacy_prefix import has_legacy_prefix, remove_legacy_pre from apps.labels.utils import is_labels_feature_enabled from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from common.api_helpers.exceptions import BadRequest -from common.api_helpers.filters import ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter +from common.api_helpers.filters import NO_TEAM_VALUE, ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter from common.api_helpers.mixins import ( FilterSerializerMixin, PreviewTemplateException, @@ -231,7 +231,7 @@ class AlertReceiveChannelView( raise BadRequest(detail="team_id must be specified") team_id = request.query_params["team_id"] - if team_id == "null": + if team_id == NO_TEAM_VALUE: team_id = None try: diff --git a/engine/apps/api/views/escalation_chain.py b/engine/apps/api/views/escalation_chain.py index c125b031..00c5c3d4 100644 --- a/engine/apps/api/views/escalation_chain.py +++ b/engine/apps/api/views/escalation_chain.py @@ -18,7 +18,12 @@ 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 ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter +from common.api_helpers.filters import ( + NO_TEAM_VALUE, + ByTeamModelFieldFilterMixin, + ModelFieldFilterMixin, + TeamModelMultipleChoiceFilter, +) from common.api_helpers.mixins import ( FilterSerializerMixin, ListSerializerMixin, @@ -128,7 +133,7 @@ class EscalationChainViewSet( name = request.data.get("name") team_id = request.data.get("team") - if team_id == "null": + if team_id == NO_TEAM_VALUE: team_id = None if not name: diff --git a/engine/apps/api/views/team.py b/engine/apps/api/views/team.py index 650d8025..f2b25e82 100644 --- a/engine/apps/api/views/team.py +++ b/engine/apps/api/views/team.py @@ -10,6 +10,7 @@ from apps.auth_token.auth import PluginAuthentication from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from apps.schedules.ical_utils import get_cached_oncall_users_for_multiple_schedules from apps.user_management.models import Team +from common.api_helpers.filters import NO_TEAM_VALUE from common.api_helpers.mixins import PublicPrimaryKeyMixin @@ -62,7 +63,7 @@ class TeamViewSet( return TeamLongSerializer if self._is_long_request() else TeamSerializer def list(self, request, *args, **kwargs): - general_team = [Team(public_primary_key="null", name="No team", email=None, avatar_url=None)] + general_team = [Team(public_primary_key=NO_TEAM_VALUE, name="No team", email=None, avatar_url=None)] queryset = self.filter_queryset(self.get_queryset()) if self.request.query_params.get("only_include_notifiable_teams", "false") == "true": diff --git a/engine/apps/public_api/views/incidents.py b/engine/apps/public_api/views/incidents.py index 4b612921..27fc71b4 100644 --- a/engine/apps/public_api/views/incidents.py +++ b/engine/apps/public_api/views/incidents.py @@ -16,7 +16,7 @@ from apps.public_api.helpers import is_valid_group_creation_date, team_has_slack from apps.public_api.serializers import IncidentSerializer from apps.public_api.throttlers.user_throttle import UserThrottle from common.api_helpers.exceptions import BadRequest -from common.api_helpers.filters import ByTeamModelFieldFilterMixin, get_team_queryset +from common.api_helpers.filters import NO_TEAM_VALUE, ByTeamModelFieldFilterMixin, get_team_queryset from common.api_helpers.mixins import RateLimitHeadersMixin from common.api_helpers.paginators import FiftyPageSizePaginator @@ -27,7 +27,7 @@ class IncidentByTeamFilter(ByTeamModelFieldFilterMixin, filters.FilterSet): queryset=get_team_queryset, to_field_name="public_primary_key", null_label="noteam", - null_value="null", + null_value=NO_TEAM_VALUE, method=ByTeamModelFieldFilterMixin.filter_model_field_with_single_value.__name__, ) diff --git a/engine/common/api_helpers/filters.py b/engine/common/api_helpers/filters.py index 21d5ac3f..7e12a140 100644 --- a/engine/common/api_helpers/filters.py +++ b/engine/common/api_helpers/filters.py @@ -7,6 +7,8 @@ from django_filters.utils import handle_timezone from apps.user_management.models import Team from common.api_helpers.exceptions import BadRequest +NO_TEAM_VALUE = "null" + class DateRangeFilterMixin: DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" @@ -100,7 +102,7 @@ class ByTeamFilter(ByTeamModelFieldFilterMixin, filters.FilterSet): queryset=get_team_queryset, to_field_name="public_primary_key", null_label="noteam", - null_value="null", + null_value=NO_TEAM_VALUE, method=ByTeamModelFieldFilterMixin.filter_model_field_with_single_value.__name__, ) @@ -112,7 +114,7 @@ class TeamModelMultipleChoiceFilter(filters.ModelMultipleChoiceFilter): queryset=get_team_queryset, to_field_name="public_primary_key", null_label="noteam", - null_value="null", + null_value=NO_TEAM_VALUE, method=ByTeamModelFieldFilterMixin.filter_model_field_with_multiple_values.__name__, ): super().__init__(