Skip to content

Commit f3b1d17

Browse files
Merge pull request #268 from SuffolkLITLab/feat/court-disclaimers
Show filing disclaimers before upload and require acceptance on review
2 parents d8bbd05 + 7320957 commit f3b1d17

21 files changed

Lines changed: 573 additions & 16 deletions
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Read court requirements and bind acceptance to the text shown on Review."""
2+
3+
from urllib.parse import quote
4+
5+
import requests
6+
from django.conf import settings
7+
from django.core import signing
8+
from django.utils import timezone
9+
10+
11+
class DisclaimerUnavailable(ValueError):
12+
pass
13+
14+
15+
def court_disclaimers(draft):
16+
if not draft.court_code:
17+
raise DisclaimerUnavailable("Choose a court to see its filing requirements.")
18+
url = (
19+
f"{settings.EFSP_URL}/jurisdictions/{quote(draft.jurisdiction, safe='')}/codes/courts/"
20+
f"{quote(draft.court_code, safe='')}/disclaimer_requirements"
21+
)
22+
try:
23+
response = requests.get(url, timeout=10)
24+
response.raise_for_status()
25+
rows = response.json()
26+
if not isinstance(rows, list):
27+
raise ValueError("Expected a list")
28+
requirements = []
29+
for row in rows:
30+
if not isinstance(row, dict) or not row.get("code") or not isinstance(row.get("requirementText"), str):
31+
raise ValueError("Invalid court requirement")
32+
if not row["requirementText"].strip():
33+
raise ValueError("Empty court requirement")
34+
requirements.append(
35+
{
36+
"code": str(row["code"]),
37+
"name": str(row.get("name") or ""),
38+
"text": row["requirementText"],
39+
"order": int(row.get("listorder") or 0),
40+
}
41+
)
42+
return sorted(requirements, key=lambda item: (item["order"], item["code"]))
43+
except (requests.RequestException, ValueError, TypeError) as error:
44+
raise DisclaimerUnavailable(
45+
"We could not load the court's filing requirements. Reload this page to try again."
46+
) from error
47+
48+
49+
def _agreement(draft, requirements):
50+
return {
51+
"draft": str(draft.pk),
52+
"jurisdiction": draft.jurisdiction,
53+
"court": draft.court_code,
54+
"requirements": requirements,
55+
}
56+
57+
58+
def disclaimer_context(draft):
59+
try:
60+
requirements = court_disclaimers(draft)
61+
except DisclaimerUnavailable as error:
62+
# An absent agreement token, not a credential.
63+
return {"disclaimer_error": str(error), "disclaimer_token": ""} # nosec B105
64+
return {
65+
"court_disclaimers": requirements,
66+
"disclaimer_token": signing.dumps(_agreement(draft, requirements), salt="court-disclaimers"),
67+
}
68+
69+
70+
def validate_acceptance(draft, payload):
71+
"""Recheck the current court text; a stale page cannot accept changed requirements."""
72+
requirements = court_disclaimers(draft)
73+
agreement = _agreement(draft, requirements)
74+
try:
75+
accepted = signing.loads(payload.get("disclaimer_token", ""), salt="court-disclaimers")
76+
except (signing.BadSignature, TypeError):
77+
accepted = None
78+
if payload.get("confirm_submission") is not True or accepted != agreement:
79+
raise ValueError("Review and accept the current court requirements. Reload the review page to continue.")
80+
return {**agreement, "accepted_at": timezone.now().isoformat(), "user_id": str(draft.user_id)}

‎efile_app/efile/static/config/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,3 +964,27 @@ creation API. User-row locking serializes concurrent creation requests. A failed
964964
account lookup never triggers creation. The payment form rechecks the selected
965965
account with Tyler before saving its ID, name, and type. Waiver selection does
966966
not grant a court waiver; the filer must provide the appropriate documents.
967+
968+
969+
## Notices before upload
970+
971+
Set `upload_disclaimers` at the top level of a state YAML file to show standard
972+
state notices in the “Before you upload your documents” accordion. Each item
973+
has a `name` and `text`; items appear in the configured order. Text supports sanitized paragraphs, emphasis, lists, and links,
974+
with line breaks preserved. Other tags and unsafe attributes are removed. Omit the setting or use `[]` to hide the accordion.
975+
976+
```yaml
977+
upload_disclaimers:
978+
- name: Personal identifying information
979+
text: |
980+
Add the state's standard notice here.
981+
```
982+
983+
These notices are available before court selection and do not call the court API,
984+
even when a filing already has a court. Record the source and retrieval date in
985+
YAML comments when updating the text. Illinois, Massachusetts, and Vermont initially use
986+
notices retrieved from the proxy code tables on September 29, 2026.
987+
988+
The upload screen does not record acceptance. Review and submission fetch the
989+
selected court's current requirements and require acceptance of that text.
990+
State notices do not replace or provide a fallback for those requirements.

‎efile_app/efile/static/config/states/illinois.yaml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,3 +865,16 @@ fee_waiver:
865865
max_fpl_percent: 200
866866
description: "You may get a partial reduction in fees above 125%. The court also looks at your assets."
867867
document_guidance: "Include your waiver form or order. An Illinois waiver order usually lasts 12 months. If it is from another court, you may need to show a copy."
868+
869+
# Standard statewide notices shown before court selection.
870+
# Source: https://efile-test.suffolklitlab.org/jurisdictions/illinois/codes/courts/adams/disclaimer_requirements
871+
# Retrieved 2026-09-29; also matched Cook, DuPage, Lake, and Will.
872+
# HTML formatting removed from the source text. Local notices remain on Review.
873+
upload_disclaimers:
874+
- name: Supreme Court Rule 15
875+
text: |-
876+
By submitting this acknowledgement, you represent that the document(s) you are filing comply with the following requirements:
877+
For all cases, Supreme Court Rule 15 requires you to redact Social Security Numbers (except the last 4 digits) in all documents and attachments or file them in conjunction with a “Notice of Confidential Information Within Court Filing” Form.
878+
- name: Supreme Court Rule 138
879+
text: |-
880+
For civil cases, Supreme Court Rule 138 requires you to redact Personal Identity Information (except the last 4 digits) in all documents and attachments or file them in conjunction with a "Notice of Confidential Information Within Court Filing" Form. Personal Identity Information includes Taxpayer Identification Numbers, Driver’s License Numbers, Financial Account Numbers, Debit/Credit Card Numbers, and any other information as provided by court order or local rule.

‎efile_app/efile/static/config/states/massachusetts.yaml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,15 @@ fee_waiver:
318318
waiver_percent: 100
319319
description: "Use your income after taxes. You may have fees waived."
320320
document_guidance: "Include an Affidavit of Indigency each time you file with a fee waiver."
321+
322+
# Standard notices shown before upload, before a court is selected.
323+
# Source: https://efile-test.suffolklitlab.org/jurisdictions/massachusetts/codes/courts/665/disclaimer_requirements
324+
# Retrieved 2026-09-29. Review still uses the selected court’s live requirements.
325+
upload_disclaimers:
326+
- name: Personal identifying information
327+
text: |-
328+
When filing a document in court, the filer must redact personal identifying information, under Supreme Judicial Court Rule 1:24, unless an exception in the rule applies. Unless restricted by a law, standing order, court rule or court order, most documents filed in and accepted by the court are public records and may be viewed or accessed by the public.
329+
For example, if a document requires a social security number, taxpayer identification number, driver’s license number, state-issued identification card number, passport number, financial account number, or credit or debit card number, the filer must omit, black out, or otherwise make unreadable all the numbers except the last four digits. Also, if the filer is required to include a parent’s birth surname, the filer must omit, black out, or otherwise make unreadable all the letters except the first letter. The court will not review filed documents to check that the filer redacted personal identifying information.
330+
- name: Service
331+
text: |-
332+
NOTE: Filers are advised that if they use E-File and Serve for initial case filings on opposing counsel’s agreement to accept service or otherwise, Filer still must perfect personal service on a defendant/respondent in the regular manner where such service is required by statute or rule.

‎efile_app/efile/static/config/states/vermont.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,14 @@ fee_waiver:
235235
description: "Use your income before taxes. You may have fees waived."
236236
# Repeat-filing and prior-order requirements have not yet been confirmed.
237237
document_guidance: "Include your waiver form or order. Ask the court if an earlier order covers this filing."
238+
239+
# Standard notices shown before upload, before a court is selected.
240+
# Source: https://efile-test.suffolklitlab.org/jurisdictions/vermont/codes/courts/sc:addison/disclaimer_requirements
241+
# Retrieved 2026-09-29. Review still uses the selected court’s live requirements.
242+
upload_disclaimers:
243+
- name: Certification agreement
244+
text: |-
245+
I certify that I have reviewed the contents of this filing, and that the filing specifies any nonpublic record or contents, and protects those records from disclosure to the public consistent with the Rules for Public Access to Court Records. If any record or content was separated or redacted and filed as confidential, the actions taken to comply with the rules are stated in the “Comments to Court” field.
246+
- name: Service agreement
247+
text: |-
248+
I certify that I am complying with service requirements in V.R.E.F. 11(g) and V.R.C.P. 5(b). (1) I am serving all persons on whom service is required through File and Serve, or (2) I am serving the persons on whom service is required through File and Serve and submitting a certificate of service that describes service on all other persons required to be served by other means, or (3) I am not required to serve my filing on any person.

‎efile_app/efile/static/js/review.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const FilingHandler = {
3131
// button waits until the quote on the page is the current one.
3232
document.getElementById("submitButton").disabled = submitting ||
3333
!document.getElementById("confirm-filing").checked ||
34+
document.getElementById("confirm-filing").disabled ||
3435
!USABLE_FEE_STATES.includes(this.feeQuoteState);
3536
},
3637

@@ -136,7 +137,7 @@ const FilingHandler = {
136137
},
137138

138139
async submitFiling() {
139-
if (!document.getElementById("confirm-filing").checked) {
140+
if (!document.getElementById("confirm-filing").checked || document.getElementById("confirm-filing").disabled) {
140141
Messages.showError(gettext("Confirm that you reviewed the filing before you submit."));
141142
return;
142143
}
@@ -146,6 +147,7 @@ const FilingHandler = {
146147
const efileData = this.buildCurrentFilingData();
147148
const result = await apiUtils.post("/api/submit-final-filing/", {
148149
efile_data: efileData,
150+
disclaimer_token: reviewJSON("disclaimer-token"),
149151
confirm_submission: true,
150152
payment_account_id: reviewJSON("payment-account-id")
151153
}, {}, {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% load disclaimer_text %}
2+
{% for requirement in court_disclaimers %}
3+
<div class="mb-3">
4+
{% if requirement.name %}<h3 class="h6">{{ requirement.name }}</h3>{% endif %}
5+
<div>{{ requirement.text|disclaimer_html }}</div>
6+
</div>
7+
{% endfor %}

‎efile_app/efile/templates/efile/review.html‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,27 @@ <h2>{% translate "Payment" %}</h2>
288288
hidden>
289289
<i class="fas fa-check-circle me-2" aria-hidden="true"></i><span id="successText"></span>
290290
</div>
291+
<section class="mt-4" aria-labelledby="court-requirements-heading">
292+
<h2 id="court-requirements-heading">{% translate "Before you submit your filing" %}</h2>
293+
{% if disclaimer_error %}
294+
<p class="alert alert-danger" role="alert">{{ disclaimer_error }}</p>
295+
{% else %}
296+
{% include "efile/partials/court_disclaimers.html" %}
297+
{% endif %}
298+
</section>
291299
<div class="review-attestation">
292300
<label>
293-
<input class="form-check-input" type="checkbox" id="confirm-filing" />
294-
<span>{% translate "I reviewed this filing and am ready to submit it to the court." %}</span>
301+
<input class="form-check-input"
302+
type="checkbox"
303+
id="confirm-filing"
304+
{% if disclaimer_error %}disabled{% endif %} />
305+
<span>
306+
{% if court_disclaimers %}
307+
{% translate "I have read and accept the court requirements above. I reviewed this filing and am ready to submit it to the court." %}
308+
{% else %}
309+
{% translate "I reviewed this filing and am ready to submit it to the court." %}
310+
{% endif %}
311+
</span>
295312
</label>
296313
</div>
297314
<div id="loadingSpinner" class="loading-spinner" aria-live="polite">
@@ -314,6 +331,7 @@ <h2>{% translate "Payment" %}</h2>
314331
</button>
315332
</div>
316333
</section>
334+
{{ disclaimer_token|json_script:"disclaimer-token" }}
317335
{{ case_data|json_script:"case-data" }}
318336
{{ upload_data|json_script:"upload-data" }}
319337
{{ draft.selected_payment_account_id|json_script:"payment-account-id" }}

‎efile_app/efile/templates/efile/upload_documents.html‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ <h1>{% translate "Upload your court documents" %}</h1>
2525
aria-label="{% translate "Change whether this is a new or existing case" %}">{% translate "Change" %}</a>
2626
</p>
2727
{% endif %}
28+
{% if upload_disclaimers %}
29+
<details class="border rounded p-3 mb-3">
30+
<summary>{% translate "Before you upload your documents" %}</summary>
31+
<div class="mt-3">
32+
{% include "efile/partials/court_disclaimers.html" with court_disclaimers=upload_disclaimers only %}
33+
<p>{% translate "These are standard notices for filing in this state. Before you submit, you will review and accept the requirements for your court." %}</p>
34+
</div>
35+
</details>
36+
{% endif %}
2837
<div class="requirements-strip">
2938
<span><i class="fa-solid fa-file-pdf" aria-hidden="true"></i> {% translate "PDF only" %}</span>
3039
<span><i class="fa-solid fa-weight-hanging" aria-hidden="true"></i> {% translate "10 MB per file" %}</span>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Display court-authored notices with a small, sanitized HTML vocabulary."""
2+
3+
import re
4+
5+
import bleach
6+
from django import template
7+
from django.utils.safestring import SafeString
8+
9+
register = template.Library()
10+
11+
12+
@register.filter
13+
def disclaimer_html(value):
14+
# Some proxy code tables contain literal backslash escapes inside the JSON
15+
# string, including quotes around link attributes and paragraph separators.
16+
text = str(value or "").replace('\\"', '"').replace("\\n", "\n")
17+
cleaned = bleach.clean(
18+
text,
19+
tags={"p", "br", "strong", "b", "em", "i", "ul", "ol", "li", "a"},
20+
attributes={"a": ["href", "title"]},
21+
protocols={"https", "http", "mailto"},
22+
strip=True,
23+
)
24+
# Drop spacer paragraphs and redundant breaks; site CSS owns block spacing.
25+
whitespace = r"(?:\s|&nbsp;|&#0*160;|&#x0*a0;)"
26+
cleaned = re.sub(rf"<p>(?:{whitespace}|<br>)*</p>", "", cleaned, flags=re.IGNORECASE)
27+
cleaned = cleaned.replace("\n", "<br>")
28+
cleaned = re.sub(r"(?:<br>\s*)+", "<br>", cleaned)
29+
blocks = r"</?(?:p|ul|ol|li)>"
30+
cleaned = re.sub(rf"(<br>\s*)+(?={blocks})", "", cleaned)
31+
cleaned = re.sub(rf"({blocks})(?:\s*<br>)+", r"\1", cleaned)
32+
cleaned = re.sub(rf"\A(?:{whitespace}|<br>)+|(?:{whitespace}|<br>)+\Z", "", cleaned)
33+
# All external markup passed through the allowlist sanitizer above.
34+
return SafeString(cleaned) # nosec B703

0 commit comments

Comments
 (0)