From 9adcd298c410ef56ae7ced70b4b77893049a34fa Mon Sep 17 00:00:00 2001 From: Elisa Anguita Date: Tue, 26 Nov 2024 16:07:28 -0300 Subject: [PATCH 1/3] feat(api): Enable filtering courts by parent court id --- cl/search/filters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cl/search/filters.py b/cl/search/filters.py index d7f11e472c..6f1d6f6603 100644 --- a/cl/search/filters.py +++ b/cl/search/filters.py @@ -28,6 +28,10 @@ class CourtFilter(NoEmptyFilterSet): "cl.search.filters.DocketFilter", queryset=Docket.objects.all() ) jurisdiction = filters.MultipleChoiceFilter(choices=Court.JURISDICTIONS) + parent_court = filters.CharFilter( + field_name="parent_court__id", + lookup_expr="exact", + ) class Meta: model = Court From 5d0938681837ca5b1855dd040062f70ef01f2dde Mon Sep 17 00:00:00 2001 From: Elisa Anguita Date: Tue, 26 Nov 2024 20:59:29 -0300 Subject: [PATCH 2/3] test(api): Add tests for court filtering by parent_court --- cl/api/tests.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/cl/api/tests.py b/cl/api/tests.py index 238c0d04a7..275695b50f 100644 --- a/cl/api/tests.py +++ b/cl/api/tests.py @@ -86,7 +86,7 @@ TagViewSet, ) from cl.search.factories import CourtFactory, DocketFactory -from cl.search.models import SOURCES, Docket, Opinion +from cl.search.models import SOURCES, Court, Docket, Opinion from cl.stats.models import Event from cl.tests.cases import SimpleTestCase, TestCase, TransactionTestCase from cl.tests.utils import MockResponse, make_client @@ -673,6 +673,56 @@ async def assertCountInResults(self, expected_count): ) +class DRFCourtApiFilterTests(TestCase, FilteringCountTestCase): + @classmethod + def setUpTestData(cls): + Court.objects.all().delete() + + cls.parent_court = CourtFactory(id="parent1", full_name="Parent Court") + + cls.child_court1 = CourtFactory( + id="child1", + parent_court=cls.parent_court, + full_name="Child Court 1", + ) + cls.child_court2 = CourtFactory( + id="child2", + parent_court=cls.parent_court, + full_name="Child Court 2", + ) + + cls.orphan_court = CourtFactory(id="orphan", full_name="Orphan Court") + + @async_to_sync + async def setUp(self): + self.path = reverse("court-list", kwargs={"version": "v4"}) + self.q: Dict[str, Any] = {} + + async def test_parent_court_filter(self): + """Can we filter courts by parent_court id?""" + self.q["parent_court"] = "parent1" + await self.assertCountInResults(2) # Should return child1 and child2 + + # Verify the returned court IDs + response = await self.async_client.get(self.path, self.q) + court_ids = [court["id"] for court in response.data["results"]] + self.assertEqual(set(court_ids), {"child1", "child2"}) + + # Filter for courts with parent_court id='orphan' (none should match) + self.q["parent_court"] = "orphan" + await self.assertCountInResults(0) + + async def test_no_parent_court_filter(self): + """Do we get all courts when using no filters?""" + self.q = {} + await self.assertCountInResults(4) # Should return all four courts + + async def test_invalid_parent_court_filter(self): + """Do we handle invalid parent_court values correctly?""" + self.q["parent_court"] = "nonexistent" + await self.assertCountInResults(0) + + class DRFJudgeApiFilterTests( SimpleUserDataMixin, TestCase, FilteringCountTestCase ): From 9093b7f707088bdc794bd8fc56fd34b056f4cff2 Mon Sep 17 00:00:00 2001 From: Elisa Anguita Date: Tue, 26 Nov 2024 22:02:36 -0300 Subject: [PATCH 3/3] test(api): Add more tests for court filtering using other fields --- cl/api/tests.py | 157 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 8 deletions(-) diff --git a/cl/api/tests.py b/cl/api/tests.py index 275695b50f..94f068b819 100644 --- a/cl/api/tests.py +++ b/cl/api/tests.py @@ -1,5 +1,5 @@ import json -from datetime import date, timedelta +from datetime import date, datetime, timedelta, timezone from http import HTTPStatus from typing import Any, Dict from unittest import mock @@ -666,11 +666,18 @@ async def assertCountInResults(self, expected_count): f"the JSON: \n{r.json()}", ) got = len(r.data["results"]) + try: + path = r.request.get("path") + query_string = r.request.get("query_string") + url = f"{path}?{query_string}" + except AttributeError: + url = self.path self.assertEqual( got, expected_count, - msg=f"Expected {expected_count}, but got {got}.\n\nr.data was: {r.data}", + msg=f"Expected {expected_count}, but got {got} in {url}\n\nr.data was: {r.data}", ) + return r class DRFCourtApiFilterTests(TestCase, FilteringCountTestCase): @@ -678,21 +685,67 @@ class DRFCourtApiFilterTests(TestCase, FilteringCountTestCase): def setUpTestData(cls): Court.objects.all().delete() - cls.parent_court = CourtFactory(id="parent1", full_name="Parent Court") + cls.parent_court = CourtFactory( + id="parent1", + full_name="Parent Court", + short_name="PC", + citation_string="PC", + in_use=True, + has_opinion_scraper=True, + has_oral_argument_scraper=False, + position=1, + start_date=date(2000, 1, 1), + end_date=None, + jurisdiction=Court.FEDERAL_APPELLATE, + date_modified=datetime(2021, 1, 1, tzinfo=timezone.utc), + ) cls.child_court1 = CourtFactory( id="child1", parent_court=cls.parent_court, full_name="Child Court 1", + short_name="CC1", + citation_string="CC1", + in_use=False, + has_opinion_scraper=False, + has_oral_argument_scraper=True, + position=2, + start_date=date(2010, 6, 15), + end_date=date(2020, 12, 31), + jurisdiction=Court.STATE_SUPREME, + date_modified=datetime(2022, 6, 15, tzinfo=timezone.utc), ) cls.child_court2 = CourtFactory( id="child2", parent_court=cls.parent_court, full_name="Child Court 2", + short_name="CC2", + citation_string="CC2", + in_use=True, + has_opinion_scraper=False, + has_oral_argument_scraper=False, + position=3, + start_date=date(2015, 5, 20), + end_date=None, + jurisdiction=Court.STATE_TRIAL, + date_modified=datetime(2023, 3, 10, tzinfo=timezone.utc), + ) + + cls.orphan_court = CourtFactory( + id="orphan", + full_name="Orphan Court", + short_name="OC", + citation_string="OC", + in_use=True, + has_opinion_scraper=False, + has_oral_argument_scraper=False, + position=4, + start_date=date(2012, 8, 25), + end_date=None, + jurisdiction=Court.FEDERAL_DISTRICT, + date_modified=datetime(2023, 5, 5, tzinfo=timezone.utc), ) - cls.orphan_court = CourtFactory(id="orphan", full_name="Orphan Court") - @async_to_sync async def setUp(self): self.path = reverse("court-list", kwargs={"version": "v4"}) @@ -701,15 +754,15 @@ async def setUp(self): async def test_parent_court_filter(self): """Can we filter courts by parent_court id?""" self.q["parent_court"] = "parent1" - await self.assertCountInResults(2) # Should return child1 and child2 + # Should return child1 and child2: + response = await self.assertCountInResults(2) # Verify the returned court IDs - response = await self.async_client.get(self.path, self.q) court_ids = [court["id"] for court in response.data["results"]] self.assertEqual(set(court_ids), {"child1", "child2"}) # Filter for courts with parent_court id='orphan' (none should match) - self.q["parent_court"] = "orphan" + self.q = {"parent_court": "orphan"} await self.assertCountInResults(0) async def test_no_parent_court_filter(self): @@ -722,6 +775,94 @@ async def test_invalid_parent_court_filter(self): self.q["parent_court"] = "nonexistent" await self.assertCountInResults(0) + async def test_id_filter(self): + """Can we filter courts by id?""" + self.q["id"] = "child1" + response = await self.assertCountInResults(1) + self.assertEqual(response.data["results"][0]["id"], "child1") + + async def test_in_use_filter(self): + """Can we filter courts by in_use field?""" + self.q = {"in_use": "true"} + await self.assertCountInResults(3) # parent1, child2, orphan + self.q = {"in_use": "false"} + await self.assertCountInResults(1) # child1 + + async def test_has_opinion_scraper_filter(self): + """Can we filter courts by has_opinion_scraper field?""" + self.q = {"has_opinion_scraper": "true"} + await self.assertCountInResults(1) # parent1 + self.q = {"has_opinion_scraper": "false"} + await self.assertCountInResults(3) # child1, child2, orphan + + async def test_has_oral_argument_scraper_filter(self): + """Can we filter courts by has_oral_argument_scraper field?""" + self.q = {"has_oral_argument_scraper": "true"} + await self.assertCountInResults(1) # child1 + self.q = {"has_oral_argument_scraper": "false"} + await self.assertCountInResults(3) # parent1, child2, orphan + + async def test_position_filter(self): + """Can we filter courts by position with integer lookups?""" + self.q = {"position__gt": "2"} + await self.assertCountInResults(2) # child2 (3), orphan (4) + self.q = {"position__lte": "2"} + await self.assertCountInResults(2) # parent1 (1), child1 (2) + + async def test_start_date_filter(self): + """Can we filter courts by start_date with date lookups?""" + self.q = {"start_date__year": "2015"} + await self.assertCountInResults(1) # child2 (2015-05-20) + self.q = {"start_date__gte": "2010-01-01"} + await self.assertCountInResults(3) # child1, child2, orphan + + async def test_end_date_filter(self): + """Can we filter courts by end_date with date lookups?""" + self.q = {"end_date__day": "31"} + await self.assertCountInResults(1) # parent1, child2, orphan + self.q = {"end_date__year": "2024"} + await self.assertCountInResults(0) + + async def test_short_name_filter(self): + """Can we filter courts by short_name with text lookups?""" + self.q = {"short_name__iexact": "Cc1"} + await self.assertCountInResults(1) # child1 + self.q = {"short_name__icontains": "cc"} + await self.assertCountInResults(2) # child1, child2 + + async def test_full_name_filter(self): + """Can we filter courts by full_name with text lookups?""" + self.q = {"full_name__istartswith": "Child"} + await self.assertCountInResults(2) # child1, child2 + self.q = {"full_name__iendswith": "Court"} + await self.assertCountInResults(2) # parent1, orphan + + async def test_citation_string_filter(self): + """Can we filter courts by citation_string with text lookups?""" + self.q = {"citation_string": "OC"} + await self.assertCountInResults(1) # orphan + self.q = {"citation_string__icontains": "2"} + await self.assertCountInResults(1) # child2 + + async def test_jurisdiction_filter(self): + """Can we filter courts by jurisdiction?""" + self.q = { + "jurisdiction": [ + Court.FEDERAL_APPELLATE, + Court.FEDERAL_DISTRICT, + ] + } + await self.assertCountInResults(2) # parent1 and orphan + + async def test_combined_filters(self): + """Can we filter courts with multiple filters applied?""" + self.q = { + "in_use": "true", + "has_opinion_scraper": "false", + "position__gt": "2", + } + await self.assertCountInResults(2) # child2 and orphan + class DRFJudgeApiFilterTests( SimpleUserDataMixin, TestCase, FilteringCountTestCase