diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f51682..961d13b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added state filter for alert_group public API endpoint. + ## v1.1.16 (2023-01-12) ### Fixed diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index abd4e294..0d756a5a 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -46,6 +46,7 @@ These available filter parameters should be provided as `GET` arguments: - `route_id` - `integration_id` +- `state` **HTTP request** diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index e201d660..48ba98c8 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -348,6 +348,22 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. # https://code.djangoproject.com/ticket/28545 is_open_for_grouping = models.BooleanField(default=None, null=True, blank=True) + @staticmethod + def get_silenced_state_filter(): + return Q(silenced=True) & Q(acknowledged=False) & Q(resolved=False) + + @staticmethod + def get_new_state_filter(): + return Q(silenced=False) & Q(acknowledged=False) & Q(resolved=False) + + @staticmethod + def get_acknowledged_state_filter(): + return Q(acknowledged=True) & Q(resolved=False) + + @staticmethod + def get_resolved_state_filter(): + return Q(resolved=True) + class Meta: get_latest_by = "pk" unique_together = [ diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 91a49579..cf28d898 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -109,13 +109,13 @@ class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.Filt q_objects = Q() if AlertGroup.NEW in statuses: - filters["new"] = Q(silenced=False) & Q(acknowledged=False) & Q(resolved=False) + filters["new"] = AlertGroup.get_new_state_filter() if AlertGroup.SILENCED in statuses: - filters["silenced"] = Q(silenced=True) & Q(acknowledged=False) & Q(resolved=False) + filters["silenced"] = AlertGroup.get_silenced_state_filter() if AlertGroup.ACKNOWLEDGED in statuses: - filters["acknowledged"] = Q(acknowledged=True) & Q(resolved=False) + filters["acknowledged"] = AlertGroup.get_acknowledged_state_filter() if AlertGroup.RESOLVED in statuses: - filters["resolved"] = Q(resolved=True) + filters["resolved"] = AlertGroup.get_resolved_state_filter() for item in filters: q_objects |= filters[item] diff --git a/engine/apps/public_api/tests/test_incidents.py b/engine/apps/public_api/tests/test_incidents.py index 041bd3e5..4a08fe3f 100644 --- a/engine/apps/public_api/tests/test_incidents.py +++ b/engine/apps/public_api/tests/test_incidents.py @@ -116,6 +116,83 @@ def test_get_incidents_filter_by_integration( assert response.json() == expected_response +@pytest.mark.django_db +def test_get_incidents_filter_by_state_new( + incident_public_api_setup, +): + token, _, _, _ = incident_public_api_setup + incidents = AlertGroup.unarchived_objects.filter(AlertGroup.get_new_state_filter()).order_by("-started_at") + expected_response = construct_expected_response_from_incidents(incidents) + client = APIClient() + + url = reverse("api-public:alert_groups-list") + response = client.get(url + f"?state=new", format="json", HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + +@pytest.mark.django_db +def test_get_incidents_filter_by_state_acknowledged( + incident_public_api_setup, +): + token, _, _, _ = incident_public_api_setup + incidents = AlertGroup.unarchived_objects.filter(AlertGroup.get_acknowledged_state_filter()).order_by("-started_at") + expected_response = construct_expected_response_from_incidents(incidents) + client = APIClient() + + url = reverse("api-public:alert_groups-list") + response = client.get(url + f"?state=acknowledged", format="json", HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + +@pytest.mark.django_db +def test_get_incidents_filter_by_state_silenced( + incident_public_api_setup, +): + token, _, _, _ = incident_public_api_setup + incidents = AlertGroup.unarchived_objects.filter(AlertGroup.get_silenced_state_filter()).order_by("-started_at") + expected_response = construct_expected_response_from_incidents(incidents) + client = APIClient() + + url = reverse("api-public:alert_groups-list") + response = client.get(url + f"?state=silenced", format="json", HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + +@pytest.mark.django_db +def test_get_incidents_filter_by_state_resolved( + incident_public_api_setup, +): + token, _, _, _ = incident_public_api_setup + incidents = AlertGroup.unarchived_objects.filter(AlertGroup.get_resolved_state_filter()).order_by("-started_at") + expected_response = construct_expected_response_from_incidents(incidents) + client = APIClient() + + url = reverse("api-public:alert_groups-list") + response = client.get(url + f"?state=resolved", format="json", HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + +@pytest.mark.django_db +def test_get_incidents_filter_by_state_unknown( + incident_public_api_setup, +): + token, _, _, _ = incident_public_api_setup + client = APIClient() + + url = reverse("api-public:alert_groups-list") + response = client.get(url + f"?state=unknown", format="json", HTTP_AUTHORIZATION=f"{token}") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db def test_get_incidents_filter_by_integration_no_result( incident_public_api_setup, diff --git a/engine/apps/public_api/views/incidents.py b/engine/apps/public_api/views/incidents.py index cd4d6098..b771da28 100644 --- a/engine/apps/public_api/views/incidents.py +++ b/engine/apps/public_api/views/incidents.py @@ -1,3 +1,4 @@ +from django.db.models import Q from django_filters import rest_framework as filters from rest_framework import mixins, status from rest_framework.exceptions import NotFound @@ -45,6 +46,7 @@ class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyM def get_queryset(self): route_id = self.request.query_params.get("route_id", None) integration_id = self.request.query_params.get("integration_id", None) + state = self.request.query_params.get("state", None) queryset = AlertGroup.unarchived_objects.filter( channel__organization=self.request.auth.organization, @@ -54,6 +56,25 @@ class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyM queryset = queryset.filter(channel_filter__public_primary_key=route_id) if integration_id: queryset = queryset.filter(channel__public_primary_key=integration_id) + if state: + choices = dict(AlertGroup.STATUS_CHOICES) + try: + choice = [i for i in choices if choices[i] == state.lower().capitalize()][0] + status_filter = Q() + if choice == AlertGroup.NEW: + status_filter = AlertGroup.get_new_state_filter() + elif choice == AlertGroup.SILENCED: + status_filter = AlertGroup.get_silenced_state_filter() + elif choice == AlertGroup.ACKNOWLEDGED: + status_filter = AlertGroup.get_acknowledged_state_filter() + elif choice == AlertGroup.RESOLVED: + status_filter = AlertGroup.get_resolved_state_filter() + queryset = queryset.filter(status_filter) + except IndexError: + valid_choices_text = ", ".join( + [status_choice[1].lower() for status_choice in AlertGroup.STATUS_CHOICES] + ) + raise BadRequest(detail={"state": f"Must be one of the following: {valid_choices_text}"}) queryset = self.serializer_class.setup_eager_loading(queryset)