diff --git a/efile_app/efile/migrations/0039_remove_preview_step.py b/efile_app/efile/migrations/0039_remove_preview_step.py new file mode 100644 index 00000000..ec6a0691 --- /dev/null +++ b/efile_app/efile/migrations/0039_remove_preview_step.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.17 on 2026-10-09 21:20 + +import efile.workflow +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('efile', '0038_existing_case_identity'), + ] + + operations = [ + migrations.AlterField( + model_name='filingdraft', + name='current_step', + field=models.CharField(choices=[('options', 'Options'), ('filing_path', 'Start'), ('upload_documents', 'Upload documents'), ('extraction_review', 'Confirm filing'), ('case_lookup', 'Find your case'), ('case_confirmation', 'Confirm your case'), ('document_checklist', 'Check documents'), ('organize_documents', 'Organize documents'), ('your_information', 'Your information'), ('parties', 'People in this filing'), ('party_details', 'Person details'), ('case_questions', 'Case questions'), ('payment', 'Fees'), ('review', 'Review'), ('confirmation', 'Confirmation')], default=efile.workflow.WorkflowStepKey['OPTIONS'], max_length=64), + ), + ] diff --git a/efile_app/efile/services/document_previews.py b/efile_app/efile/services/document_previews.py index 90c43e1c..0ae2704f 100644 --- a/efile_app/efile/services/document_previews.py +++ b/efile_app/efile/services/document_previews.py @@ -13,7 +13,7 @@ class DocumentReviewError(ValueError): """The displayed documents cannot be approved.""" - def __init__(self, message, *, status=200): + def __init__(self, message, *, status=422): super().__init__(message) self.status = status diff --git a/efile_app/efile/templates/efile/extraction_review.html b/efile_app/efile/templates/efile/extraction_review.html index 838c130c..06e1dc44 100644 --- a/efile_app/efile/templates/efile/extraction_review.html +++ b/efile_app/efile/templates/efile/extraction_review.html @@ -107,11 +107,16 @@

{% translate "Tell us about your case" %}

{% translate "go back and try a different document" %}. {% endif %} - {% if lead_document %} - {% include "efile/components/document_preview.html" with document=lead_document %} - {% endif %} + {% if preview_error %}{% endif %} + {% translate "Change files" %} + {% for document in documents %} + {% include "efile/components/document_preview.html" %} + {% endfor %}
{% csrf_token %} + {% include "efile/components/filing_availability_notice.html" %} {% comment %} @@ -518,7 +523,9 @@

{% translate "Tell us about your case" %}

{% translate "Back" %} {% endif %} - diff --git a/efile_app/efile/templates/efile/upload_documents.html b/efile_app/efile/templates/efile/upload_documents.html index e1a64199..b8c3ff4b 100644 --- a/efile_app/efile/templates/efile/upload_documents.html +++ b/efile_app/efile/templates/efile/upload_documents.html @@ -218,11 +218,11 @@

{% translate "Your documents" %}

