-
Notifications
You must be signed in to change notification settings - Fork 24
WAIT: Form-field identifiers + persist registration answers as structured data #1878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6fa81ee
e6d366c
036568d
ee47f5d
5c0258e
4b4e3a4
dd98c32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,13 +44,17 @@ def initialize(event:, form:, form_params:, scholarship_requested: false, person | |
| def call | ||
| ActiveRecord::Base.transaction do | ||
| person = find_or_create_person | ||
| sync_person_profile(person) | ||
|
|
||
| create_mailing_address(person) if field_value("mailing_city").present? | ||
| create_phone_contact(person) if field_value("phone").present? | ||
|
|
||
| organization = find_organization if field_value(ORGANIZATION_NAME_IDENTIFIER).present? | ||
| create_affiliation(person, organization) if organization | ||
| create_agency_address(organization) if organization && field_value("agency_city").present? | ||
| if organization | ||
| create_affiliation(person, organization) | ||
| sync_organization_profile(organization) | ||
| create_agency_address(organization) if field_value("agency_city").present? | ||
| end | ||
|
|
||
| assign_tags(person, organization) | ||
|
|
||
|
|
@@ -60,6 +64,7 @@ def call | |
| existing.update!(ce_credit_requested: true, ce_hours_requested: ce_hours_requested) if ce_credit_requested? | ||
| existing.update!(w9_requested: true) if w9_requested? | ||
| existing.update!(invoice_requested: true) if invoice_requested? | ||
| apply_value(existing, :expected_payment_method, field_value("payment_method")) | ||
| if existing.status == "cancelled" | ||
| existing.update!(status: "registered") | ||
| send_notifications(existing) | ||
|
|
@@ -166,6 +171,55 @@ def find_matching_person(last_name:, email:) | |
| .first | ||
| end | ||
|
|
||
| # Populate the structured columns that the registration form collects but that | ||
| # we historically stored only as form answers. A non-blank submitted value | ||
| # overwrites whatever was on file: the latest registration is treated as the | ||
| # freshest source of truth, and the prior value is preserved in the audit trail | ||
| # β every model includes AhoyTrackable, whose after_update logs an Ahoy::Event | ||
| # capturing the change. A blank answer never clobbers existing data. | ||
| def sync_person_profile(person) | ||
| apply_value(person, :racial_ethnic_identity, field_value("racial_ethnic_identity")) | ||
| record_mailing_list_consent(person) | ||
| end | ||
|
|
||
| # Consent is opt-in only and recorded once. An affirmative answer grants | ||
| # consent (stamping the time and where it came from) when none is on file; we | ||
| # never clear it from here β withdrawal is a separate, deliberate action β and | ||
| # we don't keep re-stamping a registrant who already consented. | ||
| def record_mailing_list_consent(person) | ||
| return if person.mailing_list_consent_at.present? | ||
| return unless mailing_list_consent_given? | ||
|
|
||
| person.update!( | ||
| mailing_list_consent_at: Time.current, | ||
| mailing_list_consent_source: mailing_list_consent_source | ||
| ) | ||
| end | ||
|
|
||
| # Identify the event by start date *and* title β many trainings share a title, | ||
| # so the leading date is what makes the consent source traceable to one event, | ||
| # e.g. "2026-06-23 Facilitator Training registration". | ||
| def mailing_list_consent_source | ||
| [ @event.start_date&.to_date&.iso8601, "#{@event.title} registration" ].compact.join(" ") | ||
| end | ||
|
|
||
| def mailing_list_consent_given? | ||
| Array(field_value("communication_consent")).any? { |value| value.to_s.strip.present? } | ||
| end | ||
|
|
||
| def sync_organization_profile(organization) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ 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. |
||
| apply_value(organization, :website_url, field_value("agency_website")) | ||
| apply_value(organization, :agency_type, field_value("agency_type")) | ||
| end | ||
|
|
||
| # Write value onto attribute when a non-blank value was submitted, overwriting | ||
| # any existing value. A no-op when the value is unchanged (update! records no | ||
| # change, so no spurious audit event). | ||
| def apply_value(record, attribute, value) | ||
| return if value.blank? | ||
| record.update!(attribute => value.strip) | ||
| end | ||
|
|
||
| def create_mailing_address(person) | ||
| new_city = field_value("mailing_city")&.strip | ||
| new_state = field_value("mailing_state")&.strip | ||
|
|
@@ -182,6 +236,7 @@ def create_mailing_address(person) | |
| primary: true, | ||
| inactive: false | ||
| ) | ||
| apply_value(existing, :country, field_value("mailing_country")) | ||
| return existing | ||
| end | ||
|
|
||
|
|
@@ -192,6 +247,7 @@ def create_mailing_address(person) | |
| city: new_city, | ||
| state: new_state, | ||
| zip_code: field_value("mailing_zip"), | ||
| country: field_value("mailing_country")&.strip, | ||
| locality: "Unknown", | ||
| address_type: field_value("mailing_address_type")&.downcase || "unknown", | ||
| primary: true | ||
|
|
@@ -256,6 +312,7 @@ def create_agency_address(organization) | |
| primary: true, | ||
| inactive: false | ||
| ) | ||
| apply_value(existing, :country, field_value("agency_country")) | ||
| return existing | ||
| end | ||
|
|
||
|
|
@@ -266,17 +323,18 @@ def create_agency_address(organization) | |
| city: new_city, | ||
| state: new_state, | ||
| zip_code: field_value("agency_zip"), | ||
| country: field_value("agency_country")&.strip, | ||
| locality: "Unknown", | ||
| address_type: "work", | ||
| primary: true | ||
| ) | ||
| end | ||
|
|
||
| def assign_tags(person, organization) | ||
| primary_sector_ids = collect_sector_ids(FormField::PRIMARY_SECTOR_FIELD_IDENTIFIERS) | ||
| additional_sector_ids = collect_sector_ids(FormField::ADDITIONAL_SECTOR_FIELD_IDENTIFIERS) | ||
| primary_age_ids = collect_ids_from_checkboxes("primary_age_group") | ||
| additional_age_ids = collect_ids_from_checkboxes("additional_age_group") | ||
| 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) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ 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 |
||
|
|
||
| if primary_sector_ids.any? || additional_sector_ids.any? | ||
| person.tag_sectors(primary_ids: primary_sector_ids, additional_ids: additional_sector_ids) | ||
|
|
@@ -292,7 +350,7 @@ def assign_tags(person, organization) | |
| end | ||
| end | ||
|
|
||
| def collect_sector_ids(identifiers) | ||
| def collect_ids(identifiers) | ||
| identifiers.flat_map { |id| collect_ids_from_checkboxes(id) } | ||
| end | ||
|
|
||
|
|
@@ -311,7 +369,8 @@ def create_event_registration(person) | |
| ce_credit_requested: ce_credit_requested?, | ||
| ce_hours_requested: ce_hours_requested, | ||
| w9_requested: w9_requested?, | ||
| invoice_requested: invoice_requested? | ||
| invoice_requested: invoice_requested?, | ||
| expected_payment_method: field_value("payment_method")&.strip.presence | ||
| ) | ||
| end | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,6 +165,26 @@ | |
| <%= render "shared/age_group_tags", primary: primary_age_groups, additional: additional_age_groups %> | ||
| </div> | ||
| <% end %> | ||
| <!-- Racial/ethnic identity (admin only) --> | ||
| <% if allowed_to?(:manage?, Person) && @person.racial_ethnic_identity.present? %> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ 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?. |
||
| <div class="md:col-span-2 p-3 admin-only bg-blue-100 rounded-lg"> | ||
| <h2 class="text-lg font-semibold text-gray-800 mb-3">Racial/ethnic identity</h2> | ||
| <p class="text-gray-700"><%= @person.racial_ethnic_identity %></p> | ||
| </div> | ||
| <% end %> | ||
| <!-- Mailing list consent (admin only) --> | ||
| <% if allowed_to?(:manage?, Person) %> | ||
| <div class="md:col-span-2 p-3 admin-only bg-blue-100 rounded-lg"> | ||
| <h2 class="text-lg font-semibold text-gray-800 mb-3">Mailing list consent</h2> | ||
| <% if @person.mailing_list_consent_at.present? %> | ||
| <p class="text-gray-700"> | ||
| Consented <%= @person.mailing_list_consent_at.to_date.to_fs(:long) %><%= " β #{@person.mailing_list_consent_source}" if @person.mailing_list_consent_source.present? %> | ||
| </p> | ||
| <% else %> | ||
| <p class="text-gray-500 italic">No consent on file.</p> | ||
| <% end %> | ||
| </div> | ||
| <% end %> | ||
| </div> | ||
| </div> | ||
| <!-- Bio --> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| class AddRacialEthnicIdentityToPeople < ActiveRecord::Migration[8.1] | ||
| def up | ||
| add_column :people, :racial_ethnic_identity, :string | ||
| end | ||
|
|
||
| def down | ||
| remove_column :people, :racial_ethnic_identity, if_exists: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| class AddExpectedPaymentMethodToEventRegistrations < ActiveRecord::Migration[8.1] | ||
| def up | ||
| add_column :event_registrations, :expected_payment_method, :string | ||
| end | ||
|
|
||
| def down | ||
| remove_column :event_registrations, :expected_payment_method, if_exists: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| class AddMailingListConsentToPeople < ActiveRecord::Migration[8.1] | ||
| def up | ||
| add_column :people, :mailing_list_consent_at, :datetime | ||
| add_column :people, :mailing_list_consent_source, :string | ||
| end | ||
|
|
||
| def down | ||
| remove_column :people, :mailing_list_consent_at, if_exists: true | ||
| remove_column :people, :mailing_list_consent_source, if_exists: true | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π€ From Claude: Building
DYNAMIC_FIELD_CATEGORY_TYPESfrom the identifier constants means dynamic option rendering, submission validation, and the option-source badge all pick upadditional_age_groupsfor free β these consumers key off this hash, so they needed no further change.