Include the org in CASA case URLs and finish the switch away from database ids
维护者通常 18 天内回复
@A-RYAN-1 已经在做这个了。
开始于 2026年10月1日。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 32/100
调研方向
先从 config/routes.rb、app/models/casa_case.rb 和 app/models/casa_org.rb 开始,然后审查列出的未加 scope 的查询,从 copy_court_orders 开始。选择组织标识符之前,检查现有的 case-path 调用点和 request specs。完成要求包括组织级 scope 的 URL、安全的 legacy 重定向、回填的 slug,以及跨组织访问测试。
由索引模型根据 Issue 内容生成。
描述
What type(s) of user does this feature affect?
All signed-in user types — volunteers, supervisors, and admins all navigate to CASA case pages. All casa admins are affected indirectly (they administer orgs, whose identifier would become part of the URL).
Description
CASA case URLs should identify a case the way humans and courts do — by organization + case number — instead of leaning on our internal database primary key.
Important: half of this is already built. CasaCase already uses FriendlyId, so today's URLs are usually already case-number based, not integer based:
app/models/casa_case.rb:26—friendly_id :case_number, use: :scoped, scope: :casa_org- FriendlyId's
to_paramreturns the slug when present (friendly_id-5.7.0/lib/friendly_id/base.rb:262), socasa_case_path(casa_case)renders/casa_cases/cina-21-1234 - Nine call sites already resolve slugs via
CasaCase.friendly.find(casa_cases_controller.rb:219,court_dates_controller.rb:69,placements_controller.rb:59,emancipations_controller.rb:14,41,fund_requests_controller.rb:28,case_assignments_controller.rb:88,volunteers_controller.rb:91,case_court_report_context.rb:9)
So the work here is (a) adding the organization to the path, (b) closing the gaps where the slug scheme is currently unsafe or unfinished, and (c) doing it without breaking links people have already bookmarked or received by email.
Why add the org to the path
The case slug is only unique within an org, by design:
index_casa_cases_on_case_number_and_casa_org_id (case_number, casa_org_id) UNIQUE
index_casa_cases_on_slug (slug) <-- NOT unique
Two orgs can both have case CINA-21-1234. Today, /casa_cases/cina-21-1234 is therefore globally ambiguous, and resolving it correctly depends entirely on every single lookup remembering to scope to the current org. Some do:
# app/controllers/casa_cases_controller.rb:219 — correct
@casa_case = current_organization.casa_cases.friendly.find(params[:id])
Several don't:
| Unscoped lookup | Note |
|---|---|
app/controllers/fund_requests_controller.rb:28 |
Checks casa_org == current_user.casa_org after the find, so a duplicate slug in another org can bounce a legitimate user to root_path instead of their own case |
app/controllers/emancipations_controller.rb:14,41 |
Pundit authorize blocks a cross-org leak, but a duplicate slug yields a wrong 403 on a case the user does own |
app/controllers/case_assignments_controller.rb:88 |
case_assignment_parent — authorization of the resolved parent needs auditing |
app/controllers/volunteers_controller.rb:91 |
Used to pick a redirect target |
app/controllers/casa_cases_controller.rb:146 |
CasaCase.find_by_case_number(params[:case_number_cp]) in copy_court_orders — not org-scoped at all; please audit this one first, since it reads another record's court orders |
Putting the org in the path makes the ambiguity structurally impossible instead of a convention every future contributor has to remember.
Proposed URL shape
/org/:casa_org_id/casa_cases/:case_number
/org/prince-george-casa/casa_cases/cina-21-1234
/org/prince-george-casa/casa_cases/cina-21-1234/emancipation
Recommended over a composite single segment (e.g. /casa_cases/prince-george-casa--cina-21-1234) because it's conventional Rails routing, the nested children under resources :casa_cases (config/routes.rb:46-63) keep working unchanged, and the org is readable in the address bar.
Open decision — what identifies the org? CasaOrg has a slug column with a unique DB index, but it is not managed by FriendlyId and has sharp edges (app/models/casa_org.rb:11,88):
before_create :set_slug # create only -- renaming an org leaves the old slug
def set_slug
self.slug = name.parameterize # no model-level uniqueness validation
end
Two org names that parameterize identically would hit the DB unique index and 500 on org creation. Note the existing TODOs anticipating this work: app/models/casa_org.rb:137 and app/controllers/all_casa_admins/casa_admins_controller.rb:62. Options: use the numeric org id (ugly but safe), or harden the org slug (add a uniqueness validation, a collision suffix, and a decision about renames) before it becomes load-bearing. Worth a maintainer call before step 2 below.
Suggested safe rollout — four deploys
Each step is independently shippable and leaves the app working.
Step 1 — harden what exists (no user-visible change).
- Backfill missing case slugs. The original backfill was gutted and is now a no-op:
lib/tasks/deployment/20210925143244_populate_slugs_for_orgs_and_cases.rakeprints "task deleted because it uses the now-uncallable method set_slug". Any row that predates FriendlyId and was never re-saved still hasslug IS NULLand silently falls back to its integer id in URLs. Write a fresh After Party task and confirmCasaCase.where(slug: nil).count == 0(and the same forCasaOrg) before proceeding. - Make every case lookup org-scoped (the table above) and add request specs proving a slug that exists in two orgs resolves to the caller's own case.
- Decide and implement the org identifier hardening.
Step 2 — add the new routes alongside the old ones.
- Add the nested
/org/:casa_org_id/...routes; keep the existing flat routes. Nothing generates the new shape yet, so this is inert but testable. - Consider adding FriendlyId's
:historymodule (use: [:scoped, :history], plus thefriendly_id_slugstable) in this step. Todayshould_generate_new_friendly_id?(app/models/casa_case.rb:226) regenerates the slug whenevercase_numberchanges, so renaming a case immediately 404s every existing link to it — including links already sitting in supervisors' inboxes. History keeps old slugs resolving.
Step 3 — flip URL generation.
- Make the helpers emit the new shape, and fix the call sites that pass a raw integer id, since those break the moment the route stops accepting ids:
app/views/volunteers/index.html.erb:101—casa_case_path(volunteer.most_recent_attempt_case_id), where the id comes from a datatable SQL alias (app/datatables/volunteer_datatable.rb:71) rather than a loaded recordapp/notifications/youth_birthday_notifier.rb:26—casa_case_path(params[:casa_case].id)app/javascript/controllers/copy_court_orders_controller.js:32— string-builds/casa_cases/${this.casaCaseIdValue}/copy_court_ordersapp/javascript/src/case_emancipation.js:103— path regexcasa_cases\/[A-Za-z\-0-9]+\/emancipationneeds to match the nested shape
- Keep the flat routes as redirects to the new shape. Derive the org from the record, not from
current_user, so a cross-org guess still fails authorization rather than silently redirecting into someone else's org. - Scope check: ~62
casa_case_path/casa_case_url/casa_cases_pathcall sites inapp/, ~37 spec files referencing case paths.
Step 4 — retire the old shape.
- Watch logs for traffic on the legacy routes for at least one full release cycle. Old URLs live in already-delivered email (
app/views/supervisor_mailer/_active_volunteer_info.html.erb:3embedscasa_case_url(casa_case)), so they will keep arriving for a while. - Then remove the legacy routes, and — if we actually want "no more integer ids in URLs" to be enforced — explicitly reject numeric
:idparams, with a spec.friendly.findaccepts a numeric id indefinitely otherwise; that fallback is exactly what makes steps 1-3 safe, but it also means the migration is never "done" until we opt out of it deliberately.
Notes / gotchas for whoever picks this up
- Never build a case URL from
case_numberby hand. When two case numbers in one org parameterize to the same string, FriendlyId appends a uniquifying suffix, soslug != case_number.parameterize. Always go throughto_param/ the path helpers. - Changing
to_paramhas wide reach:redirect_to @casa_case,form_with model: @casa_case, andlink_to case.case_number, caseall shift at once. - There is already precedent for case-number-in-URL elsewhere:
app/controllers/case_court_reports_controller.rb:76looks upCasaCase.find_by(case_number: params[:id], casa_org: current_user.casa_org). Worth folding into the same convention so we don't end up with two case-identifying URL styles. - Multi-tenancy is the highest-severity bug class in this app (see
CLAUDE.md), and this change touches exactly that surface. Every step should carry request specs that assert a case from org A is unreachable while signed in to org B. spec/helpers/sidebar_helper_spec.rb:62hardcodes/casa_cases/some-case-slug/emancipation; path-shape assertions like this will need updating.
How to access the QA site
Login Details:
Link to QA site
Login Emails:
- [email protected] view site as a volunteer
- [email protected] view site as a supervisor
- [email protected] view site as an admin
- [email protected] view site as an all casa admin
- go to
/all_casa_admins/sign_in
- go to
password for all users: 12345678
- 主要语言
- Ruby
- 星标
- 382
- 派生
- 546
- 平均合并
- 12 天 12 小时
- 30 天内合并 PR
- 17
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 提供 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
rubyforgood/casa 的其他 Issue
-
Bug: Reassigned cases are listed under "Not Assigned" in the Assign a new case dropdown可能已有人在做 @Anandb71 于 6 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
rubyforgood/casa#7241 · 2 条评论 ·
维护者通常 18 天内回复
-
Help Wanted Type: Bug
难度 4/5 1-2 天 新手友好度 35/100
rubyforgood/casa#7243 ·
维护者通常 18 天内回复
-
难度 3/5 1-2 天 新手友好度 35/100
rubyforgood/casa#7227 ·
维护者通常 18 天内回复
-
难度 4/5 3-5 天 新手友好度 35/100
rubyforgood/casa#7218 ·
维护者通常 18 天内回复
-
Speed up the RSpec suite: parallelize CI, then shrink the JS system-spec slice可能重新可做 @Sufiyan-MSA 于 46 天前认领,目前没有进行中的 PR。 未关闭📈 Performance Improvement 🧪 Tests
难度 5/5 一周以上 新手友好度 35/100
rubyforgood/casa#7173 · 3 条评论 ·
维护者通常 18 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 62/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 76/100
voxpupuli/puppet-archive#566 ·
-
area/tech debt
难度 2/5 1-3 小时 新手友好度 77/100
维护者通常 2 天内回复
-
verified
难度 2/5 1-3 小时 新手友好度 67/100
维护者通常 5 天内回复
-
难度 1/5 1 小时以内 新手友好度 86/100
akitaonrails/ai-book-2026#10 ·