preview it came from, not the start of the filing. {% endcomment %} {% translate "Back" %} + href="{% url 'extraction_review' jurisdiction %}?return_to={{ return_to }}"> {% translate "Back" %} {% else %} {% translate "Back" %} {% endif %} - {% translate "Preview your PDFs" %} + {% translate "Confirm information" %} {% endblock workflow_content %} diff --git a/efile_app/efile/tests/test_detours.py b/efile_app/efile/tests/test_detours.py index 40691320..d7fc3bd3 100644 --- a/efile_app/efile/tests/test_detours.py +++ b/efile_app/efile/tests/test_detours.py @@ -11,6 +11,7 @@ from django.urls import reverse from efile.models import FilingDocument, FilingParty +from efile.services.document_previews import preview_fingerprint 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 @@ -150,8 +151,8 @@ def test_finding_a_case_on_a_handoff_detour_still_asks_to_confirm_it(client, dra def test_unchecked_files_on_a_detour_return_through_the_preview(client, draft): draft.documents.update(preparation_reviewed_at=None) response = client.get(step("extraction_review", draft, "handoff")) - assert path(response.url) == reverse("preview_documents", kwargs=J) - assert "return_to=handoff" in response.url + assert response.status_code == 200 + assert response.context["return_to"] == "handoff" def test_switching_to_an_existing_case_from_review_finds_it_then_returns(client, draft): @@ -168,6 +169,7 @@ def test_switching_to_an_existing_case_from_review_finds_it_then_returns(client, "case_type_code": "contract", "case_type_name": "Contract", "reviewed_extraction": "yes", + "preview_fingerprint": preview_fingerprint(list(draft.documents.all())), "return_to": "review", }, ) diff --git a/efile_app/efile/tests/test_document_checks.py b/efile_app/efile/tests/test_document_checks.py index c8d77e17..fe6e4815 100644 --- a/efile_app/efile/tests/test_document_checks.py +++ b/efile_app/efile/tests/test_document_checks.py @@ -133,7 +133,7 @@ def test_legacy_main_documents_recover_through_preparation(client, draft, page, 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 response.url.partition("?")[0] == reverse("extraction_review", kwargs={"jurisdiction": draft.jurisdiction}) assert f"draft={draft.pk}" in response.url if return_to: assert f"return_to={return_to}" in response.url @@ -143,7 +143,7 @@ def test_legacy_main_documents_recover_through_preparation(client, draft, page, 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): + with patch("efile.views.extraction_review.S3UploadHandler", return_value=storage): prepared = client.get(response.url) assert prepared.status_code == 200 lead.refresh_from_db() @@ -151,7 +151,15 @@ def test_legacy_main_documents_recover_through_preparation(client, draft, page, assert lead.preparation_reviewed_at is None approved = client.post( response.url, - {"preview_fingerprint": preview_fingerprint(list(draft.documents.all())), "return_to": return_to}, + { + "preview_fingerprint": preview_fingerprint(list(draft.documents.all())), + "return_to": return_to, + "existing_case": draft.existing_case, + "court_code": draft.court_code, + "case_category_code": draft.case_category_code, + "case_type_code": draft.case_type_code, + "reviewed_extraction": "yes", + }, ) assert approved.status_code == 302 lead.refresh_from_db() diff --git a/efile_app/efile/tests/test_document_extractions.py b/efile_app/efile/tests/test_document_extractions.py index ef7c0400..2b338386 100644 --- a/efile_app/efile/tests/test_document_extractions.py +++ b/efile_app/efile/tests/test_document_extractions.py @@ -241,7 +241,7 @@ def test_status_endpoint_reports_when_review_is_ready(client, extraction_draft): "wait_seconds": 0, "pages_analyzed": 20, "total_pages": 30, - "review_url": reverse("preview_documents", kwargs={"jurisdiction": "illinois"}), + "review_url": reverse("extraction_review", kwargs={"jurisdiction": "illinois"}), } diff --git a/efile_app/efile/tests/test_document_previews.py b/efile_app/efile/tests/test_document_previews.py index 372f8f71..2778737a 100644 --- a/efile_app/efile/tests/test_document_previews.py +++ b/efile_app/efile/tests/test_document_previews.py @@ -40,9 +40,10 @@ def url(name, draft, **kwargs): def approve(client, draft, **overrides): documents = list(draft.documents.all()) return client.post( - url("preview_documents", draft), + url("extraction_review", draft), { "preview_fingerprint": preview_fingerprint(documents), + "existing_case": "existing", **overrides, }, ) @@ -50,15 +51,13 @@ def approve(client, draft, **overrides): def test_continue_records_preview_without_checkboxes_for_current_documents(client, preview_draft): doc = preview_draft.documents.get() - redirect = client.get(url("extraction_review", preview_draft)) - assert redirect.status_code == 302 and "preview-documents" in redirect.url - with patch("efile.views.document_previews.prepare_document_review"): - page = client.get(url("preview_documents", preview_draft)) + with patch("efile.views.extraction_review.prepare_document_review"): + page = client.get(url("extraction_review", preview_draft)) assert b'type="checkbox"' not in page.content - assert b"check every page before you continue" in page.content + assert b"preview_fingerprint" in page.content doc.refresh_from_db() assert doc.preparation_reviewed_at is None - assert approve(client, preview_draft, preview_fingerprint="stale").status_code == 200 + assert approve(client, preview_draft, preview_fingerprint="stale").status_code == 422 doc.refresh_from_db() assert doc.preparation_reviewed_at is None assert approve(client, preview_draft).status_code == 302 @@ -157,6 +156,9 @@ def test_original_and_approval_survive_supporting_row_rebuilds(preview_draft): def organized(draft): + draft.existing_case = "existing" + draft.previous_case_id = "verified-case" + draft.save() draft.documents.update(filing_type_code="complaint", document_type_code="public") @@ -164,7 +166,7 @@ def test_preview_return_destinations_are_restricted(client, preview_draft): organized(preview_draft) for unknown in ("https://attacker.example", "payment", "document_checklist"): response = approve(client, preview_draft, return_to=unknown) - assert response.url.partition("?")[0].endswith("/extraction-review/") + assert response.url.partition("?")[0].endswith("/case-lookup/") assert "return_to" not in response.url @@ -195,7 +197,7 @@ def test_legacy_word_support_is_prepared_before_it_can_be_approved(client, previ write_upload_data(preview_draft, {"files": {"supporting": [{"s3_key": "legacy.docx", "name": "legacy.docx"}]}}) supporting = preview_draft.documents.get(role="supporting") - assert approve(client, preview_draft).status_code == 200 + assert approve(client, preview_draft).status_code == 422 supporting.refresh_from_db() assert supporting.preparation_reviewed_at is None handler = MagicMock() @@ -207,11 +209,11 @@ def test_legacy_word_support_is_prepared_before_it_can_be_approved(client, previ {"success": True, "key": "prepared.pdf"}, ] with ( - patch("efile.views.document_previews.S3UploadHandler", return_value=handler), + patch("efile.views.extraction_review.S3UploadHandler", return_value=handler), patch("efile.services.document_preparation.requests.post", return_value=service_response(pdf_bytes())), patch("efile.services.document_preparation.settings.GOTENBERG_URL", "https://synthetic.invalid"), ): - assert client.get(url("preview_documents", preview_draft)).status_code == 200 + assert client.get(url("extraction_review", preview_draft)).status_code == 200 supporting.refresh_from_db() assert supporting.preparation == "converted" assert supporting.s3_key == "prepared.pdf" @@ -228,7 +230,7 @@ def test_legacy_rows_and_changed_preparation_cannot_bypass_submission(client, pr doc.original_s3_key = "new-original" doc.save() assert preview_fingerprint([doc]) != fingerprint - assert approve(client, preview_draft, preview_fingerprint=fingerprint).status_code == 200 + assert approve(client, preview_draft, preview_fingerprint=fingerprint).status_code == 422 doc.preparation = "" doc.save() with patch("efile.views.submission.forward_final_filing") as forward: @@ -274,14 +276,14 @@ def test_legacy_pdf_is_flattened_and_cannot_be_acknowledged_after_preparation_fa doc.original_filename = "source.pdf" doc.save() with ( - patch("efile.views.document_previews.S3UploadHandler", return_value=handler), + patch("efile.views.extraction_review.S3UploadHandler", return_value=handler), patch("efile.services.document_preparation.settings.GOTENBERG_URL", "https://synthetic.invalid"), patch( "efile.services.document_preparation.requests.post", return_value=service_response(pdf_bytes("Stored answer")), ), ): - assert client.get(url("preview_documents", preview_draft)).status_code == 200 + assert client.get(url("extraction_review", preview_draft)).status_code == 200 doc.refresh_from_db() assert doc.preparation == "flattened" assert doc.s3_key == "flat.pdf" @@ -292,17 +294,17 @@ def test_legacy_pdf_is_flattened_and_cannot_be_acknowledged_after_preparation_fa doc.preparation_reviewed_at = None doc.save() with ( - patch("efile.views.document_previews.S3UploadHandler", return_value=handler), + patch("efile.views.extraction_review.S3UploadHandler", return_value=handler), patch("efile.services.document_preparation.settings.GOTENBERG_URL", "https://synthetic.invalid"), patch( "efile.services.document_preparation.requests.post", return_value=service_response(pdf_bytes("Dropped answer")), ), ): - response = client.get(url("preview_documents", preview_draft)) + response = client.get(url("extraction_review", preview_draft)) assert response.status_code == 422 assert b"Some answers are missing" in response.content - assert approve(client, preview_draft).status_code == 200 + assert approve(client, preview_draft).status_code == 422 doc.refresh_from_db() assert doc.preparation_reviewed_at is None @@ -319,12 +321,12 @@ def test_accessibility_seed_starts_with_a_prepared_acknowledged_document(tmp_pat def test_change_files_keeps_where_the_filer_came_from(client, preview_draft): - with patch("efile.views.document_previews.prepare_document_review"): - preview = client.get(url("preview_documents", preview_draft) + "&return_to=review").content.decode() + with patch("efile.views.extraction_review.prepare_document_review"): + preview = client.get(url("extraction_review", preview_draft) + "&return_to=review").content.decode() assert 'upload-documents/?return_to=review"' in preview upload = client.get(url("upload_documents", preview_draft) + "&return_to=review").content.decode() # Both ways off the upload page lead back toward Review, not the start. - assert upload.count('href="/jurisdiction/vermont/preview-documents/?return_to=review"') == 2 + assert upload.count('href="/jurisdiction/vermont/extraction-review/?return_to=review"') == 2 unknown = client.get(url("upload_documents", preview_draft) + "&return_to=elsewhere").content.decode() assert "return_to" not in unknown @@ -335,7 +337,7 @@ def test_upload_from_a_detour_continues_to_its_preview(client, preview_draft): url("upload_documents", preview_draft) + "&return_to=review", {"documents": SimpleUploadedFile("new.pdf", pdf_bytes())}, ) - assert "preview-documents/?return_to=review" in response.json()["redirect_url"] + assert "extraction-review/?return_to=review" in response.json()["redirect_url"] def test_unchanged_files_return_straight_to_review(client, preview_draft): @@ -350,7 +352,7 @@ def test_a_new_file_from_review_stops_only_at_organize(client, preview_draft): response = approve(client, preview_draft, return_to="review") assert "/organize-documents/?return_to=review" in response.url # Without a return target, new files still follow the full flow. - assert "extraction-review" in approve(client, preview_draft).url + assert "case-lookup" in approve(client, preview_draft).url def test_handoff_detour_returns_to_its_list(client, preview_draft): @@ -376,3 +378,27 @@ def test_shared_review_rejects_unsafe_approval(preview_draft, state): with pytest.raises(DocumentReviewError): approve_document_review(preview_draft, fingerprint) assert not preview_draft.documents.filter(preparation_reviewed_at__isnull=False).exists() + + +def test_legacy_preview_redirects_without_approving(client, preview_draft): + response = client.post(url("preview_documents", preview_draft) + "&return_to=review") + assert response.status_code == 302 + assert "extraction-review/" in response.url + assert "return_to=review" in response.url + assert preview_draft.documents.get().preparation_reviewed_at is None + + +def test_all_documents_are_available_on_confirm(client, preview_draft): + supporting = FilingDocument.objects.create( + draft=preview_draft, role="supporting", name="second.pdf", s3_key="second.pdf", preparation="original" + ) + page = client.get(url("extraction_review", preview_draft)) + assert page.status_code == 200 + assert set(doc.pk for doc in page.context["documents"]) == set(preview_draft.documents.values_list("pk", flat=True)) + assert str(supporting.pk).encode() in page.content + + +def test_a_rejected_form_leaves_the_files_unchecked(client, preview_draft): + response = approve(client, preview_draft, existing_case="") + assert response.status_code == 200 + assert preview_draft.documents.get().preparation_reviewed_at is None diff --git a/efile_app/efile/tests/test_end_to_end_new_flow_states.py b/efile_app/efile/tests/test_end_to_end_new_flow_states.py index 3f0c2454..aeec89d3 100644 --- a/efile_app/efile/tests/test_end_to_end_new_flow_states.py +++ b/efile_app/efile/tests/test_end_to_end_new_flow_states.py @@ -230,19 +230,6 @@ def fake_download(_key, destination): assert status_resp2.json()["ready"] is True assert status_resp2.json()["status"] == "complete" - # The uploaded filing copies must be checked before extracted case details. - preview_url = reverse("preview_documents", kwargs={"jurisdiction": jurisdiction}) - preview = client.get(preview_url) - assert preview.status_code == 200 - approved = client.post( - preview_url, - { - "preview_fingerprint": preview.context["preview_fingerprint"], - "reviewed_document": [str(doc.pk) for doc in draft.documents.all()], - }, - ) - assert approved.status_code == 302 - # 3. Step: extraction-review review_page = client.get(reverse("extraction_review", kwargs={"jurisdiction": jurisdiction})) assert review_page.status_code == 200 @@ -257,6 +244,7 @@ def fake_download(_key, destination): reverse("extraction_review", kwargs={"jurisdiction": jurisdiction}), { "reviewed_extraction": "yes", + "preview_fingerprint": review_page.context["preview_fingerprint"], "existing_case": ExistingCase.NEW, "court_code": court_code, "court_name": court_name, diff --git a/efile_app/efile/tests/test_waiver_documents.py b/efile_app/efile/tests/test_waiver_documents.py index e09c8759..bb1bba1e 100644 --- a/efile_app/efile/tests/test_waiver_documents.py +++ b/efile_app/efile/tests/test_waiver_documents.py @@ -373,11 +373,19 @@ def test_review_sends_an_unchecked_copy_to_preview_and_back(client, payment_draf review_url = reverse("case_review", kwargs={"jurisdiction": "illinois"}) + f"?draft={payment_draft.pk}" bounced = client.get(review_url) assert bounced.status_code == 302 - assert "preview-documents" in bounced.url and "return_to=review" in bounced.url - preview_url = reverse("preview_documents", kwargs={"jurisdiction": "illinois"}) + f"?draft={payment_draft.pk}" + assert "extraction-review" in bounced.url and "return_to=review" in bounced.url + preview_url = reverse("extraction_review", kwargs={"jurisdiction": "illinois"}) + f"?draft={payment_draft.pk}" approved = client.post( preview_url, - {"preview_fingerprint": preview_fingerprint(list(payment_draft.documents.all())), "return_to": "review"}, + { + "preview_fingerprint": preview_fingerprint(list(payment_draft.documents.all())), + "return_to": "review", + "existing_case": payment_draft.existing_case, + "court_code": payment_draft.court_code, + "court_name": payment_draft.court_name, + "case_category_code": payment_draft.case_category_code, + "case_type_code": payment_draft.case_type_code, + }, ) assert approved.status_code == 302 assert "/review/" in approved.url diff --git a/efile_app/efile/tests/test_workflow.py b/efile_app/efile/tests/test_workflow.py index a734e512..3bd3dc73 100644 --- a/efile_app/efile/tests/test_workflow.py +++ b/efile_app/efile/tests/test_workflow.py @@ -49,7 +49,6 @@ def test_target_workflow_declares_every_reorganized_screen(): WorkflowStepKey.OPTIONS, WorkflowStepKey.FILING_PATH, WorkflowStepKey.UPLOAD_DOCUMENTS, - WorkflowStepKey.PREVIEW_DOCUMENTS, WorkflowStepKey.EXTRACTION_REVIEW, WorkflowStepKey.CASE_LOOKUP, WorkflowStepKey.CASE_CONFIRMATION, diff --git a/efile_app/efile/views/document_checklist.py b/efile_app/efile/views/document_checklist.py index 3399a72c..580774b1 100644 --- a/efile_app/efile/views/document_checklist.py +++ b/efile_app/efile/views/document_checklist.py @@ -134,7 +134,7 @@ def document_checklist(request, jurisdiction): if documents.filter(preparation="").exists(): return redirect( - with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_target(request)) + with_return_to(get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), return_target(request)) ) if request.method == "POST" and request.POST.get("action") == "upload": diff --git a/efile_app/efile/views/document_previews.py b/efile_app/efile/views/document_previews.py index 82e41367..4cd1d0f7 100644 --- a/efile_app/efile/views/document_previews.py +++ b/efile_app/efile/views/document_previews.py @@ -4,21 +4,14 @@ from botocore.exceptions import BotoCoreError, ClientError from django.conf import settings from django.http import FileResponse, Http404, HttpResponse, HttpResponseBase -from django.shortcuts import redirect, render +from django.shortcuts import redirect from django.views.decorators.http import require_http_methods from efile.api.suffolk_api_views import get_tyler_token from efile.models import FilingDocument from efile.services.current_drafts import ensure_current_draft, get_current_draft -from efile.services.document_preparation import PreparationError, PreparationUnavailable -from efile.services.document_previews import ( - DocumentReviewError, - approve_document_review, - prepare_document_review, - preview_fingerprint, -) from efile.utils.s3_upload_handler import S3UploadHandler -from efile.workflow import WorkflowStepKey, continue_url, get_workflow_context, return_target +from efile.workflow import WorkflowStepKey, get_step_url, return_target, with_return_to logger = logging.getLogger(__name__) @@ -27,39 +20,10 @@ def preview_documents(request, jurisdiction): if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): return redirect("efile_login", jurisdiction=jurisdiction) - draft = ensure_current_draft(request, jurisdiction, current_step=WorkflowStepKey.PREVIEW_DOCUMENTS) - if not FilingDocument.objects.filter(draft=draft).exists(): - return redirect("upload_documents", jurisdiction=jurisdiction) - return_to = return_target(request) - error = "" - status = 200 - if request.method == "GET": - try: - prepare_document_review(draft, S3UploadHandler()) - except PreparationUnavailable as exc: - error, status = str(exc), 503 - except PreparationError as exc: - error, status = str(exc), 422 - except (BotoCoreError, ClientError): - error, status = "We could not load your files. Try again later.", 503 - if request.method == "POST": - try: - approve_document_review(draft, request.POST.get("preview_fingerprint")) - except DocumentReviewError as exc: - error, status = str(exc), exc.status - else: - return redirect(continue_url(draft, jurisdiction, return_to, WorkflowStepKey.EXTRACTION_REVIEW)) - documents = list(FilingDocument.objects.filter(draft=draft).order_by("role", "sort_order", "pk")) - context = { - "documents": documents, - "preview_fingerprint": preview_fingerprint(documents), - "preview_error": error, - "preparation_pending": any(not doc.preparation for doc in documents), - "return_to": return_to, - "is_logged_in": True, - } - context.update(get_workflow_context(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction, draft)) - return render(request, "efile/preview_documents.html", context, status=status) + ensure_current_draft(request, jurisdiction, current_step=WorkflowStepKey.EXTRACTION_REVIEW) + return redirect( + with_return_to(get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), return_target(request)) + ) @require_http_methods(["GET"]) diff --git a/efile_app/efile/views/extraction_review.py b/efile_app/efile/views/extraction_review.py index 1a285e4e..e20e3474 100644 --- a/efile_app/efile/views/extraction_review.py +++ b/efile_app/efile/views/extraction_review.py @@ -1,5 +1,6 @@ import re +from botocore.exceptions import BotoCoreError, ClientError from django.contrib import messages from django.shortcuts import redirect, render from django.views.decorators.http import require_http_methods @@ -11,7 +12,14 @@ from efile.services.current_drafts import ensure_current_draft from efile.services.document_checklists import resolve_filer_roles from efile.services.document_extractions import extraction_for_document, extraction_is_waiting -from efile.services.document_previews import unreviewed_documents +from efile.services.document_preparation import PreparationError, PreparationUnavailable +from efile.services.document_previews import ( + DocumentReviewError, + approve_document_review, + prepare_document_review, + preview_fingerprint, + unreviewed_documents, +) from efile.services.drafts import draft_snapshot, write_case_data from efile.services.extracted_parties import review_rows, save_reviewed_parties from efile.services.extraction_fields import display_extracted_fields, document_summary_details @@ -22,6 +30,7 @@ describe_path_change, filing_path_conflict, ) +from efile.utils.s3_upload_handler import S3UploadHandler from efile.workflow import ( ExistingCase, WorkflowStepKey, @@ -146,6 +155,18 @@ def _case_identity(existing_case, docket_number): return {"docket_number": docket_number} +def _document_approval_error(draft, request): + """Approve the displayed copies, or return the message and status saying why not.""" + fingerprint = request.POST.get("preview_fingerprint") + if not fingerprint and not unreviewed_documents(draft).exists(): + return None + try: + approve_document_review(draft, fingerprint) + except DocumentReviewError as exc: + return str(exc), exc.status + return None + + @require_http_methods(["GET", "POST"]) def extraction_review(request, jurisdiction): if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): @@ -161,12 +182,17 @@ def extraction_review(request, jurisdiction): messages.error(request, "Upload at least one document before reviewing the filing.") return redirect("upload_documents", jurisdiction=jurisdiction) - if unreviewed_documents(draft).exists(): - # Checking files is a detour too: come back here, still on the way - # to wherever this screen was opened from. - return redirect( - with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_target(request)) - ) + preview_error = "" + status = 200 + if request.method == "GET": + try: + prepare_document_review(draft, S3UploadHandler()) + except PreparationUnavailable as exc: + preview_error, status = str(exc), 503 + except PreparationError as exc: + preview_error, status = str(exc), 422 + except (BotoCoreError, ClientError): + preview_error, status = "We could not load your files. Try again later.", 503 lead = FilingDocument.objects.filter(draft=draft, role=FilingDocument.Role.LEAD).first() extraction = extraction_for_document(lead) if lead else None @@ -223,6 +249,10 @@ def extraction_review(request, jurisdiction): # the side rather than the case, so there is nothing to show until # the filer says which side is theirs. messages.error(request, "Choose which side of this case you are on to continue.") + elif approval_error := _document_approval_error(draft, request): + # Last, so the files are only marked checked when the rest of the + # form is accepted along with them. + preview_error, status = approval_error else: if offered_roles and draft.filer_role != filer_role: draft.filer_role = filer_role @@ -346,7 +376,12 @@ def classified(level, key): document_title=guesses.get("document title", ""), chosen=chosen_existing_case, ) + documents = list(FilingDocument.objects.filter(draft=draft).order_by("role", "sort_order", "pk")) context = { + "documents": documents, + "preview_fingerprint": preview_fingerprint(documents), + "preview_error": preview_error, + "preparation_pending": any(not doc.preparation for doc in documents), "is_logged_in": True, "availability_message": availability_message, "lead_document": lead, @@ -378,4 +413,4 @@ def classified(level, key): "return_to": return_target(request), } context.update(get_workflow_context(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction, draft)) - return render(request, "efile/extraction_review.html", context) + return render(request, "efile/extraction_review.html", context, status=status) diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py index 7fcbd1cf..9b8d1d4d 100644 --- a/efile_app/efile/views/payment.py +++ b/efile_app/efile/views/payment.py @@ -64,8 +64,8 @@ def efile_payment(request, jurisdiction): documents = list(FilingDocument.objects.filter(draft=draft).order_by("role", "sort_order", "pk")) if any(not document.preparation for document in documents): # Legacy uploads need preparation before they can be checked inline. - # Preview handles preparation failures and offers replacement/retry. - return redirect(with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_to)) + # Confirm information handles preparation failures and offers replacement/retry. + return redirect(with_return_to(get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), return_to)) unchecked = [document for document in documents if document.preparation_reviewed_at is None] if request.method == "POST" and unchecked: # The page keeps Continue off until these are confirmed; this covers diff --git a/efile_app/efile/views/review.py b/efile_app/efile/views/review.py index b793e4df..eef1f292 100644 --- a/efile_app/efile/views/review.py +++ b/efile_app/efile/views/review.py @@ -51,7 +51,7 @@ def case_review(request, jurisdiction): if message := draft_unavailable_message(draft): return unavailable_response(request, draft, message) if unreviewed_documents(draft).exists(): - return redirect(with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), RETURN_TO_REVIEW)) + return redirect(with_return_to(get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), RETURN_TO_REVIEW)) if not appeal_answers_complete(draft): messages.error(request, "Complete the lower court information before reviewing your filing.") diff --git a/efile_app/efile/views/upload_documents.py b/efile_app/efile/views/upload_documents.py index b9806bab..d6c93e09 100644 --- a/efile_app/efile/views/upload_documents.py +++ b/efile_app/efile/views/upload_documents.py @@ -170,7 +170,7 @@ def upload_documents(request, jurisdiction): { "success": True, "redirect_url": with_return_to( - get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_target(request) + get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), return_target(request) ), "document_count": FilingDocument.objects.filter(draft=draft).count(), "extraction_pending": FilingDocument.objects.filter( @@ -248,7 +248,7 @@ def document_extraction_status(request, jurisdiction): "pages_analyzed": extraction.pages_analyzed, "total_pages": extraction.total_pages, "review_url": with_return_to( - get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_target(request) + get_step_url(WorkflowStepKey.EXTRACTION_REVIEW, jurisdiction), return_target(request) ), } ) diff --git a/efile_app/efile/workflow.py b/efile_app/efile/workflow.py index 8bae8b3a..aab2a092 100644 --- a/efile_app/efile/workflow.py +++ b/efile_app/efile/workflow.py @@ -120,7 +120,6 @@ class WorkflowStep: WorkflowStepKey.FILING_PATH, pgettext_lazy("workflow stage", "Start"), "filing_path", WorkflowStage.FILING ), WorkflowStep(WorkflowStepKey.UPLOAD_DOCUMENTS, _("Upload documents"), "upload_documents", WorkflowStage.UPLOAD), - WorkflowStep(WorkflowStepKey.PREVIEW_DOCUMENTS, _("Preview documents"), "preview_documents", WorkflowStage.UPLOAD), WorkflowStep( WorkflowStepKey.EXTRACTION_REVIEW, _("Confirm filing"), @@ -163,6 +162,7 @@ class WorkflowStep: _STEPS_BY_KEY = {step.key: step for step in FILING_WORKFLOW} LEGACY_STEP_TARGETS = { + WorkflowStepKey.PREVIEW_DOCUMENTS: WorkflowStepKey.EXTRACTION_REVIEW, WorkflowStepKey.UPLOAD_FIRST: WorkflowStepKey.UPLOAD_DOCUMENTS, WorkflowStepKey.CASE_INFORMATION: WorkflowStepKey.EXTRACTION_REVIEW, WorkflowStepKey.DOCUMENTS: WorkflowStepKey.ORGANIZE_DOCUMENTS, @@ -183,7 +183,8 @@ def get_workflow_step_choices() -> tuple[tuple[str, str | Promise], ...]: def get_step(step_key: WorkflowStepKey | str) -> WorkflowStep: try: - return _STEPS_BY_KEY[WorkflowStepKey(step_key)] + key = WorkflowStepKey(step_key) + return _STEPS_BY_KEY[LEGACY_STEP_TARGETS.get(key, key)] except (KeyError, ValueError) as exc: raise KeyError(f"Unknown workflow step: {step_key}") from exc diff --git a/efile_app/tests/document-preparation-browser.js b/efile_app/tests/document-preparation-browser.js index c69a3aa8..a44c2fb2 100644 --- a/efile_app/tests/document-preparation-browser.js +++ b/efile_app/tests/document-preparation-browser.js @@ -57,7 +57,7 @@ async function main() { await page.locator("#continue-to-analysis:not(.disabled)").waitFor(); await page.waitForFunction(() => document.querySelectorAll(".document-row").length === 2); await page.locator("#continue-to-analysis").click(); - await page.waitForURL(/preview-documents/); + await page.waitForURL(/extraction-review/); const previews = page.locator("[data-pdf-preview]"); assert.equal(await previews.count(), 2); await previews.first().locator("summary").click(); @@ -101,13 +101,14 @@ async function main() { ) ); assert.equal(await page.locator('input[type="checkbox"]').count(), 0); + await page.locator('input[name="existing_case"][value="existing"]').check(); await page .getByRole("button", { - name: "Continue", + name: "Confirm and continue", exact: true }) .click(); - await page.waitForURL(/extraction-review/); + await page.waitForURL(/case-lookup/); await page.goto(config.baseUrl + config.organizeUrl); assert.match(page.url(), /organize-documents/); assert.equal(await page.locator("h1").innerText(), "Organize your documents"); diff --git a/efile_app/tests/payment-flow-browser.js b/efile_app/tests/payment-flow-browser.js index fa810504..15019817 100644 --- a/efile_app/tests/payment-flow-browser.js +++ b/efile_app/tests/payment-flow-browser.js @@ -260,9 +260,9 @@ async function main() { await page.locator(".document-row").nth(2).waitFor(); assert.ok(page.url().includes("return_to=review"), `upload reload lost its origin: ${page.url()}`); await page.locator("#continue-to-analysis").click(); - await page.waitForURL(/preview-documents\/\?.*return_to=review/); + await page.waitForURL(/extraction-review\/\?.*return_to=review/); await Promise.all([page.waitForURL(/organize-documents\/\?.*return_to=review/), page.getByRole("button", { - name: "Continue" + name: "Confirm and continue" }).click()]); await screenshot("05-new-file-stops-at-organize.png"); // Taking the new file back out leaves nothing to organize. @@ -273,11 +273,11 @@ async function main() { await page.getByRole("link", { name: "Back" }).click(); - await page.waitForURL(/preview-documents\/\?.*return_to=review/); + await page.waitForURL(/extraction-review\/\?.*return_to=review/); await Promise.all([page.waitForURL(review), page.getByRole("button", { - name: "Continue" + name: "Confirm and continue" }).click()]); - assert.ok(!visited.slice(-3).some((url) => url.includes("extraction-review")), `visited ${visited}`); + assert.ok(!visited.slice(-3).some((url) => url.includes("preview-documents")), `visited ${visited}`); await screenshot("06-back-to-review.png"); console.log("Scenario 8 passed"); } catch (error) {