From 4cb7a50045601e75877b60b4caf8f579aca3810d Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sat, 5 Sep 2026 09:35:12 -0400 Subject: [PATCH 1/3] Add structured court selector per jurisdiction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the flat court dropdown with a multi-step court selector that walks a filer through the right questions for each jurisdiction: - Illinois routes by county using a regex group on the Tyler court list, then optionally by division (Cook County gets sub-grouped by district). - Massachusetts asks for department first, then uses MACourts to match a typed town/address to the courts that serve it; the full list is always shown alongside as a fallback. - Vermont asks for Superior Court division, then uses VTCourts to look up the unit from a town, county, or ZIP; the dropdown stays for those who know their unit. New services and files: - efile/services/court_selection.py -- YAML-driven selector engine; parses the court_selector block, evaluates when: conditions, filters courts by name_pattern/exclude_name_pattern, and computes defaults. - efile/services/court_location.py -- location-to-court matchers for MA (MACourts) and VT (VTCourts), plus parse_place() for free-text input. - efile/api/court_selector_views.py -- /dropdowns/court-selector/ endpoint that serialises the selector state for the active jurisdiction. - efile/static/js/court-selector.js + css/components/court-selector.css -- progressive-disclosure UI; each step appears only when its when: conditions are met, and location steps fire the court-lookup API. Supporting changes: - is_non_filing_court() extracted from dropdown_views into court_selection so the same filter applies to both the flat list and the selector. - EXTRACTION_KEY_ALIASES + _normalized_key() added to extraction_fields so LLM variants of party-name labels ("plaintiff name", "petitioner names" …) all resolve to the canonical key. - State YAML files (illinois, massachusetts, vermont) gain the court_selector block documented in jurisdiction-config.md. - Playwright test utilities updated with a selectCourt() helper that drives the new selector UI; existing tests updated to use it. --- Dockerfile | 5 +- .../partners-courts/jurisdiction-config.md | 139 +++ efile_app/efile/api/court_selector_views.py | 91 ++ efile_app/efile/api/dropdown_views.py | 24 +- efile_app/efile/api/urls.py | 2 + .../prompts/document_evidence_extraction.yaml | 2 + efile_app/efile/services/court_location.py | 212 +++++ efile_app/efile/services/court_selection.py | 824 ++++++++++++++++++ efile_app/efile/services/extraction_fields.py | 28 +- .../efile/static/config/states/illinois.yaml | 255 ++++++ .../static/config/states/massachusetts.yaml | 143 +++ .../efile/static/config/states/vermont.yaml | 97 +++ .../static/css/components/court-selector.css | 229 +++++ .../efile/static/css/reorganized-flow.css | 7 + efile_app/efile/static/js/case-lookup.js | 24 + efile_app/efile/static/js/court-selector.js | 414 +++++++++ .../efile/static/js/extraction-review.js | 36 + .../efile/templates/efile/case_lookup.html | 10 +- .../templates/efile/extraction_review.html | 3 + .../efile/templates/efile/workflow_base.html | 2 + efile_app/efile/tests/test_court_selection.py | 574 ++++++++++++ .../efile/tests/test_document_extractions.py | 17 +- efile_app/playwright.config.js | 10 +- efile_app/pyproject.toml | 6 + .../document-classification-sentinel.spec.js | 24 +- .../tests/reorganized-filing-matrix.spec.js | 43 +- .../tests/test-optional-services-e2e.spec.js | 32 +- efile_app/tests/test-utils.js | 104 ++- efile_app/uv.lock | 68 ++ 29 files changed, 3335 insertions(+), 90 deletions(-) create mode 100644 efile_app/efile/api/court_selector_views.py create mode 100644 efile_app/efile/services/court_location.py create mode 100644 efile_app/efile/services/court_selection.py create mode 100644 efile_app/efile/static/css/components/court-selector.css create mode 100644 efile_app/efile/static/js/court-selector.js create mode 100644 efile_app/efile/tests/test_court_selection.py diff --git a/Dockerfile b/Dockerfile index 2920b109..c1e32475 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,10 @@ FROM python:3.12-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -# Install system deps (curl for uv installer, build tools only if needed) +# Install system deps (curl for the uv installer; git because MACourts and +# VTCourts are installed straight from GitHub until they are on PyPI) RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates \ + curl ca-certificates git \ && rm -rf /var/lib/apt/lists/* # Install uv diff --git a/docs/docs/partners-courts/jurisdiction-config.md b/docs/docs/partners-courts/jurisdiction-config.md index 3b27693b..d0f3f4a5 100644 --- a/docs/docs/partners-courts/jurisdiction-config.md +++ b/docs/docs/partners-courts/jurisdiction-config.md @@ -140,3 +140,142 @@ A misspelled key is ignored, which means the page quietly keeps the default word ### Translation Strings configured here are translatable along with the rest of the application. `xgettext` cannot read YAML, so `manage.py extract_config_text` restates them in a generated Python file that `makemessages` reads. Each string carries its key as the gettext message context, so one key's Illinois wording and Vermont wording stay separate messages for a translator. See `efile_app/efile/locale/README.md` for the full workflow. + +--- + +## 4. The court question + +Every filing needs a court, and the e-filing service answers that question with one flat list: 207 courts in Illinois, 170 in Massachusetts. A single dropdown over that list asks a filer to recognize their court among the ones that happen to sort next to it. + +`court_selector` replaces the list with the questions your state's court structure is actually made of. Illinois routes by county, Massachusetts by court department plus a place the filer knows, Vermont by Superior Court unit — and each of them narrows the same live court list. A jurisdiction with no `court_selector` section keeps the flat list. + +```yaml +court_selector: + title: "Choose the Vermont court for this filing" + lede: >- + Start with the court on your paperwork. Superior Court filings then need a + division, and most divisions need the court unit the case belongs to. + steps: + - id: level + type: choice + label: "What Vermont court is this filing for?" + options: + - value: superior + label: "Superior Court" + help: "Vermont's trial court. You will choose a division next." + courts: + name_pattern: "Unit$|^Environmental Division$" + - value: supreme + label: "Supreme Court" + courts: + name_pattern: "^Supreme Court$" + + - id: unit + type: select + when: + level: [superior] + label: "Which court unit?" + placeholder: "Choose a unit…" + options_from_courts: + match: + name_pattern: " Unit$" + strip: " Unit$" + label: "{name} ({stem} County)" +``` + +### How the questions run + +An answered question folds to a single line with a **Change** link, and once there is a court, every question folds and the screen states the answer. The court field shares a row with the case category and type, so the cascade has to collapse rather than push them down the page. + +Steps are asked in order. A step is shown when its `when:` conditions hold and it has something to ask; the filer's answer narrows the pool of courts, and the deepest answer wins, so "Cook County" is replaced by the division under it. When the pool is down to one court, that is the court. When it is down to a handful, the filer chooses among those rather than among two hundred. + +| Key | Meaning | +| --- | --- | +| `type` | `choice` (radio cards), `select` (a dropdown), or `location` (a place lookup). | +| `when` | `{level: [trial]}` shows the step only for those answers; `{department: {not: [land]}}` hides it for those. | +| `options` | Written-out answers. Each may carry a `courts:` query and any data a matcher needs. | +| `options_from_courts` | Answers read off the live court list instead — counties, Cook County's divisions, Vermont's units. A step generated this way disappears when it has fewer than two options, which is how only the counties that divide their Circuit Court get asked about divisions. | +| `option_groups` | One option per `members:` entry, all sharing the group's `courts:` query. Illinois asks which county an appeal came from and answers with its appellate district. | +| `alternative_to` | Two questions that name the same court. Both stay on screen; answering either one answers for both. | +| `short_label` | What the question is called once it is answered and folded to one line, e.g. `Division or courthouse`. Defaults to the full `label`, which is usually too long to read well there. | +| `default_by` / `default_hint` | Start a question at an answer, chosen from an earlier one: `{answer: county, values: {Cook: "cook:cvd"}}` starts a Cook County filing at Municipal Civil. A suggested answer never folds away and carries `default_hint` saying it is a suggestion, so it cannot be mistaken for something the filer said. | + +### Naming the courts + +E-filing services name courts so that they sort. Massachusetts lists a court as `Juvenile Court -- Suffolk County -- Boston`, department first, because that is how the list is organized — but nobody calls it that, and a filer holding paperwork is looking for the Boston Juvenile Court. `court_names` puts the name back: + +```yaml +court_names: + - match: '^Juvenile Court -+ (?P.+?) -+ (?P.+)$' + name: "{place} Juvenile Court - {county}" + - match: '^District Court -+ (?P.+)$' + name: "{place} District Court" +``` + +Each rule is a regular expression with named captures, and `name` is the rewrite. The first rule that matches wins; a name no rule matches is already right, so Vermont's `Addison Unit` and Massachusetts' `Middlesex Probate and Family Court` need no rules at all. + +The rewrite happens once, when the court list is read, so everything downstream — the questions, the court finally chosen, and what is saved on the filing — uses the readable name. That includes the `courts:` queries below, so write them against the names as they read, not as the service lists them. + +### Court queries + +A `courts:` query says which courts an answer leads to. Every rule is optional and they combine with "and": + +| Rule | Matches | +| --- | --- | +| `codes` | Exact court codes, in the order given. | +| `code_prefix`, `code_pattern` | The court's code starts with, or matches, this. | +| `name_pattern` | The court's name matches this (case-insensitive). | +| `exclude_code_pattern`, `exclude_name_pattern` | Drop the courts these match. | +| `group` | The court belongs to this group — see `group_by` below. | + +Courts that are only a heading over the courts beneath them never reach the questions at all. "Cook County" is such a row: every Cook filing goes to one of the eighty locations whose code hangs off it, and choosing the county itself returns an empty case-category list with nothing to explain why. A court is dropped only when the e-filing service leaves it out of its fileable list **and** other courts hang off its code — Cook County - Chancery fails the second test, has locations under it, and takes filings of its own. A caption that names only such a county still routes: the questions it does settle are filled in, and `default_by` can start the rest somewhere sensible. + +Prefer `name_pattern`. Court **names** are stable and readable; Tyler's codes differ from court to court and change without notice. `{value}`, and any earlier step's id, can be used as a placeholder inside a query. + +`option_group_pattern` groups a dropdown under headings. It is matched against each court's name (after `strip`), and its named captures say which part is which: `group` is the heading, `label` is what to show under it, and `extra` is anything worth adding only when it is not the heading's own place. Cook County lists more than eighty locations, and reading them as one alphabetical run is what made the old dropdown unusable. + +```yaml + option_group_pattern: '^(?P