Skip to content

Enhance accessibility and security with ARIA labels and rate limiting#620

Merged
pboachie merged 76 commits intomainfrom
staging
Apr 9, 2026
Merged

Enhance accessibility and security with ARIA labels and rate limiting#620
pboachie merged 76 commits intomainfrom
staging

Conversation

@pboachie
Copy link
Copy Markdown
Collaborator

@pboachie pboachie commented Apr 9, 2026

This pull request introduces several important enhancements and fixes across accessibility, security, admin tooling, and evaluation export governance. The most significant changes include new accessibility guidelines, improved rate limiting for invite endpoints, expanded admin setup automation, and the introduction of a new evaluation export permission for users. Additionally, supporting documentation and review processes have been updated to reflect these changes.

Accessibility Improvements:

  • Added guidelines to wrap decorative HTML entities in <span aria-hidden="true"> for better screen reader support, and clarified the use of contextual aria-labels for action buttons in lists to improve accessibility for users with assistive technology (.Jules/palette.md, .jules/palette.md) [1] [2].

Security Enhancements:

  • Added rate limiting to the invite_accept endpoint to prevent brute-force and DoS attacks, including the necessary import and decorator application (apps/auth_app/invite_views.py, .jules/sentinel.md) [1] [2] [3].

Evaluation Export Governance:

  • Introduced a new evaluation_export_granted Boolean field to the User model, with corresponding admin UI and migration, to support permission-gated de-identified evaluation exports (apps/auth_app/migrations/0011_user_evaluation_export_granted.py, apps/auth_app/admin.py) [1] [2].
  • Added demo seeding logic and TODO tasks for evaluation export governance, including audit and documentation requirements (apps/admin_settings/management/commands/seed.py, TODO.md) [1] [2] [3] [4].

Admin Tooling and Automation:

  • Enhanced apply_setup.py to support updating and configuring metrics and custom fields, including updating existing records when config changes, and improved logging/return values (apps/admin_settings/management/commands/apply_setup.py) [1] [2].
  • Updated demo data health check to use the correct program status field (apps/admin_settings/checks.py).

Documentation and Review Process:

  • Added and updated documentation for code review rules, design rationale, and new evaluation export features (REVIEW.md, CLAUDE.md, TODO.md) [1] [2] [3] [4].

These changes collectively improve the system's accessibility, security, governance, and maintainability.

pboachie and others added 30 commits March 24, 2026 03:00
Wrapped the decorative &times; entity in the onboarding banner
close button inside an element with aria-hidden="true". This
prevents screen readers from announcing "multiplication sign"
or "times" redundantly alongside the existing aria-label.

Also recorded this learning in .Jules/palette.md for future reference.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Added `@ratelimit(key="ip", rate="10/m", method=["GET", "POST"], block=True)` decorator to `invite_accept` to prevent brute force and DoS attacks.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Added dynamic, descriptive `aria-label`s to the "Approve" and "Dismiss" buttons within the pending surveys table (`templates/surveys/client_surveys.html`).

Previously, screen readers iterating through the table read "Approve, button" and "Dismiss, button" consecutively without indicating which survey the action applied to.

Using Django's `blocktrans` block, the labels now include the corresponding survey name directly (e.g. "Approve Initial Intake Survey"), providing clarity and fulfilling WCAG guidelines on providing context for interactive elements.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…you.

I implemented a global listener in `static/js/app.js` to automatically set `aria-busy="true"` and temporarily disable pointer events on form buttons when they are clicked. This ensures users get immediate visual feedback (a loading spinner provided by Pico CSS) and prevents duplicate requests.

Additionally, I included a safety check for `event.defaultPrevented` to avoid breaking forms that handle their own client-side validation logic, along with an event listener on `pageshow` to remove the loading state if the user navigates back to the page using the browser's Back/Forward Cache (BFCache).

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Add `content: "\1F4CB" / "";` to `.empty-state::before` so screen readers ignore the decorative clipboard icon instead of announcing "clipboard".
- Added a fallback `content: "\1F4CB";` declaration for older browsers.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…aries

