Skip to content

Commit

Permalink
Merge pull request #4729 from freelawproject/4054-add-nested-results-…
Browse files Browse the repository at this point in the history
…filtering

feat(api): Adds logic to filter nested results
  • Loading branch information
mlissner authored Dec 6, 2024
2 parents d1ccf3a + 4b686e3 commit d5aa1d0
Show file tree
Hide file tree
Showing 5 changed files with 541 additions and 32 deletions.
5 changes: 4 additions & 1 deletion cl/api/templates/pacer-api-docs-vlatest.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ <h2 id="party-endpoint">Parties <small> — <code>{% url "party-list" version=ve
</p>
<p>This API can be filtered by docket ID to show all the parties for a particular case.
</p>
<p class="alert alert-warning"><i class="fa fa-warning"></i> <strong>Listen Up:</strong> Filters apply to this endpoint, not the data nested within it. Therefore, each party returned by this API will list all the attorneys that have represented them in any case, even if the parties themselves are filtered to a particular case.
<p class="alert alert-warning"><i class="fa fa-warning"></i> <strong>Listen Up:</strong> Filters applied to this endpoint only affect the top-level data, not the data nested records within it. Therefore, each party returned by this API will list all the attorneys that have represented them in any case, even if the parties themselves are filtered to a particular case. <br><br>
To filter the nested attorney data for each party, include the <code>filter_nested_results=True</code> parameter in your API request.
</p>
<p>For example, this query returns the parties for docket number <code>123</code>:</p>
<pre class="pre-scrollable">curl -v \
Expand Down Expand Up @@ -281,6 +282,8 @@ <h2 id="attorney-endpoint">Attorneys <small> — <code>{% url "attorney-list" ve
<p>To look up field descriptions or options for filtering, ordering, or rendering, complete an HTTP OPTIONS request.</p>
<p>Like docket entries and parties, attorneys can be filtered to a particular docket. For example:
</p>
<p class="alert alert-warning"><i class="fa fa-warning"></i> <strong>Listen Up:</strong> Like the parties endpoint, filters applied to this endpoint only affect the top-level data. To filter the nested data for each attorney, include the <code>filter_nested_results=True</code> parameter in your API request URL.
</p>
<pre class="pre-scrollable">curl -v \
--header 'Authorization: Token {% if user.is_authenticated %}{{ user.auth_token }}{% else %}&lt;your-token-here&gt;{% endif %}' \
"{% get_full_host %}{% url "attorney-list" version=version %}?docket=4214664"</pre>
Expand Down
293 changes: 270 additions & 23 deletions cl/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@
SchoolViewSet,
SourceViewSet,
)
from cl.people_db.factories import PartyFactory, PartyTypeFactory
from cl.people_db.factories import (
AttorneyFactory,
AttorneyOrganizationFactory,
PartyFactory,
PartyTypeFactory,
RoleFactory,
)
from cl.people_db.models import Attorney
from cl.recap.factories import ProcessingQueueFactory
from cl.recap.views import (
Expand Down Expand Up @@ -1110,10 +1116,7 @@ async def test_exclusion_filters(self) -> None:


class DRFRecapApiFilterTests(TestCase, FilteringCountTestCase):
fixtures = [
"recap_docs.json",
"attorney_party.json",
]
fixtures = ["recap_docs.json"]

@classmethod
def setUpTestData(cls) -> None:
Expand All @@ -1125,6 +1128,40 @@ def setUpTestData(cls) -> None:
ps = Permission.objects.filter(codename="has_recap_api_access")
up.user.user_permissions.add(*ps)

cls.docket = Docket.objects.get(pk=1)
cls.docket_2 = DocketFactory()
firm = AttorneyOrganizationFactory(
name="Lawyers LLP", lookup_key="6201in816"
)
cls.attorney = AttorneyFactory(
name="Juneau",
contact_raw="Juneau\r\nAlaska",
phone="555-555-5555",
fax="555-555-5555",
email="[email protected]",
date_created="2017-04-25T23:52:43.497Z",
date_modified="2017-04-25T23:52:43.497Z",
organizations=[firm],
docket=cls.docket,
)
cls.attorney_2 = AttorneyFactory(
organizations=[firm], docket=cls.docket_2
)
cls.party = PartyFactory(
name="Honker",
extra_info="",
docket=cls.docket,
attorneys=[cls.attorney],
date_created="2017-04-25T23:53:11.745Z",
date_modified="2017-04-25T23:53:11.745Z",
)
PartyTypeFactory.create(
party=cls.party,
name="Defendant",
extra_info="Klaxon",
docket=cls.docket,
)

@async_to_sync
async def setUp(self) -> None:
self.assertTrue(
Expand Down Expand Up @@ -1215,19 +1252,19 @@ async def test_recap_document_filters(self) -> None:
async def test_attorney_filters(self) -> None:
self.path = reverse("attorney-list", kwargs={"version": "v3"})

self.q["id"] = 1
self.q["id"] = self.attorney.pk
await self.assertCountInResults(1)
self.q["id"] = self.attorney_2.pk
await self.assertCountInResults(1)
self.q["id"] = 2
await self.assertCountInResults(0)

self.q = {"docket__id": 1}
self.q = {"docket__id": self.docket.pk}
await self.assertCountInResults(1)
self.q = {"docket__id": 2}
self.q = {"docket__id": self.docket_2.pk}
await self.assertCountInResults(0)

self.q = {"parties_represented__id": 1}
self.q = {"parties_represented__id": self.party.pk}
await self.assertCountInResults(1)
self.q = {"parties_represented__id": 2}
self.q = {"parties_represented__id": 9999}
await self.assertCountInResults(0)
self.q = {"parties_represented__name__contains": "Honker"}
await self.assertCountInResults(1)
Expand All @@ -1237,40 +1274,119 @@ async def test_attorney_filters(self) -> None:
# Adds extra role to the existing attorney
docket = await Docket.objects.afirst()
attorney = await Attorney.objects.afirst()
party = await sync_to_async(PartyFactory)(
docket=self.docket_2,
attorneys=[attorney],
)
await sync_to_async(PartyTypeFactory.create)(
party=await sync_to_async(PartyFactory)(
docket=docket,
attorneys=[attorney],
),
docket=docket,
party=party,
docket=self.docket_2,
)
self.q = {"docket__date_created__range": "2017-04-14,2017-04-15"}
await self.assertCountInResults(1)
self.q = {"docket__date_created__range": "2017-04-15,2017-04-16"}
await self.assertCountInResults(0)

# Initial request: Fetch all related records
self.q = {"docket__id": self.docket.pk}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of parties
self.assertEqual(
len(results[0]["parties_represented"]),
2,
msg=f"Expected 2, but got {len(results[0]['parties_represented'])}.\n\nr.data was: {r.data}",
)

# Fetch attorney records for docket (repeat request with "filter_nested_results")
self.q = {"docket__id": self.docket.pk, "filter_nested_results": True}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify top-level record has single party (due to filter)
self.assertEqual(
len(results[0]["parties_represented"]),
1,
msg=f"Expected 1, but got {len(results[0]['parties_represented'])}.\n\nr.data was: {r.data}",
)
# Verify expected party is present in the parties_represented list
self.assertIn(
str(self.party.pk), results[0]["parties_represented"][0]["party"]
)

# Request using parties_represented lookup: Fetch all related records
self.q = {"parties_represented__docket__id": self.docket_2.pk}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of parties
self.assertEqual(
len(results[0]["parties_represented"]),
2,
msg=f"Expected 2, but got {len(results[0]['parties_represented'])}.\n\nr.data was: {r.data}",
)

# Fetch attorney records for parties associated with docket_2 (repeat with "filter_nested_results")
self.q = {
"parties_represented__docket__id": self.docket_2.pk,
"filter_nested_results": True,
}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of parties
self.assertEqual(
len(results[0]["parties_represented"]),
1,
msg=f"Expected 1, but got {len(results[0]['parties_represented'])}.\n\nr.data was: {r.data}",
)
# Verify expected party is present in the parties_represented list
self.assertIn(
str(party.pk), results[0]["parties_represented"][0]["party"]
)

async def test_party_filters(self) -> None:
self.path = reverse("party-list", kwargs={"version": "v3"})

self.q["id"] = 1
self.q["id"] = self.party.pk
await self.assertCountInResults(1)
self.q["id"] = 2
self.q["id"] = 999_999
await self.assertCountInResults(0)

# This represents dockets that the party was a part of.
self.q = {"docket__id": 1}
self.q = {"docket__id": self.docket.id}
await self.assertCountInResults(1)
self.q = {"docket__id": 2}
self.q = {"docket__id": self.docket_2.id}
await self.assertCountInResults(0)

# Contrasted with this, which joins based on their attorney.
self.q = {"attorney__docket__id": 1}
self.q = {"attorney__docket__id": self.docket.pk}
await self.assertCountInResults(1)
self.q = {"attorney__docket__id": 2}
self.q = {"attorney__docket__id": self.docket_2.pk}
await self.assertCountInResults(0)

self.q = {"name": "Honker"}
await self.assertCountInResults(1)
self.q = {"name__icontains": "Honk"}
await self.assertCountInResults(1)

self.q = {"name": "Cardinal Bonds"}
await self.assertCountInResults(0)

Expand All @@ -1279,6 +1395,137 @@ async def test_party_filters(self) -> None:
self.q = {"attorney__name__icontains": "Juno"}
await self.assertCountInResults(0)

# Add another attorney to the party record but linked to another docket
await sync_to_async(RoleFactory.create)(
party=self.party, docket=self.docket_2, attorney=self.attorney_2
)

# Fetch all party records for docket
self.q = {"docket__id": self.docket.pk}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of attorneys
self.assertEqual(
len(results[0]["attorneys"]),
2,
msg=f"Expected 2, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)

# Fetch top-level record for docket (repeat with "filter_nested_results")
self.q = {"docket__id": self.docket.pk, "filter_nested_results": True}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify the record has only one attorney (due to filter)
self.assertEqual(
len(results[0]["attorneys"]),
1,
msg=f"Expected 1, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)
# Check if retrieved attorney matches expected record
self.assertEqual(
results[0]["attorneys"][0]["attorney_id"], self.attorney.pk
)

# Add another party type to the party record but linked to a different docket
await sync_to_async(PartyTypeFactory.create)(
party=self.party, docket=self.docket_2
)

# Fetch all party records for docket
self.q = {"docket__id": self.docket.pk}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of attorneys
self.assertEqual(
len(results[0]["attorneys"]),
2,
msg=f"Expected 2, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of party types
self.assertEqual(
len(results[0]["party_types"]),
2,
msg=f"Expected 2, but got {len(results[0]['party_types'])}.\n\nr.data was: {r.data}",
)

# Fetch top-level record for docket (repeat with "filter_nested_results")
self.q = {"docket__id": self.docket.pk, "filter_nested_results": True}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify the record has only one attorney and one party type (due to filter)
self.assertEqual(
len(results[0]["attorneys"]),
1,
msg=f"Expected 1, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)
self.assertEqual(
len(results[0]["party_types"]),
1,
msg=f"Expected 1, but got {len(results[0]['party_types'])}.\n\nr.data was: {r.data}",
)
# Check if retrieved party type matches expected record
self.assertNotEqual(
results[0]["party_types"][0]["docket_id"], self.docket_2.pk
)

# Fetch party details based on attorney lookup
self.q = {"attorney__docket__id": self.docket_2.pk}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Verify record has expected number of attorneys
self.assertEqual(
len(results[0]["attorneys"]),
2,
msg=f"Expected 2, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)

# Apply additional filter to refine previous request
self.q = {
"attorney__docket__id": self.docket_2.pk,
"filter_nested_results": True,
}
r = await self.async_client.get(self.path, self.q)
results = r.data["results"]
self.assertEqual(
len(results),
1,
msg=f"Expected 1, but got {len(results)}.\n\nr.data was: {r.data}",
)
# Ensure only attorney_2 is associated with the filtered party
self.assertEqual(
len(results[0]["attorneys"]),
1,
msg=f"Expected 1, but got {len(results[0]['attorneys'])}.\n\nr.data was: {r.data}",
)
self.assertEqual(
results[0]["attorneys"][0]["attorney_id"], self.attorney_2.pk
)


class DRFSearchAppAndAudioAppApiFilterTest(
TestCase, AudioTestCase, FilteringCountTestCase
Expand Down
Loading

0 comments on commit d5aa1d0

Please sign in to comment.