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
60 changes: 60 additions & 0 deletions docs/docs/partners-courts/jurisdiction-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,63 @@ and [Vermont small claims guidance](https://www.vtcourts.gov/civil/suing-and-bei

The guidance configuration is cached per web process. Restart the web service
after editing it; no filing catalog rebuild or database migration is required.

## Contextual filing guidance

Use `filing_guidance` for informational warnings and help that depend on the
filer's saved choices. These messages never require an acknowledgment or block
navigation or submission. Court requirements accepted on Review remain separate.

Each message appears above the form on its configured workflow steps. Choose a
step before the relevant task: for example, `upload_documents` for statewide
upload help, `document_checklist` or `organize_documents` for court-specific
document instructions, and `payment` for payment help. Include `review` when the
same information is useful before submission. Court-specific help appears only
after a court is saved; use a later step if court selection happens after upload.

The following example uses **placeholder codes and copy**, not actual court
instructions. Replace them with current provider codes and partner-reviewed text
and links before deploying:

```yaml
filing_guidance:
- id: local_document_help
steps: [document_checklist, organize_documents, review]
title: "Preparing documents for this court"
text: "Read this court's document instructions if you need help preparing your filing."
when:
court_codes: ["example:division-a", "example:division-b"]
case_type_codes: ["example-case-type"]
existing_case: ["new"]
resources:
- label: "Court document instructions"
url: "https://court.example.org/filing-help"
```

- `id`, `steps`, `title`, and `text` are required. IDs must be unique within the
jurisdiction. Titles and text are plain text; HTML is escaped.
- Omit `when` for statewide help. All configured conditions must match; within
each list, any one value matches. Missing selections do not match a condition.
- Supported conditions are `court_codes`, `case_category_codes`, `case_type_codes`,
`case_subtype_codes`, `filing_type_codes`, and `existing_case` (`new` or
`existing`). Code matching is exact. Quote numeric provider codes in YAML.
- A `filing_type_codes` condition matches any saved document, including supporting
documents. It does not use an AI prediction or an obsolete draft-level default.
- To target a county, division, or district, list its current filing court codes
in `court_codes`. Names, prefixes, and inferred geographic matches are not used.
Maintain this list when provider codes change.
- `resources` is optional. Each resource needs a descriptive `label` and an
absolute HTTPS `url`. Links open in the same tab.
- Supported steps are the active keys in `efile/workflow.py` (`FILING_WORKFLOW`).
Messages appear in configuration order. They refresh on the next rendered page
after choices are saved, including when a draft is resumed. They do not update
while a dropdown has an unsaved selection.

No local court guidance is enabled by default. Add reviewed messages to the
appropriate state's YAML file. Run `uv run python manage.py check` after editing;
invalid configuration produces `efile.W004` and that jurisdiction's guidance is
omitted. Filing remains available. Use
`uv run python manage.py check --fail-level WARNING` to reject these errors in a
configuration review. Run `uv run python manage.py extract_config_text` and then
`uv run python manage.py makemessages -l es` to include the new copy and link
labels in the existing translation workflow.
5 changes: 5 additions & 0 deletions efile_app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ EFSP_URL = "https://efile-test.suffolklitlab.org"
# the repo-root .env instead.
EFSP_TEST_DOCUMENT_URL = "https://raw.githubusercontent.com/SuffolkLITLab/LITEFile/main/testing/sample_test.pdf"

# Local development only. Shows the demonstration filing guidance messages so the
# guidance boxes can be checked in a browser. Leave unset to show only the
# guidance configured for each state.
# FILING_GUIDANCE_DEMO_FILE = "../testing/filing-guidance-demo.yaml"

# AWS S3 Configuration
AWS_S3_ENDPOINT_URL = "http://localstack:4566" # if mocking S3 locally, see [testing README](../testing/README.md).
AWS_ACCESS_KEY_ID = "your-aws-access-key-id-here"
Expand Down
17 changes: 17 additions & 0 deletions efile_app/efile/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,20 @@ def configured_ui_text_keys_are_known(app_configs, **kwargs):
)
)
return problems


