Consolidate event roster/breakdown data sources into a population-parameterized service

未关闭
#2,116 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
重构
描述清晰度
描述清楚
活跃度
冷清
技术栈
ruby
领域
backend

调研方向

从 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 内容生成。

描述

enhancement event mgmt

🤖 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/AttendeesBreakdowns were deliberately written to mirror EventDashboard'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 to EventDashboard and both Attendees* services separately.
  • EventDashboard is 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 a people collection (attended-training registrations).
  • app/services/attendees_breakdowns.rb — the breakdown-dataset subset over a people collection.
  • 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 (feeds EventDashboard, context: :event) and EventsController#attendees (feeds Attendees*, context: :index).

Proposed design

Introduce three POROs (names from the original plan; open to alternatives):

  1. 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.
  2. RegistrantRoster.new(population) — every per-registrant lookup map _registrant_roster reads. Replaces AttendeesRoster + EventDashboard's roster maps.
  3. RegistrantBreakdowns.new(population) — every chart dataset + *_registrant_ids drill-in map _registrant_breakdowns reads (including the pie denominators currently computed in ERB). Replaces AttendeesBreakdowns + 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 RegistrantPopulation with both constructors and the shared date-anchored primitives; unit-spec the two anchorings explicitly (the reference-date behavior is currently only implicit).
  • Build RegistrantRoster + RegistrantBreakdowns over a population; move the pie-denominator math out of _registrant_breakdowns.html.erb into RegistrantBreakdowns.
  • Point EventsController#attendees at the new services; have EventDashboard compose + delegate.
  • Delete AttendeesRoster / AttendeesBreakdowns.
  • Carry over the org-affiliation preload (the N+1 fix from PR #2113) into RegistrantPopulation so 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.md services 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: EventDashboard is 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: :event vs context: :index drill-in branching in _registrant_breakdowns.html.erb can 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

  • AttendeesRoster and AttendeesBreakdowns are 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

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

rubyforgood/awbw 的其他 Issue

查看 rubyforgood/awbw 的全部 Issue

相似的 Issue

更多 Ruby Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。