Skip to content

Commit a44ddb0

Browse files
authored
Merge pull request #5971 from freelawproject/4938-feat-adds-support-to-purchase-acms-attachment-pages
feat(recap): Adds support for ACMS attachment page purchases
2 parents cc21b42 + 9e94ffd commit a44ddb0

File tree

5 files changed

+174
-39
lines changed

5 files changed

+174
-39
lines changed

cl/corpus_importer/tasks.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from juriscraper.lib.exceptions import PacerLoginException, ParsingException
3535
from juriscraper.lib.string_utils import CaseNameTweaker, harmonize
3636
from juriscraper.pacer import (
37+
ACMSAttachmentPage,
3738
AppellateAttachmentPage,
3839
AppellateDocketReport,
3940
AttachmentPage,
@@ -1868,11 +1869,24 @@ def get_att_report_by_rd(
18681869
cookies=session_data.cookies, proxy=session_data.proxy_address
18691870
)
18701871
pacer_court_id = map_cl_to_pacer_id(rd.docket_entry.docket.court_id)
1871-
if is_appellate_court(pacer_court_id):
1872-
att_report = AppellateAttachmentPage(pacer_court_id, s)
1872+
is_appellate_case = is_appellate_court(pacer_court_id)
1873+
is_acms_document = rd.is_acms_document()
1874+
1875+
if is_acms_document:
1876+
report_class = ACMSAttachmentPage
1877+
elif is_appellate_case:
1878+
report_class = AppellateAttachmentPage
1879+
else:
1880+
report_class = AttachmentPage
1881+
1882+
att_report = report_class(pacer_court_id, s)
1883+
1884+
if is_acms_document:
1885+
docket_case_id = rd.docket_entry.docket.pacer_case_id
1886+
rd_entry_id = rd.pacer_doc_id
1887+
att_report.query(docket_case_id, rd_entry_id)
18731888
else:
1874-
att_report = AttachmentPage(pacer_court_id, s)
1875-
att_report.query(rd.pacer_doc_id)
1889+
att_report.query(rd.pacer_doc_id)
18761890
return att_report
18771891

18781892

cl/recap/mergers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1813,8 +1813,13 @@ async def merge_attachment_page_data(
18131813
pacer_file = await sync_to_async(PacerHtmlFiles)(
18141814
content_object=de, upload_type=UPLOAD_TYPE.ATTACHMENT_PAGE
18151815
)
1816+
pacer_file_name = (
1817+
"attachment_page.json"
1818+
if is_acms_attachment
1819+
else "attachment_page.html"
1820+
)
18161821
await sync_to_async(pacer_file.filepath.save)(
1817-
"attachment_page.html", # Irrelevant b/c S3PrivateUUIDStorageTest
1822+
pacer_file_name, # Irrelevant b/c S3PrivateUUIDStorageTest
18181823
ContentFile(text.encode()),
18191824
)
18201825

cl/recap/tasks.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import concurrent.futures
33
import hashlib
4+
import json
45
import logging
56
from dataclasses import dataclass
67
from datetime import datetime
@@ -2119,9 +2120,10 @@ def fetch_attachment_page(self: Task, fq_pk: int) -> list[int]:
21192120
self.request.chain = None
21202121
return []
21212122

2122-
if rd.is_acms_document():
2123-
msg = "ACMS attachment pages are not currently supported"
2124-
mark_fq_status(fq, msg, PROCESSING_STATUS.FAILED)
2123+
is_acms_case = rd.is_acms_document()
2124+
if is_acms_case and not pacer_case_id:
2125+
msg = f"Unable to complete purchase: Missing case_id for RECAP Document object {rd.pk}."
2126+
mark_fq_status(fq, msg, PROCESSING_STATUS.NEEDS_INFO)
21252127
self.request.chain = None
21262128
return []
21272129

@@ -2179,32 +2181,44 @@ def fetch_attachment_page(self: Task, fq_pk: int) -> list[int]:
21792181
)
21802182
raise self.retry(exc=exc)
21812183

2182-
text = r.response.text
21832184
is_appellate = is_appellate_court(court_id)
2184-
# Determine the appropriate parser function based on court jurisdiction
2185-
# (appellate or district)
2186-
att_data_parser = (
2187-
get_data_from_appellate_att_report
2188-
if is_appellate
2189-
else get_data_from_att_report
2190-
)
2191-
att_data = att_data_parser(text, court_id)
2185+
if not is_acms_case:
2186+
text = r.response.text
2187+
# Determine the appropriate parser function based on court jurisdiction
2188+
# (appellate or district)
2189+
att_data_parser = (
2190+
get_data_from_appellate_att_report
2191+
if is_appellate
2192+
else get_data_from_att_report
2193+
)
2194+
att_data = att_data_parser(text, court_id)
2195+
else:
2196+
att_data = r.data
2197+
text = json.dumps(r.data, default=str)
21922198

21932199
if att_data == {}:
21942200
msg = "Not a valid attachment page upload"
21952201
mark_fq_status(fq, msg, PROCESSING_STATUS.INVALID_CONTENT)
21962202
self.request.chain = None
21972203
return []
21982204

2205+
if is_acms_case:
2206+
document_number = att_data["entry_number"]
2207+
elif is_appellate:
2208+
# Appellate attachments don't contain a document_number
2209+
document_number = None
2210+
else:
2211+
document_number = att_data["document_number"]
2212+
21992213
try:
22002214
async_to_sync(merge_attachment_page_data)(
22012215
rd.docket_entry.docket.court,
22022216
pacer_case_id,
22032217
att_data["pacer_doc_id"],
2204-
# Appellate attachments don't contain a document_number
2205-
None if is_appellate else att_data["document_number"],
2218+
document_number,
22062219
text,
22072220
att_data["attachments"],
2221+
is_acms_attachment=is_acms_case,
22082222
)
22092223
except RECAPDocument.MultipleObjectsReturned:
22102224
msg = (

cl/recap/tests/tests.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3490,6 +3490,30 @@ def setUp(self) -> None:
34903490
recap_document_id=self.rd_appellate.pk,
34913491
)
34923492

3493+
self.acms_court = CourtFactory(
3494+
id="ca9", jurisdiction=Court.FEDERAL_APPELLATE
3495+
)
3496+
self.acms_docket = DocketFactory(
3497+
source=Docket.RECAP,
3498+
court=self.acms_court,
3499+
pacer_case_id="5d8e355d-b229-4b16-b00f-7552d2f79d4f",
3500+
)
3501+
self.rd_acms = RECAPDocumentFactory(
3502+
docket_entry=DocketEntryWithParentsFactory(
3503+
docket=self.acms_docket, entry_number=9
3504+
),
3505+
document_number=9,
3506+
pacer_doc_id="4e108d6c-ad5b-f011-bec2-001dd80b194b",
3507+
is_available=False,
3508+
document_type=RECAPDocument.PACER_DOCUMENT,
3509+
)
3510+
3511+
self.fq_acms = PacerFetchQueue.objects.create(
3512+
user=User.objects.get(username="recap"),
3513+
request_type=REQUEST_TYPE.ATTACHMENT_PAGE,
3514+
recap_document_id=self.rd_acms.pk,
3515+
)
3516+
34933517
def test_fetch_attachment_page_no_pacer_doc_id(
34943518
self, mock_court_accessible
34953519
) -> None:
@@ -3517,26 +3541,6 @@ def test_fetch_att_page_no_cookies(
35173541
self.assertEqual(self.fq.status, PROCESSING_STATUS.FAILED)
35183542
self.assertIn("Unable to find cached cookies", self.fq.message)
35193543

3520-
def test_fetch_acms_att_page(self, mock_court_accessible) -> None:
3521-
rd_acms = RECAPDocumentFactory(
3522-
docket_entry=DocketEntryWithParentsFactory(docket=DocketFactory()),
3523-
pacer_doc_id="784459c4-e2cd-ef11-b8e9-001dd804c0b4",
3524-
)
3525-
fq_acms = PacerFetchQueue.objects.create(
3526-
user=User.objects.get(username="recap"),
3527-
request_type=REQUEST_TYPE.ATTACHMENT_PAGE,
3528-
recap_document_id=rd_acms.pk,
3529-
)
3530-
result = do_pacer_fetch(fq_acms)
3531-
result.get()
3532-
3533-
fq_acms.refresh_from_db()
3534-
self.assertEqual(fq_acms.status, PROCESSING_STATUS.FAILED)
3535-
self.assertIn(
3536-
"ACMS attachment pages are not currently supported",
3537-
fq_acms.message,
3538-
)
3539-
35403544
@mock.patch(
35413545
"cl.recap.tasks.get_pacer_cookie_from_cache",
35423546
)
@@ -3630,6 +3634,56 @@ def test_fetch_att_page_from_appellate(
36303634
"Successfully completed fetch", self.fq_appellate.message
36313635
)
36323636

3637+
@mock.patch(
3638+
"cl.recap.tasks.get_pacer_cookie_from_cache",
3639+
)
3640+
@mock.patch(
3641+
"cl.corpus_importer.tasks.ACMSAttachmentPage",
3642+
new=fakes.FakeAcmsAttachmentPage,
3643+
)
3644+
@mock.patch(
3645+
"cl.corpus_importer.tasks.AppellateAttachmentPage",
3646+
)
3647+
@mock.patch(
3648+
"cl.corpus_importer.tasks.AttachmentPage",
3649+
)
3650+
@mock.patch(
3651+
"cl.corpus_importer.tasks.is_appellate_court", wraps=is_appellate_court
3652+
)
3653+
@mock.patch("cl.recap.tasks.is_appellate_court", wraps=is_appellate_court)
3654+
def test_fetch_att_page_from_acms(
3655+
self,
3656+
mock_court_check_task,
3657+
mock_court_check_parser,
3658+
mock_district_report_parser,
3659+
mock_appellate_report_parser,
3660+
mock_get_cookies,
3661+
mock_court_accessible,
3662+
):
3663+
# Trigger the fetch operation for an ACMS attachment page
3664+
result = do_pacer_fetch(self.fq_acms)
3665+
result.get()
3666+
3667+
self.fq_acms.refresh_from_db()
3668+
3669+
docket_entry = self.rd_acms.docket_entry
3670+
amcs_court_id = docket_entry.docket.court_id
3671+
# Verify court validation calls with expected court ID
3672+
mock_court_check_task.assert_called_with(amcs_court_id)
3673+
mock_court_check_parser.assert_called_with(amcs_court_id)
3674+
3675+
# Ensure that only the ACMS parser was used
3676+
mock_district_report_parser.assert_not_called()
3677+
mock_appellate_report_parser.assert_not_called()
3678+
3679+
# Assert successful fetch status and expected message
3680+
self.assertEqual(self.fq_acms.status, PROCESSING_STATUS.SUCCESSFUL)
3681+
self.assertIn("Successfully completed fetch", self.fq_acms.message)
3682+
3683+
# Verify that 3 RECAPDocument objects were created for the docket entry
3684+
docket_entry.refresh_from_db()
3685+
self.assertEqual(docket_entry.recap_documents.count(), 3)
3686+
36333687

36343688
class ProcessingQueueApiFilterTest(TestCase):
36353689
def setUp(self) -> None:

cl/tests/fakes.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,54 @@ def data(self, *args, **kwargs):
170170
}
171171

172172

173+
class FakeAcmsAttachmentPage(FakeAppellateAttachmentPage):
174+
@property
175+
def data(self, *args, **kwargs):
176+
return {
177+
"pacer_doc_id": "4e108d6c-ad5b-f011-bec2-001dd80b194b",
178+
"pacer_case_id": "5d8e355d-b229-4b16-b00f-7552d2f79d4f",
179+
"entry_number": 9,
180+
"description": "MOTION [Entered: 07/07/2025 08:41 PM]",
181+
"date_filed": date(2025, 7, 8),
182+
"date_end": date(2025, 7, 7),
183+
"attachments": [
184+
{
185+
"attachment_number": 1,
186+
"description": "Motion",
187+
"page_count": 30,
188+
"pacer_doc_id": "4e108d6c-ad5b-f011-bec2-001dd80b194b",
189+
"acms_document_guid": "d1358903-ad5b-f011-a2da-001dd80b00cb",
190+
"cost": 3.0,
191+
"date_filed": date(2025, 7, 8),
192+
"permission": "Public",
193+
"file_size": 864.0,
194+
},
195+
{
196+
"attachment_number": 2,
197+
"description": "Declaration",
198+
"page_count": 4,
199+
"pacer_doc_id": "4e108d6c-ad5b-f011-bec2-001dd80b194b",
200+
"acms_document_guid": "2f373c0f-ad5b-f011-a2da-001dd80b00cb",
201+
"cost": 0.4,
202+
"date_filed": date(2025, 7, 8),
203+
"permission": "Public",
204+
"file_size": 288.0,
205+
},
206+
{
207+
"attachment_number": 3,
208+
"description": "Declaration",
209+
"page_count": 30,
210+
"pacer_doc_id": "4e108d6c-ad5b-f011-bec2-001dd80b194b",
211+
"acms_document_guid": "c6aae921-ad5b-f011-a2da-001dd80b00cb",
212+
"cost": 3.0,
213+
"date_filed": date(2025, 7, 8),
214+
"permission": "Public",
215+
"file_size": 11264.0,
216+
},
217+
],
218+
}
219+
220+
173221
class FakeFreeOpinionReport:
174222
def __init__(self, *args, **kwargs):
175223
pass

0 commit comments

Comments
 (0)