@register()
def configured_filing_guidance_is_valid(app_configs, **kwargs):
"""Report invalid informational rules before partners deploy their copy."""
from efile.services.filing_guidance import guidance_errors
from efile.utils.config_loader import config_loader

return [
Warning(
f"{jurisdiction}.yaml: {message}",
hint="Correct filing_guidance; invalid guidance is omitted, and never blocks a filing.",
id="efile.W004",
)
for jurisdiction in config_loader.get_available_jurisdictions()
for message in guidance_errors(config_loader.load_jurisdiction_config(jurisdiction))
]
9 changes: 8 additions & 1 deletion efile_app/efile/management/commands/extract_config_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from django.core.management.base import BaseCommand, CommandError

from efile.services.filing_guidance import guidance_errors, guidance_strings
from efile.utils.config_loader import config_loader
from efile.utils.ui_text import UI_STRINGS, config_overrides

Expand Down Expand Up @@ -54,11 +55,17 @@ def _render():
for jurisdiction in config_loader.get_available_jurisdictions():
config = config_loader.load_jurisdiction_config(jurisdiction) or {}
overrides = {key: value for key, value in config_overrides(config).items() if key in UI_STRINGS}
errors = guidance_errors(config)
if errors:
raise CommandError(f"{jurisdiction}.yaml: {'; '.join(errors)}")
overrides.update(guidance_strings(config))
if not overrides:
continue
lines.append(f" # {jurisdiction}.yaml\n")
for key, value in sorted(overrides.items()):
description = UI_STRINGS[key].description
description = (
UI_STRINGS[key].description if key in UI_STRINGS else "Jurisdiction-maintained filing guidance."
)
if description:
lines.append(f" # Translators: {description}\n")
# Written the way ruff would format it -- double quotes, one argument
Expand Down
179 changes: 179 additions & 0 deletions efile_app/efile/services/filing_guidance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""Informational, jurisdiction-maintained help matched to saved filing choices."""

import logging
import os
from urllib.parse import urlsplit

import yaml
from django.conf import settings
from django.utils.translation import pgettext

from efile.utils.config_loader import config_loader

logger = logging.getLogger(__name__)

# Exact provider codes, not names inferred from a document or a court caption.
DRAFT_CONDITIONS = {
"court_codes": "court_code",
"case_category_codes": "case_category_code",
"case_type_codes": "case_type_code",
"case_subtype_codes": "case_subtype_code",
"existing_case": "existing_case",
}
CONDITION_KEYS = {*DRAFT_CONDITIONS, "filing_type_codes"}
RULE_KEYS = {"id", "steps", "title", "text", "when", "resources"}

# jurisdiction -> (loaded config, demo file version, valid rules or None)
_checked_rules = {}


def _strings(value):
return isinstance(value, list) and bool(value) and all(isinstance(item, str) and item.strip() for item in value)


def _resource_is_valid(resource):
if not isinstance(resource, dict) or set(resource) != {"label", "url"}:
return False
if not all(isinstance(resource[key], str) and resource[key].strip() for key in ("label", "url")):
return False
try:
url = urlsplit(resource["url"])
return url.scheme == "https" and bool(url.hostname) and not url.username and not url.password
except ValueError:
return False


def guidance_errors(config):
"""Validate authoring errors without treating guidance as a filing requirement."""
from efile.workflow import FILING_WORKFLOW

rules = config.get("filing_guidance", [])
if not isinstance(rules, list):
return ["filing_guidance must be a list."]
steps = {step.key for step in FILING_WORKFLOW}
errors = []
identifiers = set()
for index, rule in enumerate(rules):
prefix = f"filing_guidance[{index}]"
if not isinstance(rule, dict):
errors.append(f"{prefix} must be a mapping.")
continue
if set(rule) - RULE_KEYS:
errors.append(
f"{prefix} contains unknown keys: {', '.join(sorted(str(key) for key in set(rule) - RULE_KEYS))}."
)
for key in ("id", "title", "text"):
if not isinstance(rule.get(key), str) or not rule[key].strip():
errors.append(f"{prefix}.{key} must be nonempty text.")
identifier = rule.get("id")
if isinstance(identifier, str):
if identifier in identifiers:
errors.append(f"{prefix}.id duplicates {identifier}.")
identifiers.add(identifier)
if not _strings(rule.get("steps")) or not set(rule["steps"]).issubset(steps):
errors.append(f"{prefix}.steps must list active workflow step keys.")
conditions = rule.get("when", {})
if not isinstance(conditions, dict):
errors.append(f"{prefix}.when must be a mapping.")
else:
for key, values in conditions.items():
if key not in CONDITION_KEYS or not _strings(values):
errors.append(f"{prefix}.when.{key} must be a supported condition with a nonempty list of strings.")
elif key == "existing_case" and not set(values).issubset({"new", "existing"}):
errors.append(f"{prefix}.when.existing_case accepts only new and existing.")
resources = rule.get("resources", [])
if not isinstance(resources, list) or not all(_resource_is_valid(resource) for resource in resources):
errors.append(f"{prefix}.resources must contain label and absolute HTTPS url pairs.")
return errors


def guidance_strings(config):
"""Strings shared by runtime translation and the config extraction command."""
for rule in config.get("filing_guidance", []):
prefix = f"filing_guidance.{rule['id']}"
for field in ("title", "text"):
yield f"{prefix}.{field}", rule[field]
for index, resource in enumerate(rule.get("resources", [])):
yield f"{prefix}.resources.{index}.label", resource["label"]


def _demo_file_version():
path = getattr(settings, "FILING_GUIDANCE_DEMO_FILE", "")
if not path:
return None
try:
stat = os.stat(path)
except OSError:
return (path, None)
return (path, stat.st_mtime_ns, stat.st_size)


def _demo_rules(version, jurisdiction):
"""Local demonstration guidance, kept out of the deployed jurisdiction files."""
if version is None:
return []
try:
with open(version[0]) as demo_file:
rules = (yaml.safe_load(demo_file) or {}).get(jurisdiction, [])
except (OSError, yaml.YAMLError, AttributeError):
logger.warning("Could not read the filing guidance demo file %s", version[0])
return []
return rules if isinstance(rules, list) else [rules]


def _rules(jurisdiction):
"""The jurisdiction's guidance rules, validated once per loaded config."""
config = config_loader.load_jurisdiction_config(jurisdiction)
demo_version = _demo_file_version()
checked = _checked_rules.get(jurisdiction)
if checked and checked[0] is config and checked[1] == demo_version:
return checked[2]
rules = config.get("filing_guidance", [])
demo = _demo_rules(demo_version, jurisdiction)
if demo:
rules = [*rules, *demo] if isinstance(rules, list) else rules
errors = guidance_errors({"filing_guidance": rules})
if errors:
# Invalid informational copy must not prevent a filer from proceeding.
# The system check reports it to maintainers before deployment.
logger.warning("Invalid filing guidance for %s: %s", jurisdiction, "; ".join(errors))
rules = None
_checked_rules[jurisdiction] = (config, demo_version, rules)
return rules


