Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions efile_app/efile/migrations/0030_document_type_confirmed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.17 on 2026-10-01 19:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("efile", "0029_document_preparation_and_preview"),
]

operations = [
migrations.AddField(
model_name="filingdocument",
name="document_type_confirmed",
field=models.BooleanField(default=False),
),
]
4 changes: 4 additions & 0 deletions efile_app/efile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ class Role(models.TextChoices):
)
preparation_reviewed_at = models.DateTimeField(null=True, blank=True)

# A blank document type is a completed answer only when Organize checked
# that the court offers no confidentiality choices for this filing type.
document_type_confirmed = models.BooleanField(default=False)

filing_type_code = models.CharField(max_length=100, blank=True)
filing_type_name = models.CharField(max_length=255, blank=True)
document_type_code = models.CharField(max_length=100, blank=True)
Expand Down
12 changes: 12 additions & 0 deletions efile_app/efile/services/drafts.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ def _apply_document(doc: FilingDocument, file_obj: dict[str, Any], config: dict[
value = config.get(config_key)
if isinstance(value, dict):
value = value.get("id") or value.get("code") or ""
if model_field in {"filing_type_code", "document_type_code"} and getattr(doc, model_field) != _as_str(
value
):
doc.document_type_confirmed = False
setattr(doc, model_field, _as_str(value))

if "optional_services" in config:
Expand Down Expand Up @@ -508,6 +512,11 @@ def write_upload_data(
if document.s3_key
]
claimed_items = {document.s3_key: document.checklist_item_id for document in previous}
confirmed_types = {
document.s3_key: (document.filing_type_code, document.document_type_code)
for document in previous
if document.document_type_confirmed
}
# Handoff provenance and pending corrections are keyed by row id, so
# they follow the file to its rebuilt row the same way.
previous_ids = {document.s3_key: document.pk for document in previous}
Expand All @@ -524,6 +533,9 @@ def write_upload_data(
_upsert_document(draft, FilingDocument.Role.SUPPORTING, index, file_obj or {}, config or {})
moved = {}
for document in FilingDocument.objects.filter(draft=draft, role=FilingDocument.Role.SUPPORTING):
if confirmed_types.get(document.s3_key) == (document.filing_type_code, document.document_type_code):
document.document_type_confirmed = True
document.save(update_fields=["document_type_confirmed", "updated_at"])
if metadata := preparation_metadata.get(document.s3_key):
for field, value in metadata.items():
setattr(document, field, value)
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/services/filing_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def change_filing_path(draft: FilingDraft, new_path: str) -> FilingPathChange:
chosen.update(
filing_type_code="",
filing_type_name="",
document_type_confirmed=False,
filing_component_code="",
filing_component_name="",
requested_optional_services=[],
Expand Down
2 changes: 2 additions & 0 deletions efile_app/efile/static/js/document-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const DocumentChecks = {
body.append("action", action);
body.append("document_id", check.dataset.documentId);
body.append("preview_fingerprint", check.dataset.fingerprint);
const feeToken = document.getElementById("fee-inputs-token");
if (feeToken) body.append("fee_inputs_token", JSON.parse(feeToken.textContent));
body.append("csrfmiddlewaretoken", apiUtils.getCSRFToken());
buttons.forEach((element) => {
element.disabled = true;
Expand Down
2 changes: 2 additions & 0 deletions efile_app/efile/static/js/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ const PaymentPage = {
action,
data
}) {
// The check endpoint verified this page's fee token before changing
// documents, so the returned token still matches our caseData.
document.getElementById("fee-inputs-token").textContent = JSON.stringify(data.fee_inputs_token);
const confirmation = document.getElementById("waiver-upload-confirmation");
if (action === "remove") {
Expand Down
42 changes: 42 additions & 0 deletions efile_app/efile/tests/test_detours.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
from django.urls import reverse

from efile.models import FilingDocument, FilingParty
from efile.services.drafts import read_upload_data, write_upload_data
from efile.services.filing_path import change_filing_path
from efile.tests.test_review_submit_flow import submission_draft as _submission_draft
from efile.views.organize_documents import _save_document_details
from efile.workflow import (
WorkflowStepKey,
clean_return_to,
Expand Down Expand Up @@ -67,12 +70,51 @@ def test_an_empty_document_type_is_a_finished_answer(draft):
# Organize saves no document type when the court offers no confidentiality
# choices. Treating that as unorganized would send Organize back to itself.
draft.documents.update(document_type_code="", document_type_name="")
assert documents_need_organizing(draft)
lead = draft.documents.get()

with patch("efile.views.organize_documents._court_document_types", return_value=[]):
_save_document_details(
draft,
[{"id": lead.pk, "filing_type": lead.filing_type_code, "document_type": ""}],
lead.pk,
)
assert not documents_need_organizing(draft)
assert continue_url(draft, "illinois", "review", WorkflowStepKey.YOUR_INFORMATION) == reverse(
"case_review", kwargs=J
)


def test_empty_confidentiality_confirmation_is_reset_when_the_filing_path_changes(draft):
draft.documents.update(document_type_code="", document_type_confirmed=True)
change_filing_path(draft, "existing")
lead = draft.documents.get()
assert not lead.document_type_confirmed
lead.filing_type_code = "automatically-assigned"
lead.save()
assert documents_need_organizing(draft)


def test_empty_confidentiality_confirmation_survives_rebuild_only_for_the_same_types(draft):
document = FilingDocument.objects.create(
draft=draft,
role="supporting",
s3_key="supporting.pdf",
filing_type_code="motion",
document_type_confirmed=True,
)
wire = read_upload_data(draft)
write_upload_data(draft, wire)
document = draft.documents.get(s3_key=document.s3_key)
assert document.document_type_confirmed
assert not documents_need_organizing(draft)

wire["supporting_documents"][0]["filing_type"] = "different-type"
write_upload_data(draft, wire)
assert not draft.documents.get(s3_key=document.s3_key).document_type_confirmed
assert documents_need_organizing(draft)


def test_adding_a_person_on_a_handoff_detour_stays_on_the_people_screens(client, draft):
response = client.post(step("parties", draft, "handoff"), {"action": "add", "return_to": "handoff"})
assert response.status_code == 302
Expand Down
83 changes: 82 additions & 1 deletion efile_app/efile/tests/test_document_checks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Confirming or removing a newly prepared copy on the page it was added to."""

import io
from unittest.mock import MagicMock, patch

import pytest
Expand All @@ -9,6 +10,7 @@
from efile.models import FilingDocument, FilingDraft
from efile.services.document_previews import preview_fingerprint
from efile.services.fee_quotes import fee_inputs_token
from efile.tests.pdf_helpers import pdf_bytes
from efile.tests.test_review_submit_flow import submission_draft as _submission_draft

draft = _submission_draft
Expand Down Expand Up @@ -49,6 +51,29 @@ def test_confirming_marks_only_that_copy_checked(client, draft, added):
assert added.preparation_reviewed_at is not None


@pytest.mark.parametrize("action", ["confirm", "remove"])
def test_payment_checks_reject_unrelated_fee_input_changes(client, draft, added, action):
token = fee_inputs_token(draft)
# Another tab changes the case, but this document's preview is unchanged.
draft.case_type_code = "another-case-type"
draft.save(update_fields=["case_type_code", "updated_at"])
response = client.post(checks(draft), confirm(added, action=action, fee_inputs_token=token))
assert response.status_code == 409
assert "Reload this page" in response.json()["error"]
assert "fee_inputs_token" not in response.json()
added.refresh_from_db()
assert added.preparation_reviewed_at is None


def test_payment_confirmation_returns_only_a_token_matching_the_page(client, draft, added):
token = fee_inputs_token(draft)
response = client.post(checks(draft), confirm(added, fee_inputs_token=token))
assert response.status_code == 200
assert response.json()["fee_inputs_token"] == token
added.refresh_from_db()
assert added.preparation_reviewed_at is not None


def test_a_changed_unprepared_or_foreign_copy_is_not_confirmed(client, draft, added):
assert client.post(checks(draft), confirm(added, preview_fingerprint="old")).status_code == 409
FilingDocument.objects.filter(pk=added.pk).update(preparation="")
Expand All @@ -66,7 +91,7 @@ def test_removing_takes_out_a_supporting_file_and_reprices(client, draft, added,
lead = draft.documents.get(role=FilingDocument.Role.LEAD)
draft.quoted_fee_total = "100"
draft.save()
remove = {"action": "remove"}
remove = {"action": "remove", "fee_inputs_token": fee_inputs_token(draft)}
assert client.post(checks(draft), remove | {"document_id": lead.pk}).status_code == 400
storage = MagicMock()
storage.delete_file.return_value = {"success": True}
Expand All @@ -80,6 +105,7 @@ def test_removing_takes_out_a_supporting_file_and_reprices(client, draft, added,
draft.refresh_from_db()
assert draft.quoted_fee_total == ""
assert response.json()["fee_inputs_token"] == fee_inputs_token(draft)
assert response.json()["fee_inputs_token"] != remove["fee_inputs_token"]
storage.delete_file.assert_called_with("supporting/exhibit.pdf")


Expand All @@ -93,3 +119,58 @@ def test_checks_need_a_signed_in_named_filing_and_csrf(client, draft, added):
protected = Client(enforce_csrf_checks=True)
protected.force_login(draft.user)
assert protected.post(checks(draft), confirm(added)).status_code == 403


@pytest.mark.parametrize("page", ["payment", "document_checklist"])
@pytest.mark.parametrize("method", ["get", "post"])
@pytest.mark.parametrize("return_to", ["", "review", "handoff"])
def test_legacy_main_documents_recover_through_preparation(client, draft, page, method, return_to):
lead = draft.documents.get(role="lead")
draft.documents.update(preparation="", preparation_reviewed_at=None, s3_key="legacy/filing.pdf")
url = reverse(page, kwargs={"jurisdiction": draft.jurisdiction}) + f"?draft={draft.pk}"
if return_to:
url += f"&return_to={return_to}"
with patch("efile.views.document_checklist.draft_unavailable_message", return_value=""):
response = getattr(client, method)(url)
assert response.status_code == 302
assert response.url.partition("?")[0] == reverse("preview_documents", kwargs={"jurisdiction": draft.jurisdiction})
assert f"draft={draft.pk}" in response.url
if return_to:
assert f"return_to={return_to}" in response.url

storage = MagicMock()
storage.bucket_name = "private"
storage.s3_client.get_object.side_effect = lambda **_kwargs: {"Body": io.BytesIO(pdf_bytes())}
storage.upload_file.return_value = {"success": True, "key": "prepared/filing.pdf"}
storage.get_public_url.return_value = "https://synthetic.invalid/prepared.pdf"
with patch("efile.views.document_previews.S3UploadHandler", return_value=storage):
prepared = client.get(response.url)
assert prepared.status_code == 200
lead.refresh_from_db()
assert lead.preparation == "unchanged"
assert lead.preparation_reviewed_at is None
approved = client.post(
response.url,
{"preview_fingerprint": preview_fingerprint(list(draft.documents.all())), "return_to": return_to},
)
assert approved.status_code == 302
lead.refresh_from_db()
assert lead.preparation_reviewed_at is not None
# The original screen is now reachable and the filer can continue there.
with (
patch("efile.views.document_checklist.draft_unavailable_message", return_value=""),
patch("efile.views.payment.estimate_fees", return_value={}),
):
assert client.get(url).status_code == 200


@pytest.mark.parametrize("page", ["payment", "document_checklist"])
def test_prepared_unchecked_files_stay_inline(client, draft, added, page):
url = reverse(page, kwargs={"jurisdiction": draft.jurisdiction}) + f"?draft={draft.pk}"
with (
patch("efile.views.document_checklist.draft_unavailable_message", return_value=""),
patch("efile.views.payment.estimate_fees", return_value={}),
):
response = client.get(url)
assert response.status_code == 200
assert f'data-document-id="{added.pk}"' in response.content.decode()
3 changes: 3 additions & 0 deletions efile_app/efile/tests/test_filing_path_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ def test_changing_back_to_a_new_case_drops_the_existing_case_it_was_filed_into(s
def test_an_unchanged_answer_from_review_still_returns_to_review(signed_in):
draft = start_from_menu(signed_in, ExistingCase.NEW)
lead = lead_with_evidence(draft, phase="initial", title="Complaint")
# This detour starts after the filer organized the filing for Review.
lead.document_type_code = "public"
lead.save()

response = confirm(signed_in, existing_case=ExistingCase.NEW, return_to="review")

Expand Down
69 changes: 69 additions & 0 deletions efile_app/efile/tests/test_filing_plan_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
there -- and the home the plan lives at between filings.
"""

import json
from unittest.mock import Mock, patch

import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse

from efile.models import FilingDocument, FilingDraft, FilingPlan
from efile.services.current_drafts import CURRENT_DRAFT_SESSION_KEY
from efile.services.document_previews import preview_fingerprint
from efile.services.drafts import read_upload_data, write_upload_data
from efile.services.filing_plans import (
documents_missing_from_envelope,
Expand Down Expand Up @@ -419,6 +422,72 @@ def test_a_complete_filing_returns_straight_to_review(client, signed_in):
assert response.url.partition("?")[0] == reverse("case_review", kwargs={"jurisdiction": "illinois"})


@pytest.mark.django_db
def test_checklist_upload_with_an_automatic_filing_type_requires_confidentiality(client, signed_in):
def upload(draft, *_args, **_kwargs):
FilingDocument.objects.create(
draft=draft,
role="supporting",
name="waiver.pdf",
preparation="unchanged",
)

with (
patch("efile.views.document_checklist.upload_files", side_effect=upload),
patch("efile.views.document_checklist.filing_type_for_item", return_value=("6529", "Fee waiver")),
):
attached = client.post(
f"{CHECKLIST_URL}?return_to=review",
{
"action": "attach_item",
"item_id": "fee_waiver",
"document": SimpleUploadedFile("waiver.pdf", b"uploaded copy"),
},
)
assert "#document-checks" in attached.url
document = signed_in.documents.get(role="supporting")
assert document.filing_type_code == "6529"
assert document.document_type_code == ""
assert not document.document_type_confirmed
checked = client.post(
reverse("document_checks", kwargs={"jurisdiction": "illinois"}) + f"?draft={signed_in.pk}",
{"action": "confirm", "document_id": document.pk, "preview_fingerprint": preview_fingerprint([document])},
)
assert checked.status_code == 200
continued = client.post(f"{CHECKLIST_URL}?return_to=review", {"return_to": "review"})
assert continued.url.partition("?")[0] == reverse("organize_documents", kwargs={"jurisdiction": "illinois"})
assert "return_to=review" in continued.url

lead = signed_in.documents.get(role="lead")
details = {
"main_document_id": lead.pk,
"return_to": "review",
"documents": [
{"id": lead.pk, "filing_type": lead.filing_type_code, "document_type": "public"},
{"id": document.pk, "filing_type": document.filing_type_code, "document_type": ""},
],
}
organize = reverse("organize_documents", kwargs={"jurisdiction": "illinois"}) + f"?draft={signed_in.pk}"
with patch("efile.views.organize_documents._court_document_types", return_value=[{"code": "sealed"}]):
refused = client.post(organize, json.dumps(details), content_type="application/json")
assert refused.status_code == 400
assert "Choose a confidentiality setting" in refused.json()["error"]
document.refresh_from_db()
assert not document.document_type_confirmed

details["documents"][1].update(document_type="sealed", filing_component="supporting")
with patch("efile.views.organize_documents.draft_unavailable_message", return_value=""):
organized = client.post(organize, json.dumps(details), content_type="application/json")
assert organized.status_code == 200
assert organized.json()["redirect_url"].partition("?")[0] == reverse(
"case_review", kwargs={"jurisdiction": "illinois"}
)
document.refresh_from_db()
assert document.document_type_code == "sealed"
assert document.filing_component_code == "supporting"
assert document.document_type_confirmed


# --- The plan's own home -----------------------------------------------------


Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/tests/test_reorganized_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def test_extraction_review_returns_to_review_when_edited_from_there(client, reor
name="petition.pdf",
# Organized already, as every document on Review has been.
filing_type_code="petition",
document_type_code="public",
)

response = client.post(
Expand Down
Loading
Loading