🚨 Severity: HIGH
💡 Vulnerability: Missing rate limiting on `accept_invite` and `staff_assisted_login` (apps/portal/views.py) and `public_registration_form` (apps/registration/views.py).
🎯 Impact: This exposes the application to brute-force or Denial-of-Service (DoS) attacks on authentication-related endpoints that consume one-time tokens or handle user registrations.
🔧 Fix: Added `@ratelimit` decorator with `block=True` to the affected endpoints to enforce rate limits and protect against abuse.
✅ Verification: Ran `python -m pytest tests/test_security.py` successfully.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
**Severity:** HIGH
**Vulnerability:** The `accept_invite` and `staff_assisted_login` endpoints in `apps/portal/views.py` handle authentication logic and token validation but were missing rate limiting.
**Impact:** Attackers could attempt brute-force or Denial-of-Service (DoS) attacks on these sensitive endpoints.
**Fix:** Added the `@ratelimit(key="ip", rate="10/m", method=["GET", "POST"], block=True)` decorator to both functions, standardizing them with other authentication boundaries.
**Verification:** Ran `python -m pytest tests/test_security.py` to ensure existing behavior is preserved and syntax compiled properly.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…eview

Reviewed the old OpenWebUI transcript structuring prompt that preceded
KoNote's qualitative analysis features. Three ideas worth preserving:
data quality transparency panel, language-aware quote collection, and
graduated confidence on theme auto-links.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DQ1 — Data quality transparency (practice signal):
- Enhanced summary bar with month count for temporal context
- Added contextual sentence above Participant Voice section when
  voice coverage is below 30% ("Participant reflections were recorded
  in X of Y sessions")
- Added raw counts (with_voice, total_full) to practice_health dict

CONF1 — Raised auto-link threshold from 2 to 3 words:
- Reduces false positives in theme auto-linking without creating
  a review queue for program managers

LANG1 — Language-aware quote collection (Phase 1):
- Added detect_language() heuristic (FR/EN) using French function
  words — no external dependencies
- Each quote now carries a "lang" field from collect_quotes()
- AI prompt in generate_outcome_insights() includes language
  distribution and instructions to analyse French quotes in French
- Templates show FR pill on quotes only when mixed-language content
  is detected (invisible for monolingual programs)
- Added lang="fr" attribute to French blockquotes for accessibility

Tests: 31 pass (6 new — language detection + 2-word rejection)
French translations added for new blocktrans strings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The contextual sentence about participant reflections was rendered
twice — the second copy also used a different blocktrans variable
name (voice vs with_voice), so it wouldn't match the French .po entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove redundant re.IGNORECASE from _WORD_SPLIT_RE (text is already
  lowered before matching)
- Fix double blank line in AI prompt when lang_line is non-empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…point

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Added rate limiting to the `accept_invite` and `staff_assisted_login`
views in `apps/portal/views.py`. These endpoints handle authentication
and registration actions using one-time tokens, making them sensitive
to brute-force and DoS attacks. Applying `@ratelimit` with `block=True`
ensures they are protected consistently with other auth endpoints in
the application.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Wrap decorative HTML entities (&times;, &larr;, &rarr;) in `<span aria-hidden="true">` across templates so that screen readers ignore them, improving accessibility.

Carefully places the spans OUTSIDE of Django `{% trans %}` and `{% blocktrans %}` translation tags to avoid invalidating existing translations or causing template syntax errors.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…ata export

New export tier for external program evaluators: de-identified participant-level
CSV with k-anonymity enforcement, pseudonymous IDs, and generalised demographics.
Designed through expert panel (evaluator, privacy, nonprofit PM, health informatics).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a-export-drr

Add DRR for de-identified evaluation microdata export
Session kickoff prompt for implementing EVAL-EXPORT1 (de-identified
microdata export). Added to Parking Lot: Ready to Build in TODO.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…session-prompt

