Consolidate event roster/breakdown data sources into a population-parameterized service
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
调研方向
从 app/services/event_dashboard.rb、app/services/attendees_roster.rb 和 app/services/attendees_breakdowns.rb 开始,然后跟踪 EventsController#roster 和 #attendees,直到共享的 roster 和 breakdown partial。修改行为前,先运行按事件的 Roster 和跨事件的 Attendees request/view spec。完成意味着删除重复的 service、两个 context 的渲染结果完全一致,并且新的 population、roster 和 breakdown service 共享一个实现。
由索引模型根据 Issue 内容生成。
描述
🤖 From Claude: filed at the maintainer's request as a follow-up to the events-IA refactor (PR #2113).
Summary
The per-event Roster page and the cross-event Attendees index render the same shared partials (app/views/events/_registrant_roster.html.erb, app/views/events/_registrant_breakdowns.html.erb) but are fed by two different data sources that each re-implement the same roster/breakdown method surface over a different population:
EventDashboard— one event's active registrants (registered + attended + incomplete_attendance + transferred_in), anchored to the event date.AttendeesRoster/AttendeesBreakdowns— the cross-event population of people who attended a facilitator training, anchored to "now".
This is the deferred "Phase 4" of the IA work. The goal is to collapse the duplicated roster/breakdown logic behind a single population-parameterized service so there's one implementation, differing only in which people, via which registrations, as of when.
Why
- Duplication / drift risk.
AttendeesRoster/AttendeesBreakdownswere deliberately written to mirrorEventDashboard's method names and return shapes so the shared partials read either one. Any change to a breakdown (a new dimension, a bug fix, a query optimization) must be made in two places or they silently diverge. The recent affiliations-N+1 fix, for example, had to be applied toEventDashboardand bothAttendees*services separately. EventDashboardis a god object (~1,300 lines, ~120 public methods) doing money, attendance, scholarships, roster maps, and breakdown datasets. The roster/breakdown concern is cleanly separable.- The "which people / as of when" difference is currently implicit and hand-rolled in each service, rather than an explicit, testable property.
Current state (files)
app/services/event_dashboard.rb— roster maps (registrants,organization_ids_by_registrant,primary_sector_names_by_registrant,scholarship_by_recipient,program_statuses_by_registrant,location_label_by_registrant,ce_registration_by_registrant, …) and breakdown datasets (primary_sectors/sector_counts,age_groups,state_counts,country_counts,program_status_counts/program_status_by_organization/program_status_registrant_ids,life_experiences,settings,registrant_city_breakdown,organizations, scholarship/CE pies, and the*_registrant_ids_by_*drill-in maps).app/services/attendees_roster.rb— the roster-map subset over apeoplecollection (attended-training registrations).app/services/attendees_breakdowns.rb— the breakdown-dataset subset over apeoplecollection.- Shared consumers:
_registrant_roster.html.erb,_registrant_breakdowns.html.erb,_breakdown_card.html.erb,_registrant_city_row.html.erb,_registrant_city_breakdown.html.erb. - Controller:
EventsController#roster(feedsEventDashboard,context: :event) andEventsController#attendees(feedsAttendees*,context: :index).
Proposed design
Introduce three POROs (names from the original plan; open to alternatives):
RegistrantPopulation— the single source of "which people, via which registrations, as of when." Constructors for each context, e.g.RegistrantPopulation.for_event(event)→ the event's active registrations, reference_date = event start.RegistrantPopulation.attended_trainings(people_scope)→ attended facilitator-training registrations, reference_date = today.
Owns the shared, date-anchored primitives currently duplicated:city_by_organization,program_status_by_organization, org/person id maps, the reference-date anchoring, and the org-affiliation preload.
RegistrantRoster.new(population)— every per-registrant lookup map_registrant_rosterreads. ReplacesAttendeesRoster+EventDashboard's roster maps.RegistrantBreakdowns.new(population)— every chart dataset +*_registrant_idsdrill-in map_registrant_breakdownsreads (including the pie denominators currently computed in ERB). ReplacesAttendeesBreakdowns+EventDashboard's breakdown methods.
EventDashboard keeps money / attendance / scholarship-money / applicants / shout-outs, composes a RegistrantPopulation.for_event, and delegates the roster/breakdown method names so dashboard.html.erb / recipients.html.erb / roster.html.erb don't change.
Tasks / scope
- Build
RegistrantPopulationwith both constructors and the shared date-anchored primitives; unit-spec the two anchorings explicitly (the reference-date behavior is currently only implicit). - Build
RegistrantRoster+RegistrantBreakdownsover a population; move the pie-denominator math out of_registrant_breakdowns.html.erbintoRegistrantBreakdowns. - Point
EventsController#attendeesat the new services; haveEventDashboardcompose + delegate. - Delete
AttendeesRoster/AttendeesBreakdowns. - Carry over the org-affiliation preload (the N+1 fix from PR #2113) into
RegistrantPopulationso it lives in one place. - Port the two deleted services' specs to the new names; add per-event-population examples (reference-date anchoring).
- Update
AGENTS.mdservices section.
Risks / notes
- Behavior-preserving refactor. The per-event Roster and cross-event Attendees request/view specs are the safety net — their output must be unchanged. This is the main reason it was deferred:
EventDashboardis large and the roster/breakdown extraction touches many methods. - Watch the subtle sourcing difference already encoded today: on the per-event Roster the scholarship/CE/org columns come from this event's registrations; on the cross-event index they come from each person's attended-training registrations. The population abstraction must make "which registrations" an explicit property so both are expressed without branching in the views.
- The
context: :eventvscontext: :indexdrill-in branching in_registrant_breakdowns.html.erbcan likely be simplified once the population owns the id maps, but that's optional polish.
Out of scope
- No DB/schema changes.
- No UI/behavior changes — pure internal consolidation.
- No policy changes.
Acceptance criteria
AttendeesRosterandAttendeesBreakdownsare gone; one roster service + one breakdown service back both the per-event Roster and the cross-event Attendees index.- The per-event Roster, cross-event Attendees, dashboard, and recipients pages render identically (existing request/view specs pass unchanged).
- Adding a new breakdown dimension requires editing exactly one place.
References
- PR #2113 (events IA refactor) — where these services were renamed and the N+1 was fixed in triplicate (the motivating pain).
- 主要语言
- Ruby
- 星标
- 15
- 派生
- 28
- 平均合并
- 16 小时 38 分钟
- 30 天内合并 PR
- 298
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 提供 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
rubyforgood/awbw 的其他 Issue
-
blocked
难度 2/5 1-3 小时 新手友好度 68/100
rubyforgood/awbw#2824 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
rubyforgood/awbw#2822 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
rubyforgood/awbw#2160 ·
维护者通常 1 天内回复
-
Update callout faq defualt text when the new person subscription launches可能已有人在做 @maebeale 于 85 天前认领。 未关闭blocked
难度 2/5 1-3 小时 新手友好度 72/100
rubyforgood/awbw#2095 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 84/100
rubyforgood/awbw#2088 ·
维护者通常 1 天内回复
相似的 Issue
-
JWT::EncodedToken raises TypeError for a non-object header可能已有人在做 @anakinj 今天认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 78/100
jwt/ruby-jwt#780 · 1 个 reaction ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 82/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
-
难度 2/5 1-3 小时 新手友好度 82/100
zold-io/zold-ruby-sdk#85 ·
-
难度 1/5 1 小时以内 新手友好度 88/100
维护者通常 1 天内回复