Skip to content
Open
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
57 changes: 57 additions & 0 deletions efile_app/efile/migrations/0003_branch_aware_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.db import migrations, models


def normalize_existing_case(apps, schema_editor):
FilingDraft = apps.get_model("efile", "FilingDraft")
FilingDraft.objects.filter(existing_case="no").update(existing_case="new")
FilingDraft.objects.filter(existing_case__in=["yes", "responding"]).update(existing_case="existing")


class Migration(migrations.Migration):
dependencies = [("efile", "0002_filing_drafts")]

operations = [
migrations.RunPython(normalize_existing_case, migrations.RunPython.noop),
migrations.AddField(
model_name="filingdraft",
name="workflow_version",
field=models.PositiveSmallIntegerField(default=1),
),
migrations.AlterField(
model_name="filingdraft",
name="existing_case",
field=models.CharField(
blank=True,
choices=[("new", "New"), ("existing", "Existing"), ("unsure", "Unsure")],
max_length=20,
),
),
migrations.AlterField(
model_name="filingdraft",
name="current_step",
field=models.CharField(
choices=[
("options", "Options"),
("filing_path", "Filing"),
("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"),
("upload_first", "Upload lead document"),
("case_information", "Case information"),
("documents", "Documents"),
],
default="options",
max_length=64,
),
),
]
9 changes: 7 additions & 2 deletions efile_app/efile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.db import models
from django.utils import timezone

from efile.workflow import WorkflowStepKey, get_workflow_step_choices
from efile.workflow import ExistingCase, WorkflowStepKey, get_workflow_step_choices


class UserProfile(AbstractUser):
Expand Down Expand Up @@ -54,8 +54,13 @@ class Status(models.TextChoices):
choices=get_workflow_step_choices(),
default=WorkflowStepKey.OPTIONS,
)
workflow_version = models.PositiveSmallIntegerField(default=1)

existing_case = models.CharField(max_length=20, blank=True)
existing_case = models.CharField(
max_length=20,
choices=[(value.value, value.name.title()) for value in ExistingCase],
blank=True,
)
court_code = models.CharField(max_length=100, blank=True)
court_name = models.CharField(max_length=255, blank=True)
case_category_code = models.CharField(max_length=100, blank=True)
Expand Down
9 changes: 7 additions & 2 deletions efile_app/efile/services/drafts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from django.db.models import QuerySet

from efile.models import FilingDocument, FilingDraft, FilingParty
from efile.workflow import WorkflowStepKey
from efile.workflow import WorkflowStepKey, legacy_existing_case_value, normalize_existing_case

