From fdff780624118ed365b86042bbba210870cc4c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Jes=C3=BAs=20Pe=C3=B1a=20Rodr=C3=ADguez?= Date: Thu, 9 Apr 2026 12:49:48 +0200 Subject: [PATCH] feat: add status to sort param --- api/src/backend/api/tests/test_views.py | 62 +++++++++++++++++++++++++ api/src/backend/api/v1/views.py | 14 ++++++ 2 files changed, 76 insertions(+) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 439a355193..ae22bc23dd 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -16872,6 +16872,68 @@ class TestFindingGroupViewSet: asc_keys = [delta_key(item) for item in response.json()["data"]] assert asc_keys == sorted(asc_keys) + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_sort_by_status( + self, + authenticated_client, + finding_groups_fixture, + endpoint_name, + ): + """Sort by status orders by aggregated status (FAIL > PASS > MUTED).""" + status_order = {"FAIL": 3, "PASS": 2, "MUTED": 1} + + # Descending: FAIL groups first, then PASS + params = {"sort": "-status"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + + desc_statuses = [item["attributes"]["status"] for item in data] + desc_keys = [status_order[s] for s in desc_statuses] + assert desc_keys == sorted(desc_keys, reverse=True) + + # Ascending: PASS groups first, then FAIL + params["sort"] = "status" + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + asc_statuses = [ + item["attributes"]["status"] for item in response.json()["data"] + ] + asc_keys = [status_order[s] for s in asc_statuses] + assert asc_keys == sorted(asc_keys) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_sort_by_status_includes_muted( + self, + authenticated_client, + finding_groups_fixture, + endpoint_name, + ): + """When include_muted is set, MUTED groups participate in status sort.""" + status_order = {"FAIL": 3, "PASS": 2, "MUTED": 1} + + params = {"sort": "status", "filter[include_muted]": "true"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + + statuses = [item["attributes"]["status"] for item in data] + assert "MUTED" in statuses + assert statuses[0] == "MUTED" + keys = [status_order[s] for s in statuses] + assert keys == sorted(keys) + def test_finding_groups_latest_ignores_date_filters( self, authenticated_client, finding_groups_fixture ): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 2392b818ac..1a0610c5a9 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -7219,6 +7219,7 @@ class FindingGroupViewSet(BaseRLSViewSet): "check_id": "check_id", "check_title": "check_title", "severity": "severity_order", + "status": "status_order", "delta": "delta_order", "fail_count": "fail_count", "pass_count": "pass_count", @@ -7570,6 +7571,19 @@ class FindingGroupViewSet(BaseRLSViewSet): sort_param, self._FINDING_GROUP_SORT_MAP ) if ordering: + # status_order is annotated on demand so groups can be sorted + # by their aggregated status (FAIL > PASS > MUTED), mirroring + # the priority used in _post_process_aggregation. + if any(field.lstrip("-") == "status_order" for field in ordering): + aggregated_queryset = aggregated_queryset.annotate( + status_order=Case( + When(fail_count__gt=0, then=Value(3)), + When(pass_count__gt=0, then=Value(2)), + default=Value(1), + output_field=IntegerField(), + ) + ) + # delta_order is a virtual sort field: expand it to a # lexicographic ordering by (new_count, changed_count) so groups # with more new findings rank higher, with changed_count as the