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
11 changes: 11 additions & 0 deletions docs/docs/admin/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ OPENAI_API_KEY="sk-..."
```


## Court filing availability

Court restrictions live in the jurisdiction YAML, not in environment variables.
Use `court_specific_requirements.<court>.filing_availability` to block a whole
court or selected human-readable category, case-type, and filing-type names.
The default is enabled; exact-name and explicit regex matching are supported.

The [filing availability guide](../partners-courts/filing-availability.md) covers
the complete schema, examples, immediate filer warnings, server enforcement,
deployment, re-enabling, and troubleshooting.

## Document preparation and previews

Configure Gotenberg 8.16 or newer for Word conversion and PDF form flattening.
Expand Down
393 changes: 393 additions & 0 deletions docs/docs/partners-courts/filing-availability.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/docs/partners-courts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ efile_app/efile/static/config/

## Guides in this section

1. [**Jurisdiction & court configuration**](./jurisdiction-config.md): Setting up state metadata, logos, court codes, and clerk contact numbers.
1. [**Jurisdiction & court configuration**](./jurisdiction-config.md): Setting up state metadata, logos, court codes, and clerk contact numbers. See [Filing availability](./filing-availability.md) to disable filing for courts or selected category, case-type, and filing-type names, with immediate explanations for filers.
2. [**Document checklists & filing plans**](./document-checklists.md): Authoring plain-language document checklists with requirement levels and role-based conditions.
3. [**Customizing AI extraction & prompts**](./ai-customization.md): Fine-tuning LLM extraction prompts and field dictionaries for court documents.
4. [**Docassemble & AssemblyLine integration**](./interview-integration.md): Connecting automated interview workflows to e-file directly.
13 changes: 13 additions & 0 deletions docs/docs/partners-courts/jurisdiction-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,16 @@ three states are saved in
`efile_app/efile/tests/fixtures/fee_code_samples.json` for regression tests.
State configuration changes refresh the cached state settings on the next
request.

## Temporarily disable filing

Add `filing_availability` under a court's `court_specific_requirements` entry to
block the whole court or selected categories, case types, and filing types.
Filing is enabled by default. Type selectors match human-readable names exactly
or with explicit regexes, independent of Tyler's numeric type IDs. Filers see
restrictions immediately after choosing an affected court or type.

See [Control filing availability](./filing-availability.md) for the complete
field reference, Cook County hearing-scheduling example, county-prefix rules,
message precedence, deployment and re-enabling instructions, troubleshooting,
and live-check API.
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const sidebars = {
items: [
'partners-courts/index',
'partners-courts/jurisdiction-config',
'partners-courts/filing-availability',
'partners-courts/document-checklists',
'partners-courts/ai-customization',
'partners-courts/interview-integration',
Expand Down
24 changes: 24 additions & 0 deletions efile_app/efile/api/filing_availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Read-only availability checks as a filer changes court and type selections."""

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

from efile.services.filing_availability import filing_unavailable_message
from efile.utils.config_loader import InvalidJurisdiction


@require_http_methods(["GET"])
def get_filing_availability(request):
try:
message = filing_unavailable_message(
request.GET.get("jurisdiction") or request.session.get("jurisdiction"),
request.GET.get("court", ""),
case_category=request.GET.get("case_category_name", ""),
case_type=request.GET.get("case_type_name", ""),
filing_types=request.GET.getlist("filing_type_name"),
)
except InvalidJurisdiction as error:
return JsonResponse({"success": False, "error": str(error)}, status=400)
response = JsonResponse({"success": True, "available": not bool(message), "message": message})
response.headers["Cache-Control"] = "no-store"
return response
2 changes: 2 additions & 0 deletions efile_app/efile/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
get_optional_services,
get_party_types,
)
from .filing_availability import get_filing_availability
from .filing_views import get_filings, payment_fees
from .payment_views import delete_payment_account, waiver_account
from .suffolk_api_views import get_party_types_from_suffolk_api, lookup_case
Expand All @@ -44,6 +45,7 @@
path("dropdowns/optional-services/", get_optional_services, name="optional_services"),
path("dropdowns/party-types/", get_party_types, name="party_types"),
path("dropdowns/name-suffixes/", get_name_suffixes, name="name_suffixes"),
path("filing-availability/", get_filing_availability, name="filing_availability"),
# Form configuration endpoints
path("form-config/", get_form_config, name="form_config"),
path("case-type-config/", get_case_type_config, name="case_type_config"),
Expand Down
194 changes: 194 additions & 0 deletions efile_app/efile/services/filing_availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""Deployment-owned restrictions on filing through LITEFile, matched by human-readable type names."""

import re
from urllib.parse import quote, urlencode

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import render
from django.utils.translation import gettext as _

from efile.models import FilingDocument
from efile.services.efsp_payload import _EfspLookups
from efile.utils.config_loader import config_loader
from efile.workflow import ExistingCase, WorkflowStepKey, get_step_url, get_workflow_context

_SELECTORS = ("case_categories", "case_types", "filing_types")


def _court_availability(jurisdiction, court):
courts = config_loader.load_jurisdiction_config(jurisdiction).get("court_specific_requirements") or {}
keys = [court]
if ":" in court:
keys.append(court.split(":", 1)[0] + ":*")
availabilities = [(courts.get(key) or {}).get("filing_availability") or {} for key in keys]
for availability in availabilities:
_validate(availability)
return availabilities


def _validate(availability):
"""Reject a malformed rule on every check, not only when a name reaches it.

Otherwise a draft with blank saved names passes the early checks and the
rule first fails at submit, after the draft is claimed.
"""
for rule in availability.get("rules") or []:
for name in _SELECTORS:
matchers = rule.get(name)
if matchers is None:
continue
if not isinstance(matchers, list):
raise ImproperlyConfigured(f"Filing availability {name} must be a list of names.")
for matcher in matchers:
if isinstance(matcher, str):
continue
if not (isinstance(matcher, dict) and isinstance(matcher.get("regex"), str)):
raise ImproperlyConfigured("Availability selectors must contain names or {regex: pattern} entries.")
try:
re.compile(matcher["regex"])
except re.error as error:
raise ImproperlyConfigured(f"Invalid filing availability regex: {matcher['regex']!r}") from error


def _selectors(availabilities):
"""The selectors this court's rules actually use."""
return {
name
for availability in availabilities
for rule in availability.get("rules") or []
for name in _SELECTORS
if rule.get(name)
}


def _matches_name(name, matcher):
"""Strings match exactly; explicit regex entries match the entire name."""
if not name:
return False
return name == matcher if isinstance(matcher, str) else re.fullmatch(matcher["regex"], name) is not None


def filing_unavailable_message(jurisdiction, court, *, case_category="", case_type="", filing_types=()):
"""Return a reason, or an empty string when no restriction matches.

Exact court settings are checked before a county prefix (``cook:*``).
Rules are additive: an exact court cannot enable a county-wide restriction.
Values within a selector are alternatives; selectors within a rule must all
match. Category, case-type, and filing-type values are human-readable names,
never Tyler numeric IDs. Matching is case-sensitive; regex flags are explicit.
"""
return _unavailable_message(
_court_availability(jurisdiction, court),
case_category=case_category,
case_type=case_type,
filing_types=filing_types,
)


def _unavailable_message(availabilities, *, case_category="", case_type="", filing_types=()):
selections = {
"case_categories": {str(case_category or "").strip()} - {""},
"case_types": {str(case_type or "").strip()} - {""},
"filing_types": {str(value).strip() for value in filing_types if value},
}
for availability in availabilities:
fallback = availability.get("message") or _(
"LITEFile cannot submit this filing to this court right now. Contact the court clerk to ask how to file."
)
# Specific explanations take precedence over the court's generic one.
for rule in availability.get("rules") or []:
# An empty selector (``case_types:``) matches nothing, like ``[]``.
selectors = [name for name in selections if name in rule]
if selectors and all(
any(_matches_name(value, matcher) for value in selections[name] for matcher in rule[name] or [])
for name in selectors
):
return rule.get("message") or fallback
if availability.get("enabled") is False:
return fallback
return ""


def draft_unavailable_message(draft):
availabilities = _court_availability(draft.jurisdiction, draft.court_code)
# Most courts have no filing-type rule; skip the document query for them.
filing_types = (
FilingDocument.objects.filter(draft=draft).values_list("filing_type_name", flat=True)
if "filing_types" in _selectors(availabilities)
else ()
)
return _unavailable_message(
availabilities,
case_category=draft.case_category_name,
case_type=draft.case_type_name,
filing_types=filing_types,
)


def outgoing_unavailable_message(jurisdiction, court, case_data, payload):
"""Resolve outgoing IDs to the court's names; never trust client labels at submit.

Lookups are only needed for selectors configured for this court. A failed
lookup blocks submission, rather than letting an unknown name evade a rule.
"""
availabilities = _court_availability(jurisdiction, court)
selectors = _selectors(availabilities)
if not selectors:
return _unavailable_message(availabilities)
lookups = _EfspLookups()
base = f"{settings.EFSP_URL}/jurisdictions/{quote(jurisdiction, safe='')}/codes/courts/{quote(court, safe=':')}"
category = payload.get("efile_case_category") or case_data.get("case_category", "")
case_type = payload.get("efile_case_type") or case_data.get("case_type", "")
initial = not (payload.get("previous_case_id") or case_data.get("previous_case_id"))

def resolve(path, codes):
choices = lookups.get(f"{base}/{path}")
if not isinstance(choices, list):
choices = []
names = {
str(item["code"]): item["name"]
for item in choices
if isinstance(item, dict) and "code" in item and isinstance(item.get("name"), str)
}
if any(not names.get(str(code)) for code in codes):
raise ValueError(_("We could not confirm this filing's availability with the court. Try again later."))
return [names[str(code)] for code in codes]

category_name = resolve("categories", [category])[0] if "case_categories" in selectors else ""
type_name = (
resolve("case_types/?" + urlencode({"category_id": category}), [case_type])[0]
if "case_types" in selectors
else ""
)
filing_names = []
if "filing_types" in selectors:
bundles = payload.get("al_court_bundle", [])
if not isinstance(bundles, list) or not all(isinstance(item, dict) for item in bundles):
raise ValueError(_("We could not read the filing types. Reload the review page and try again."))
query = urlencode({"initial": str(initial).lower(), "category_id": category, "type_id": case_type})
filing_names = resolve(f"filing_types/?{query}", [item.get("filing_type", "") for item in bundles])
return _unavailable_message(
availabilities,
case_category=category_name,
case_type=type_name,
filing_types=filing_names,
)


def unavailable_response(request, draft, message):
"""Keep the draft intact and offer corrections without suggesting a false court."""
case_step = (
WorkflowStepKey.CASE_LOOKUP
if draft.existing_case == ExistingCase.EXISTING
else WorkflowStepKey.EXTRACTION_REVIEW
)
context = {
"is_logged_in": True,
"draft": draft,
"availability_message": message,
"change_case_url": get_step_url(case_step, draft.jurisdiction),
"change_documents_url": get_step_url(WorkflowStepKey.ORGANIZE_DOCUMENTS, draft.jurisdiction),
}
context.update(get_workflow_context(draft.current_step, draft.jurisdiction, draft))
return render(request, "efile/filing_unavailable.html", context, status=403)
2 changes: 2 additions & 0 deletions efile_app/efile/services/submission_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class SubmissionErrorCode:
"""Machine-readable codes for errors returned before filing submission."""

FILING_UNAVAILABLE = "submission_filing_unavailable"
CASE_DATA_MISSING = "submission_case_data_missing"
UPLOAD_DATA_MISSING = "submission_upload_data_missing"
EFILE_DATA_MISSING = "submission_efile_data_missing"
Expand All @@ -15,6 +16,7 @@ class SubmissionErrorCode:

PRE_SUBMIT_ERROR_CODES = frozenset(
{
SubmissionErrorCode.FILING_UNAVAILABLE,
SubmissionErrorCode.CASE_DATA_MISSING,
SubmissionErrorCode.UPLOAD_DATA_MISSING,
SubmissionErrorCode.EFILE_DATA_MISSING,
Expand Down
9 changes: 9 additions & 0 deletions efile_app/efile/static/config/states/illinois.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,15 @@ court_specific_requirements:
requirement: always

"cook:cvd1": # Cook County Circuit Court - Municipal Civil Division
# Optional availability hook. Leave commented until affected names and
# hearing-scheduling requirements are confirmed against the target EFSP.
# filing_availability:
# rules:
# - case_types: ["Contract"] # Illustrative, not a live restriction.
# message: >-
# LITEFile cannot file this case type in Cook County yet because it
# requires scheduling a hearing. Our e-filing service does not support
# hearing scheduling yet. Contact the court clerk to ask how to file.
case_types:
eviction:
documents:
Expand Down
4 changes: 4 additions & 0 deletions efile_app/efile/static/css/reorganized-flow.css
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@
margin-top: 0.3rem;
}

.review-field {
min-width: 0;
}

.review-field>span {
color: var(--text-heading);
display: block;
Expand Down
5 changes: 5 additions & 0 deletions efile_app/efile/static/js/api-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class ApiUtils {
return String(value || "").replace(/ \(Recommended\)$/, "").replace(/ \*$/, "");
}

// The court's name for a select's choice, or "" when nothing is chosen.
selectedOptionText(select) {
return select.value ? this.cleanOptionText(select.selectedOptions[0]?.textContent) : "";
}

getCache() {
try {
const cached = localStorage.getItem('apiResponseCache');
Expand Down
12 changes: 11 additions & 1 deletion efile_app/efile/static/js/case-lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
const extractionHelp = document.getElementById("court-extraction-help");
const selectedCourtCode = JSON.parse(document.getElementById("selected-court-code").textContent || '""');

const availability = window.filingAvailability.mount({
form,
notice: document.getElementById("filing-availability-notice"),
selection: () => ({
jurisdiction: apiUtils.getCurrentJurisdiction(),
court: courtSelect.value
}),
});
courtSelect.addEventListener("change", () => availability.check(courtSelect.closest(".form-field")));

async function mountCourtSelector() {
const container = document.getElementById("court-selector");
if (!container || !window.courtSelector) return false;
Expand Down Expand Up @@ -112,5 +122,5 @@
}
});

loadCourts();
loadCourts().then(() => availability.check(courtSelect.closest(".form-field")));
})();
Loading
Loading