|
| 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)} |
0 commit comments