ACTIVE_DRAFT_STATUSES = (FilingDraft.Status.DRAFT, FilingDraft.Status.ERROR)
# A draft mid-submission is still the user's current draft (so the submit flow can
Expand Down Expand Up @@ -198,6 +198,8 @@ def write_case_data(
if value is _MISSING:
continue
value = _as_str(value)
if field == "existing_case":
value = normalize_existing_case(value)
if getattr(draft, field) != value:
setattr(draft, field, value)
update_fields.append(field)
Expand Down Expand Up @@ -246,7 +248,9 @@ def read_case_data(draft: FilingDraft | None) -> dict[str, Any]:
data: dict[str, Any] = {}
_put(data, "jurisdiction", draft.jurisdiction)
_put(data, "jurisdiction_id", draft.jurisdiction)
_put(data, "existing_case", draft.existing_case)
# Old screens still branch on yes/no. The durable value is normalized now;
# remove this translation when the last legacy screen is retired.
_put(data, "existing_case", legacy_existing_case_value(draft.existing_case))
_put(data, "court", draft.court_code)
_put(data, "court_name", draft.court_name)
_put(data, "case_category", draft.case_category_code)
Expand Down Expand Up @@ -475,6 +479,7 @@ def draft_snapshot(draft: FilingDraft | None) -> dict[str, Any] | None:
"jurisdiction": draft.jurisdiction,
"status": draft.status,
"current_step": draft.current_step,
"workflow_version": draft.workflow_version,
"existing_case": draft.existing_case,
"court_code": draft.court_code,
"court_name": draft.court_name,
Expand Down
176 changes: 120 additions & 56 deletions efile_app/efile/tests/test_workflow.py
Original file line number Diff line number Diff line change
@@ -1,117 +1,181 @@
from types import SimpleNamespace

import pytest
from django.urls import reverse

from efile.workflow import (
FILING_WORKFLOW,
LEGACY_WORKFLOW,
ExistingCase,
WorkflowStepKey,
get_next_step,
get_previous_step,
get_resume_step_url,
get_step,
get_step_url,
get_visible_workflow,
get_workflow_context,
get_workflow_steps,
legacy_existing_case_value,
normalize_existing_case,
)

EXPECTED_WORKFLOW_KEYS = [
WorkflowStepKey.OPTIONS,
WorkflowStepKey.UPLOAD_FIRST,
WorkflowStepKey.CASE_INFORMATION,
WorkflowStepKey.DOCUMENTS,
WorkflowStepKey.PAYMENT,
WorkflowStepKey.REVIEW,
WorkflowStepKey.CONFIRMATION,
]

def draft(**overrides):
values = {
"current_step": WorkflowStepKey.FILING_PATH,
"workflow_version": 2,
"existing_case": ExistingCase.NEW,
"case_questions_required": False,
"parties": [],
}
values.update(overrides)
return SimpleNamespace(**values)


def keys(workflow):
return [step.key for step in workflow]


def test_target_workflow_declares_every_reorganized_screen():
assert get_workflow_steps() == FILING_WORKFLOW
assert keys(FILING_WORKFLOW) == [
WorkflowStepKey.OPTIONS,
WorkflowStepKey.FILING_PATH,
WorkflowStepKey.UPLOAD_DOCUMENTS,
WorkflowStepKey.EXTRACTION_REVIEW,
WorkflowStepKey.CASE_LOOKUP,
WorkflowStepKey.CASE_CONFIRMATION,
WorkflowStepKey.DOCUMENT_CHECKLIST,
WorkflowStepKey.ORGANIZE_DOCUMENTS,
WorkflowStepKey.YOUR_INFORMATION,
WorkflowStepKey.PARTIES,
WorkflowStepKey.PARTY_DETAILS,
WorkflowStepKey.CASE_QUESTIONS,
WorkflowStepKey.PAYMENT,
WorkflowStepKey.REVIEW,
WorkflowStepKey.CONFIRMATION,
]


def test_legacy_drafts_keep_the_current_linear_route_during_migration():
legacy_draft = draft(current_step=WorkflowStepKey.DOCUMENTS, workflow_version=1)

assert get_visible_workflow(legacy_draft) == LEGACY_WORKFLOW
assert get_previous_step(WorkflowStepKey.PAYMENT, legacy_draft).key == WorkflowStepKey.DOCUMENTS
assert get_next_step(WorkflowStepKey.PAYMENT, legacy_draft).key == WorkflowStepKey.REVIEW


@pytest.mark.parametrize(
("step_key", "label"),
"shared_step",
[
(WorkflowStepKey.OPTIONS, "Options"),
(WorkflowStepKey.UPLOAD_FIRST, "Upload lead document"),
(WorkflowStepKey.CASE_INFORMATION, "Case information"),
(WorkflowStepKey.DOCUMENTS, "Documents"),
(WorkflowStepKey.PAYMENT, "Payment"),
(WorkflowStepKey.REVIEW, "Review"),
(WorkflowStepKey.CONFIRMATION, "Confirmation"),
WorkflowStepKey.OPTIONS,
WorkflowStepKey.PAYMENT,
WorkflowStepKey.REVIEW,
WorkflowStepKey.CONFIRMATION,
],
)
def test_get_step_returns_registered_step(step_key, label):
step = get_step(step_key)
def test_shared_steps_without_draft_context_default_to_legacy(shared_step):
assert get_visible_workflow(current_step=shared_step) == LEGACY_WORKFLOW

assert step.key == step_key
assert step.label == label

@pytest.mark.parametrize(
("value", "expected"),
[
("no", ExistingCase.NEW),
("yes", ExistingCase.EXISTING),
("responding", ExistingCase.EXISTING),
("unsure", ExistingCase.UNSURE),
("", ""),
],
)
def test_existing_case_values_are_normalized(value, expected):
assert normalize_existing_case(value) == expected

def test_get_workflow_steps_returns_ordered_workflow():
assert get_workflow_steps() == FILING_WORKFLOW
assert [step.key for step in get_workflow_steps()] == EXPECTED_WORKFLOW_KEYS

def test_normalized_case_values_can_be_read_by_legacy_clients():
assert legacy_existing_case_value(ExistingCase.NEW) == "no"
assert legacy_existing_case_value(ExistingCase.EXISTING) == "yes"

def test_get_step_raises_key_error_for_invalid_step():
with pytest.raises(KeyError):
get_step("invalid_step")

def test_new_case_skips_lookup_and_confirmation():
new_case = draft(existing_case=ExistingCase.NEW)

def test_get_previous_step_returns_none_for_first_step():
assert get_previous_step(WorkflowStepKey.OPTIONS) is None
assert WorkflowStepKey.CASE_LOOKUP not in keys(get_visible_workflow(new_case))
assert WorkflowStepKey.CASE_CONFIRMATION not in keys(get_visible_workflow(new_case))
assert get_next_step(WorkflowStepKey.EXTRACTION_REVIEW, new_case).key == WorkflowStepKey.DOCUMENT_CHECKLIST


def test_get_previous_step_returns_prior_step():
previous_step = get_previous_step(WorkflowStepKey.CASE_INFORMATION)
def test_existing_case_uses_lookup_and_confirmation():
existing_case = draft(existing_case=ExistingCase.EXISTING)

assert previous_step is not None
assert previous_step.key == WorkflowStepKey.UPLOAD_FIRST
assert WorkflowStepKey.CASE_LOOKUP in keys(get_visible_workflow(existing_case))
assert WorkflowStepKey.CASE_CONFIRMATION in keys(get_visible_workflow(existing_case))
assert get_next_step(WorkflowStepKey.EXTRACTION_REVIEW, existing_case).key == WorkflowStepKey.CASE_LOOKUP


def test_get_next_step_returns_following_step():
next_step = get_next_step(WorkflowStepKey.CASE_INFORMATION)
def test_unsure_case_stays_on_extraction_review():
unsure_case = draft(existing_case=ExistingCase.UNSURE)

assert next_step is not None
assert next_step.key == WorkflowStepKey.DOCUMENTS
assert get_next_step(WorkflowStepKey.EXTRACTION_REVIEW, unsure_case) is None


def test_get_next_step_returns_none_for_last_step():
assert get_next_step(WorkflowStepKey.CONFIRMATION) is None
def test_party_details_only_appear_for_incomplete_parties():
incomplete = SimpleNamespace(first_name="Ada", last_name="", organization_name="")
complete = SimpleNamespace(first_name="Ada", last_name="Lovelace", organization_name="")

assert WorkflowStepKey.PARTY_DETAILS in keys(get_visible_workflow(draft(parties=[incomplete])))
assert WorkflowStepKey.PARTY_DETAILS not in keys(get_visible_workflow(draft(parties=[complete])))


def test_case_questions_only_appear_when_required():
assert WorkflowStepKey.CASE_QUESTIONS not in keys(get_visible_workflow(draft()))
assert WorkflowStepKey.CASE_QUESTIONS in keys(get_visible_workflow(draft(case_questions_required=True)))

def test_get_step_url_reverses_workflow_route():
expected_url = reverse("payment", kwargs={"jurisdiction": "illinois"})

def test_get_step_raises_key_error_for_invalid_step():
with pytest.raises(KeyError):
get_step("invalid_step")


def test_get_step_url_reverses_an_available_route():
expected_url = reverse("payment", kwargs={"jurisdiction": "illinois"})
assert get_step_url(WorkflowStepKey.PAYMENT, "illinois") == expected_url


def test_get_resume_step_url_returns_the_drafts_own_step():
def test_resume_preserves_legacy_draft_routes():
expected_url = reverse("upload", kwargs={"jurisdiction": "illinois"})

assert get_resume_step_url(WorkflowStepKey.DOCUMENTS, "illinois") == expected_url


def test_get_resume_step_url_skips_options_so_resuming_does_not_loop():
# OPTIONS is the model default for older drafts; resuming there would just
# return the user to the page that offered to resume.
def test_resume_skips_options_for_pre_migration_drafts():
expected_url = reverse("upload_first", kwargs={"jurisdiction": "illinois"})

assert get_resume_step_url(WorkflowStepKey.OPTIONS, "illinois") == expected_url


def test_get_resume_step_url_falls_back_for_an_unrecognised_step():
def test_resume_falls_back_for_an_unrecognised_step():
expected_url = reverse("upload_first", kwargs={"jurisdiction": "illinois"})

assert get_resume_step_url("a_step_that_was_removed", "illinois") == expected_url


def test_get_resume_step_url_returns_none_without_a_draft():
def test_resume_returns_none_without_a_draft():
assert get_resume_step_url(None, "illinois") is None


def test_get_workflow_context_includes_current_previous_and_next_urls():
context = get_workflow_context(WorkflowStepKey.PAYMENT, "illinois")
previous_url = reverse("upload", kwargs={"jurisdiction": "illinois"})
next_url = reverse("case_review", kwargs={"jurisdiction": "illinois"})
def test_workflow_context_uses_draft_branch_and_includes_stage_progress():
legacy_draft = draft(current_step=WorkflowStepKey.PAYMENT, workflow_version=1)
context = get_workflow_context(WorkflowStepKey.PAYMENT, "illinois", legacy_draft)

assert context["workflow_current_step"].key == WorkflowStepKey.PAYMENT
assert context["workflow_previous_step"].key == WorkflowStepKey.DOCUMENTS
assert context["workflow_next_step"].key == WorkflowStepKey.REVIEW
assert context["workflow_previous_url"] == previous_url
assert context["workflow_next_url"] == next_url
assert context["workflow_previous_url"] == reverse("upload", kwargs={"jurisdiction": "illinois"})
assert context["workflow_next_url"] == reverse("case_review", kwargs={"jurisdiction": "illinois"})
assert [stage.value for stage in context["workflow_stages"]] == [
"filing",
"upload",
"confirm_case",
"organize_documents",
"fees",
"review",
]
2 changes: 1 addition & 1 deletion efile_app/efile/views/expert_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ def efile_expert_form(request, jurisdiction):
"missing_required_fields": not has_all_required,
"missing_party_info": has_all_required and not has_party_info,
}
context.update(get_workflow_context(WorkflowStepKey.CASE_INFORMATION, jurisdiction))
context.update(get_workflow_context(WorkflowStepKey.CASE_INFORMATION, jurisdiction, filing_draft))

