WAIT: Form-field identifiers + persist registration answers as structured data#1878
WAIT: Form-field identifiers + persist registration answers as structured data#1878maebeale wants to merge 7 commits into
Conversation
Some production registration forms carry the pluralized identifier "additional_age_groups" on the additional age-group field. The code only ever recognized the canonical singular "additional_age_group", so on those forms the field rendered with no options, rejected submissions, and never tagged age groups — failing silently end to end. Lift the hardcoded age-group identifiers into PRIMARY/ADDITIONAL identifier- list constants (mirroring the sector legacy-coverage pattern) and accept the plural alias everywhere they resolve: dynamic options, submission validation, tagging, profile display, and the form-submission show page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| "additional_age_group" => "AgeRange" | ||
| }.freeze | ||
| # Every "primary" and "additional" age group field (canonical or legacy) is | ||
| # backed by the published AgeRange categories. Unlike the sector fields, age |
There was a problem hiding this comment.
🤖 From Claude: Building DYNAMIC_FIELD_CATEGORY_TYPES from the identifier constants means dynamic option rendering, submission validation, and the option-source badge all pick up additional_age_groups for free — these consumers key off this hash, so they needed no further change.
| primary_sector_ids = collect_ids(FormField::PRIMARY_SECTOR_FIELD_IDENTIFIERS) | ||
| additional_sector_ids = collect_ids(FormField::ADDITIONAL_SECTOR_FIELD_IDENTIFIERS) | ||
| primary_age_ids = collect_ids(FormField::PRIMARY_AGE_GROUP_FIELD_IDENTIFIERS) | ||
| additional_age_ids = collect_ids(FormField::ADDITIONAL_AGE_GROUP_FIELD_IDENTIFIERS) |
There was a problem hiding this comment.
🤖 From Claude: Collecting across the identifier list (like sectors already did) means a form with either the canonical or the legacy plural name tags correctly; renamed the helper from collect_sector_ids to collect_ids since it now serves both.
Several registration fields were saved only as form_answers, never mapped onto their existing model columns: mailing/agency country, organization website and type. And there was no structured home for racial/ethnic identity at all. - Add people.racial_ethnic_identity, shown on the person profile and editable on the edit form, both gated to admins (allowed_to?(:manage?, Person) for display; the edit page is already admin-only). - In PublicRegistration, map racial_ethnic_identity, mailing_country/ agency_country, agency_website, and agency_type onto Person/Organization/ Address. A non-blank answer overwrites the value on file (latest registration wins); the prior value is preserved by AhoyTrackable's after_update audit event. A blank answer never clobbers existing data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the dev registration/scholarship forms to the real form screenshots:
reword the CE question ("Do you seek CE hours for this training?"), add the
license-number disclaimer, label the primary email "Primary Email", drop the
optional nickname/pronouns questions, order "What motivated you" before "How
did you hear", note payment timing under Payment Information, and head the
scholarship section "Partial Scholarship Application".
Every clause is idempotent and only matches the prior default, so re-seeds and
admin edits are left alone — including updates that bring an already-seeded CE
block up to the new wording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| apply_value(person, :racial_ethnic_identity, field_value("racial_ethnic_identity")) | ||
| end | ||
|
|
||
| def sync_organization_profile(organization) |
There was a problem hiding this comment.
🤖 From Claude: Overwrite (not fill-when-blank) is deliberate: the latest registration is the freshest source of truth, and because every model includes AhoyTrackable, this update! logs an Ahoy::Event capturing the prior→new value — so no history is lost. A blank answer is a no-op, and an unchanged value records no event.
| </div> | ||
| <% end %> | ||
| <!-- Racial/ethnic identity (admin only) --> | ||
| <% if allowed_to?(:manage?, Person) && @person.racial_ethnic_identity.present? %> |
There was a problem hiding this comment.
🤖 From Claude: Display is gated with allowed_to?(:manage?, Person) so an owner viewing their own profile (show? = admin? || owner?) does not see this; editing is already admin-only via PersonPolicy#edit?.
The scholarship edit view read the registration form's submission and filtered for "scholarship" section fields, so it only worked when the questions were embedded in the registration form. The app actually links a separate role: "scholarship" form whose answers are saved as their own submission, so the edit view showed "No scholarship questions were answered" even when they were. load_scholarship_submission now prefers the event's dedicated scholarship form (and its role: "scholarship" submission), falling back to the embedded-section registration form for older events. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more registration answers gain structured homes: - expected_payment_method (string) on event_registrations records the registrant's stated payment intent (Credit card now/later, Check) — the one payment signal not otherwise visible for the non-Stripe paths. Set on registration and overwritten on re-registration (latest intent wins); shown read-only in the admin registration form. Bulk payment keeps it as a form answer only, since it creates no EventRegistration until allocation. - mailing_list_consent_at / mailing_list_consent_source on people record email consent. Opt-in only: an affirmative answer stamps the time and source when none is on file; we never re-stamp or clear it from registration (withdrawal is a separate action). Shown admin-gated on the profile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Include the event start date in the consent source ("Registration: <title>
(<date>)") so consent is traceable when many trainings share a title.
- Add a "Consented to mailing list" checkbox to the admin person form via a
virtual mailing_list_consented attribute: unchecking withdraws (clears the
timestamp and source), checking grants when none is on file (stamped "Admin
update"), and re-checking leaves an existing consent untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lead with the ISO date so the registration source reads "2026-06-23 Facilitator Training registration". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🤖 From Claude: Closing — this branch's work has been split into focused PRs, and every concern here is now covered:
Nothing in this branch is left unrepresented, so it's superseded. |
🤖 PR, suggested 👤 review level: 🔬 Inspect — substantive logic across the registration service, a new column, and admin-gated profile UI
What is the goal of this PR and why is this important?
additional_age_groups: production forms carrying the pluralized identifier rendered no options, rejected submissions, and never tagged age groups — all silently. The code only knew the singularadditional_age_group.form_answers, never mapped onto their existing model columns (mailing/agency country, org website, org type), and racial/ethnic identity had no structured home at all.How did you approach the change?
PRIMARY/ADDITIONAL_AGE_GROUP_FIELD_IDENTIFIERSconstants (mirroring the sector legacy-coverage pattern);additional_age_groupsis now accepted everywhere age fields resolve.people.racial_ethnic_identity, shown on the person profile and editable on the edit form, both gated to admins (allowed_to?(:manage?, Person); edit is already admin-only).PublicRegistration, mappedracial_ethnic_identity,mailing_country/agency_country,agency_website, andagency_typeonto Person/Organization/Address. A non-blank answer overwrites the value on file (latest registration wins); the prior value is preserved byAhoyTrackable'safter_updateaudit event. A blank answer never clobbers.Anything else to add?
payment_methodis intentionally left as a form answer only; at registration it drives the Stripe-checkout decision but persists no dedicated column.