def filing_guidance(draft, step, *, jurisdiction=None):
if draft is None:
return []
rules = _rules(jurisdiction or draft.jurisdiction)
if not rules:
return []
matches = []
document_codes = None
for rule in rules:
if step not in rule["steps"]:
continue
conditions = rule.get("when", {})
if any(
getattr(draft, field, "") not in conditions[key]
for key, field in DRAFT_CONDITIONS.items()
if key in conditions
):
continue
if "filing_type_codes" in conditions:
if document_codes is None:
document_codes = set(draft.documents.values_list("filing_type_code", flat=True)) - {""}
if not document_codes.intersection(conditions["filing_type_codes"]):
continue
prefix = f"filing_guidance.{rule['id']}"
matches.append(
{
"title": pgettext(f"{prefix}.title", rule["title"]),
"text": pgettext(f"{prefix}.text", rule["text"]),
"resources": [
{"label": pgettext(f"{prefix}.resources.{index}.label", resource["label"]), "url": resource["url"]}
for index, resource in enumerate(rule.get("resources", []))
],
}
)
return matches
4 changes: 4 additions & 0 deletions efile_app/efile/settings_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@
# and URLs; only the URL handed to the proxy changes. Defined here rather than in
# settings_base so no environment variable can enable it outside development.
EFSP_TEST_DOCUMENT_URL = os.getenv("EFSP_TEST_DOCUMENT_URL", "").strip()
# Extra filing guidance for local validation, such as
# ../testing/filing-guidance-demo.yaml. Development only, like the setting above,
# so demonstration messages can never be shown to filers on a deployed site.
FILING_GUIDANCE_DEMO_FILE = os.getenv("FILING_GUIDANCE_DEMO_FILE", "").strip()
CSRF_TRUSTED_ORIGINS = [
"http://localhost",
"http://127.0.0.1",
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/case_confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ <h1>{% translate "Is this your court case?" %}</h1>
<p class="workflow-lede">{% translate "Check the details before you add this filing to the case." %}</p>
</div>
</div>
{% include "efile/partials/filing_guidance.html" %}
<dl class="case-summary">
<div class="case-summary__primary">
<dt>{% translate "Case name" %}</dt>
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/case_lookup.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<section class="workflow-card workflow-card--narrow">
<div class="workflow-eyebrow">{% translate "Confirm case" %}</div>
<h1>{% translate "Find your court case" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% translate "Choose the court and enter the case number exactly as it appears on your documents." %}
</p>
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/case_questions.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<section class="workflow-card">
<div class="workflow-eyebrow">{% translate "People" %}</div>
<h1>{% translate "A few questions about your case" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">{% translate "The court needs these answers for the case type you selected." %}</p>
<form method="post" id="case-questions-form">
{% csrf_token %}
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</div>
<div class="workflow-eyebrow">{% translate "Submitted" %}</div>
<h1>{% translate "Your filing was sent to the court" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% translate "We will email you when the court accepts, returns, or asks for changes to this filing." %}
</p>
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/document_checklist.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<section class="workflow-card">
<div class="workflow-eyebrow">{% translate "Check documents" %}</div>
<h1>{% translate "Check your documents" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% translate "Review the files below. Add anything else you want the court to get with this filing." %}
</p>
Expand Down
2 changes: 2 additions & 0 deletions efile_app/efile/templates/efile/extraction_review.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{% ui_text "extraction_review.case_category_help" as case_category_help %}
{% if has_guesses %}
<h1>{% translate "Check what we read from your document" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% if ai_opted_out %}
{% translate "You turned AI off, so we searched the text of your document for a form number and a case number instead. These are clues from the document, not answers you gave us, and the search can be wrong. Correct anything that is." %}
Expand Down Expand Up @@ -94,6 +95,7 @@ <h2 class="confirm-filing-details-heading">{% translate "Confirm the details use
{% endif %}
{% else %}
<h1>{% translate "Tell us about your case" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<div class="alert alert-info">
{% if extraction_failed %}
{% translate "Our system could not pull details from your document. You can enter the details below, or" %}
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/filing_path.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<section class="workflow-card workflow-card--narrow">
<div class="workflow-eyebrow">{% translate "Start" context "workflow stage" %}</div>
<h1>{% translate "What are you trying to do?" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% translate "Choose the option that best describes your filing. You can change this after we review your documents." %}
</p>
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/filing_unavailable.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{% block workflow_content %}
<section class="workflow-card workflow-card--narrow">
<h1>{% translate "Filing is unavailable" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<div class="alert alert-warning" role="alert">{{ availability_message }}</div>
<dl class="case-summary">
<div>
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/templates/efile/organize_documents.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<section class="workflow-card">
<div class="workflow-eyebrow">{% translate "Organize documents" %}</div>
<h1>{% translate "Organize your documents" %}</h1>
{% include "efile/partials/filing_guidance.html" %}
<p class="workflow-lede">
{% translate "Tell the court what each PDF is and if it should be public or confidential. You can also rename and reorder additional documents." %}
</p>
Expand Down
Loading
Loading