return render(request, "efile/expert_form.html", context)
2 changes: 1 addition & 1 deletion efile_app/efile/views/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ def efile_options(request, jurisdiction):
"resume_url": get_resume_step_url(active_draft.current_step if active_draft else None, jurisdiction),
"has_case_data": bool(case_data or active_draft),
}
context.update(get_workflow_context(WorkflowStepKey.OPTIONS, jurisdiction))
context.update(get_workflow_context(WorkflowStepKey.OPTIONS, jurisdiction, active_draft))

return render(request, "efile/options.html", context)
2 changes: 1 addition & 1 deletion efile_app/efile/views/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ def efile_payment(request, jurisdiction):
"case_data": case_data,
"filing_draft": draft_snapshot(filing_draft),
}
context.update(get_workflow_context(WorkflowStepKey.PAYMENT, jurisdiction))
context.update(get_workflow_context(WorkflowStepKey.PAYMENT, jurisdiction, filing_draft))

return render(request, "efile/payment.html", context)
2 changes: 1 addition & 1 deletion efile_app/efile/views/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@ def case_review(request, jurisdiction):
"document_type": friendly_document_type,
},
}
context.update(get_workflow_context(WorkflowStepKey.REVIEW, jurisdiction))
context.update(get_workflow_context(WorkflowStepKey.REVIEW, jurisdiction, filing_draft))

return render(request, "efile/review.html", context)
2 changes: 1 addition & 1 deletion efile_app/efile/views/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ def efile_upload(request, jurisdiction):
"filing_type_raw": case_classification["filing_type"],
"court_raw": case_classification["court"],
}
context.update(get_workflow_context(WorkflowStepKey.DOCUMENTS, jurisdiction))
context.update(get_workflow_context(WorkflowStepKey.DOCUMENTS, jurisdiction, filing_draft))

return render(request, "efile/upload.html", context)
Loading