diff --git a/assets b/assets index 51d42292a..fad73f395 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 51d42292abdc12d512d0b4acb170efa2f0524a12 +Subproject commit fad73f39573256159ae264d3897bb85ce6e0d95e diff --git a/dref/test_dref3_filters.py b/dref/test_dref3_filters.py index 289bceb37..69a71aa99 100644 --- a/dref/test_dref3_filters.py +++ b/dref/test_dref3_filters.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta from django.contrib.auth import get_user_model +from django.utils import timezone from rest_framework import status from api.models import Country, Region, RegionName @@ -145,14 +146,46 @@ def test_pagination(self): self.authenticate(self.superuser) full = self.client.get(self.url) self.assertEqual(full.status_code, status.HTTP_200_OK) - total = len(full.json()) + all_rows = full.json() + total_codes = {row["appeal_id"] for row in all_rows} page1 = self.client.get(self.url, {"limit": 1, "offset": 0}) self.assertEqual(page1.status_code, status.HTTP_200_OK) - assert len(page1.json()) == 1 - if total > 1: + page1_rows = page1.json() + assert len({row["appeal_id"] for row in page1_rows}) == 1 + if len(total_codes) > 1: page2 = self.client.get(self.url, {"limit": 1, "offset": 1}) self.assertEqual(page2.status_code, status.HTTP_200_OK) - assert len(page2.json()) == 1 + page2_rows = page2.json() + assert len({row["appeal_id"] for row in page2_rows}) == 1 + assert {row["appeal_id"] for row in page2_rows} != {row["appeal_id"] for row in page1_rows} + + def test_pagination_order_by_created_at(self): + self.authenticate(self.superuser) + now = timezone.now() + self.dref_a.created_at = now - timedelta(days=2) + self.dref_a.save(update_fields=["created_at"]) + self.dref_b.created_at = now - timedelta(days=1) + self.dref_b.save(update_fields=["created_at"]) + + page1 = self.client.get(self.url, {"limit": 1, "offset": 0, "order_by": "created_at"}) + self.assertEqual(page1.status_code, status.HTTP_200_OK) + page1_codes = {row["appeal_id"] for row in page1.json()} + assert page1_codes == {"APPEAL_A"} + + page2 = self.client.get(self.url, {"limit": 1, "offset": 1, "order_by": "created_at"}) + self.assertEqual(page2.status_code, status.HTTP_200_OK) + page2_codes = {row["appeal_id"] for row in page2.json()} + assert page2_codes == {"APPEAL_B"} + + page1_desc = self.client.get(self.url, {"limit": 1, "offset": 0, "order_by": "-created_at"}) + self.assertEqual(page1_desc.status_code, status.HTTP_200_OK) + page1_desc_codes = {row["appeal_id"] for row in page1_desc.json()} + assert page1_desc_codes == {"APPEAL_B"} + + page2_desc = self.client.get(self.url, {"limit": 1, "offset": 1, "order_by": "-created_at"}) + self.assertEqual(page2_desc.status_code, status.HTTP_200_OK) + page2_desc_codes = {row["appeal_id"] for row in page2_desc.json()} + assert page2_desc_codes == {"APPEAL_A"} def test_export_csv(self): self.authenticate(self.superuser) diff --git a/dref/views.py b/dref/views.py index 23bb51e71..d90a0f3ca 100644 --- a/dref/views.py +++ b/dref/views.py @@ -8,7 +8,7 @@ from django.http import HttpResponse from django.templatetags.static import static from django.utils.translation import gettext -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import ( mixins, permissions, @@ -464,6 +464,52 @@ def _status_to_int(self, raw): name_map = {s.name.lower(): s.value for s in Dref.Status} return label_map.get(str(raw).lower()) or name_map.get(str(raw).lower()) + def _order_codes(self, codes, request): + order_by = request.query_params.get("order_by") + if order_by not in ("created_at", "-created_at"): + return sorted(codes) + + created_map = { + row["appeal_code"]: row["first_created_at"] + for row in Dref.objects.filter(appeal_code__in=codes) + .values("appeal_code") + .annotate(first_created_at=models.Min("created_at")) + } + + present = [code for code in codes if created_map.get(code) is not None] + missing = sorted([code for code in codes if created_map.get(code) is None]) + present_sorted = sorted(present, key=lambda code: (created_map.get(code), code), reverse=order_by == "-created_at") + return present_sorted + missing + + def _paginate_codes(self, codes, request): + try: + limit = int(request.query_params.get("limit")) if request.query_params.get("limit") else None + except ValueError: + limit = None + try: + offset = int(request.query_params.get("offset")) if request.query_params.get("offset") else 0 + except ValueError: + offset = 0 + + if not offset and limit is None: + return codes + + end = offset + limit if limit is not None else None + return codes[offset:end] + + @extend_schema( + parameters=[ + OpenApiParameter( + name="order_by", + description=( + "Ordering for paged appeal codes. Use 'created_at' or '-created_at' to sort by the first " + "DREF application created_at per appeal_code; any other value defaults to appeal_code ordering." + ), + required=False, + type=str, + ) + ] + ) def list(self, request): # === First approach – would be nice to work like this, but recent definitons are more complex than that: # # Aggregate all appeal-codes from the three models @@ -564,7 +610,7 @@ def list(self, request): combined = set() for s in codes_sets: combined.update([c for c in s if c]) - codes = sorted(combined) + codes = list(combined) # Additional date range filters (applied to root Dref only where fields exist) date_range_fields = [ @@ -592,6 +638,9 @@ def list(self, request): if excluded_codes: codes = [c for c in codes if c and c.upper() not in excluded_codes] + codes = self._order_codes(codes, request) + codes = self._paginate_codes(codes, request) + data = [] old_kwargs = getattr(self, "kwargs", {}).copy() self.kwargs = {self.lookup_field: codes} @@ -630,20 +679,7 @@ def list(self, request): if id_param: if wanted_ids := {i.strip() for i in str(id_param).split(",")}: data = [row for row in data if row.get("id") in wanted_ids] - # pagination - try: - limit = int(request.query_params.get("limit")) if request.query_params.get("limit") else None - except ValueError: - limit = None - try: - offset = int(request.query_params.get("offset")) if request.query_params.get("offset") else 0 - except ValueError: - offset = 0 - if offset or limit is not None: - end = offset + limit if limit is not None else None - data_paginated = data[offset:end] - else: - data_paginated = data + data_paginated = data export_param = request.query_params.get("export") if export_param and export_param.lower() == "csv":