Add session prompt and TODO for evaluation microdata export
…microdata exports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10-step pipeline: extract → decrypt → consent filter → strip PII →
generalise QIs → k-anonymity → resolve violations → population gate →
generate CSV → suppression report. Enforces k=5 anonymity, population
thresholds, and 15% suppression ceiling per DRR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two-step flow: form → preview (shows pipeline results, population
summary, generalisations, suppression) → generate (creates
SecureExportLink, audit log, encrypted linkage). View enforces
can_create_evaluation_export() permission check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add evaluation_microdata to SecureExportLink.EXPORT_TYPE_CHOICES.
Add permission-gated nav link in Reports dropdown. Linkage key stored
in filters_json (no migration needed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admins can mark non-sensitive field groups as available for evaluation
export QI columns. Validation blocks marking groups with sensitive
fields. Shown in Django admin list and agency admin form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bulk-fetch ProgressNote stats, MetricValues, and ClientDetailValues
  in Step 2 (was ~2800 queries for 200 clients, now ~4)
- Remove unused _suppressed_records list and SMALL_CELL_THRESHOLD import
- Extract _active_records property to replace 7 repeated list filters
- Move inline `import re` to module level
- Use ProgramSelectionMixin for clean_program() RBAC validation
- Remove duplicate audit log entry (pipeline already logs the export)
- Use settings.SECURE_EXPORT_DIR directly (no fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add clean_program override to reject __all__ sentinel (crash fix)
- Fix broken blocktrans with inline filter in preview template
- Filter pipeline custom fields by is_evaluation_exportable (data leak fix)
- Remove dead duplicate ProgressNote query
- Remove unused imports (field, PlanTarget)
- Fix stale link object — refresh_from_db + single update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hand-written migration 0043 for CustomFieldGroup.is_evaluation_exportable
  (makemigrations cannot run locally — no Django env)
- Add metric slug collision detection with warning log in Step 1
- Deduplicate metric slugs with _2, _3 suffix in bulk metric fetch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a-export

Add de-identified evaluation microdata export
gilliankerr and others added 13 commits April 8, 2026 16:26
Gives Claude Code Review KoNote-specific rules to check:
PHIPA consent, encrypted fields, form validation, template
conventions, accessibility, translations, and design rationale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused namedtuple import
- Fix name uniqueness check: was querying encrypted field values
  and comparing to plaintext tuples (would never match). Replaced
  with a within-batch uniqueness tracker
- Make consent flag assignment deterministic: use record_id order
  instead of random so repeated runs produce consistent results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ixes

Fix review issues in eval export seed command
Session review panel flagged that the boolean field should clearly
indicate it's a stopgap, with a pointer to the full grant model
design in tasks/eval-export-governance.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ment

Add TODO comment on evaluation_export_granted field
Differentiates from standard reports in the menu. Makes it clear
this export is for an external evaluator and contains sensitive
de-identified data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename Evaluation Export to Evaluator Export (Confidential)
…8-162435

Add REVIEW.md for Claude Code Review
…o seed

Closes the admin bypass in the Evaluator Export (Confidential) flow and
restores the governance model from tasks/eval-export-governance.md: the
report.evaluation_export permission is DENY for all roles by default and
must be explicitly granted per-user. Admins are the granters, not the
operators, so they no longer auto-hold the permission.

Fixes:

- apps/reports/utils.py: remove is_admin bypass in
  can_create_evaluation_export so an admin without an explicit grant
  gets 403 at the view layer (previously 200).
- templates/base.html: remove `or user.is_admin` from the Evaluator
  Export nav visibility so the link no longer appears for admins unless
  they have been explicitly granted the permission.
- templates/base.html: add missing "Team Members" link to the admin
  dropdown (admin_users:user_list). Previously the admin menu only had
  "User Invites", giving admins no way to view or edit existing users.
- apps/auth_app/permissions.py: update stale comment on the four DENY
  entries to reflect that admins are NOT auto-granted.

Demo seed:

- apps/reports/management/commands/seed_eval_export_demo.py: extend the
  permission grant to cover Casey Worker (demo-worker-1, PM in the
  Supported Employment program the demo export is wired up against) in
  addition to Morgan Manager and Eva Executive.
- apps/admin_settings/management/commands/seed.py: wire
  seed_eval_export_demo into the main seed orchestrator so the full
  Evaluator Export demo (extra participants, demographics, consent
  flags, discharges, plans, notes, permission grants) runs automatically
  on container startup when DEMO_MODE is on. Non-fatal on failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…pass

Enforce per-user grant for Evaluator Export, add Team Members link, wire demo seed
…mmary, and improve UI elements

- Updated metrics application to support configuration of cadence sessions.
- Enhanced custom fields management with updates for existing fields.
- Introduced financial coaching summary feature with toggle.
- Improved UI for survey progress and timeline events.
- Added action steps to plan targets and updated related forms.
- Enhanced CSS for better visual differentiation of goals and timeline events.
Enhance metrics and custom fields handling, add financial coaching summary, and improve UI elements
Enhance accessibility and security with ARIA labels and rate limiting
@pboachie pboachie self-assigned this Apr 9, 2026
gilliankerr and others added 15 commits April 9, 2026 11:41
.Jules/ was added to .gitignore (line 37) but palette.md and sentinel.md
were already tracked, so git kept watching them. Removing from the index
stops tracking without deleting the files from disk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…9-112619

Untrack .Jules/ files so .gitignore rule takes effect
Follow-up to 546cda4 cleaning up findings from /simplify review.

- apps/reports/utils.py: delete the dead role-loop fallback in
  can_create_evaluation_export. All four roles have DENY for this
  permission in apps/auth_app/permissions.py, so the loop could never
  return True — and worse, it would have silently bypassed the per-user
  governance if a role were ever flipped to ALLOW. The function is now
  a one-line attribute read, matching the template tag's fast path.
  Also collapses the 8-line docstring that was narrating the governance
  doc.
- apps/auth_app/templatetags/permissions_tags.py: add a comment noting
  that the per-user evaluation_export_granted check mirrors the helper
  in reports.utils, so future changes keep both sites in sync.
- apps/reports/management/commands/seed_eval_export_demo.py: add a
  fast-path short-circuit at the top of _run(). The seed runs on every
  container startup (wired via the main seed orchestrator) and
  previously did ~1-5s of read + unconditional-write DB work even when
  the demo was already fully seeded. Two cheap indexed queries now
  let us skip the whole sweep when enrolments are at target and all
  grantees already hold the permission. Hoists the grantee list to a
  module-level EVAL_EXPORT_GRANTEES constant shared with _grant_permission
  so the fast-path check and the grant step can't drift.
- apps/admin_settings/management/commands/seed.py: drop the 5-line
  "Step 3" narration comment. The method name self-documents and the
  comment just restated the sub-command's docstring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…pass

Simplify eval export check and short-circuit demo seed
Follow-up to #617 / #622 addressing the "Fix soon" items from the
/review-session expert panel.

1. tests/test_export_permissions.py: add EvaluatorExportPermissionTest
   with three tests against /reports/evaluation-export/:

   - admin without the per-user grant → 403 (regression guard for the
     is_admin bypass that PRs #617 / #622 removed; if anyone ever adds
     `or user.is_admin` back to can_create_evaluation_export or the nav
     check, this test fails)
   - non-admin with evaluation_export_granted=True → 200
   - anonymous → 302 login redirect (confirms @login_required runs
     before the permission check so the 403 path isn't reachable
     without a session)

   Uses the existing test_export_permissions.py fixture style (direct
   User.create_user with TEST_KEY Fernet, Client().force_login, no PM
   role needed because the permission is per-user not per-role).

2. apps/reports/management/commands/seed_eval_export_demo.py: extend
   the fast-path comment block noting that the short-circuit's
   correctness depends on handle() wrapping _run() in
   transaction.atomic() — so a future refactor that relaxes atomicity
   must re-audit the check or risk skipping a partially-seeded state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Splits EvaluatorExportPermissionTest into two layers:
- 3 helper-level unit tests of can_create_evaluation_export (pure
  function, no view/template rendering — robust against unrelated
  template context issues): admin without grant → False, granted
  non-admin → True, plain user without flag → False.
- 2 view-level tests: admin without grant → 403 (the actual
  regression guard), anonymous → 302 login redirect.

Removed the earlier "granted user → 200" view test: that path renders
the full evaluation_export.html template which pulls in terminology
and feature context processors — a single-purpose permission test
shouldn't be fragile to unrelated template changes. The helper unit
test covers the positive path more cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…egression

Add regression test and atomicity comment for Evaluator Export
Detailed prompt for a future session to build the admin UI for granting
report.evaluation_export (with mandatory reason field, audit trail, and
revoke flow). Covers: data model, signal-based cache sync, form
validation, view + URL + template scaffolding, demo seed update, test
expectations, acceptance criteria, and a list of explicit anti-patterns
from the governance doc.

Closes the policy hole flagged by the /review-session expert panel:
the bug-fix PRs (#617, #622, #623) closed the technical hole (admin
bypass + regression tests) but without EVAL-GOV1 the governance model
is only half-enforced because there is still no reason field on grants
and the only way to grant is via Django admin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Recently Done: add EVAL-GOV-BYPASS1 entry bundling PRs #617, #622,
  #623, #624 (governance fix + simplify + regression test + EVAL-GOV1
  prompt).
- EVAL-GOV1: add pointer to tasks/phase-eval-gov1-prompt.md so the
  next session doesn't have to re-derive the implementation plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ss-done

TODO: record Evaluator Export bypass fix and link EVAL-GOV1 prompt
…xport to perm map

ProgressNote.created_at uses auto_now_add=True which ignores values
passed in create(). Now that today (2026-04-09) is past the hardcoded
fiscal year end (2026-03-31), notes were invisible to date-filtered
queries. Switch to backdate which the filter already checks first.

Also adds the missing report.evaluation_export entry to PERMISSION_URL_MAP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd-perm-map

fix: test date filters and perm map entry
chore: promote develop to staging
@pboachie pboachie merged commit e5a6cc7 into main Apr 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants