From 0e7981b0a85adbc08b09b865bb3e1974141cd019 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 00:28:26 -0400 Subject: [PATCH 1/6] Consolidate sector field identifiers to primary_sector + additional_sectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field identifiers now carry the name their behavior implies: the single-select primary field is primary_sector (was primary_sector_single), the multi-select additional field stays additional_sectors. Drop the legacy lookups the old misnomer created — primary_sector meaning "additional", plus primary_service_area / primary_service_area_single — now that the stored data is consolidated. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/models/form_field.rb | 18 ++-- app/services/form_builder_service.rb | 4 +- app/services/smart_form_fields.rb | 7 +- .../public_registrations/_form_field.html.erb | 2 +- db/seeds/dev/events_management.rb | 2 +- db/seeds/dev/legacy_form_identifiers.rb | 96 ------------------- lib/tasks/dev.rake | 6 -- 7 files changed, 13 insertions(+), 122 deletions(-) delete mode 100644 db/seeds/dev/legacy_form_identifiers.rb diff --git a/app/models/form_field.rb b/app/models/form_field.rb index cc6b391876..d45883dac2 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -15,17 +15,13 @@ class FormField < ApplicationRecord # "required" flag is meaningless for them (nothing to fill in). NON_INPUT_ANSWER_TYPES = %w[no_user_input group_header].freeze - # Multi-select "additional sectors" field identifiers. "additional_sectors" is - # the canonical name new forms are built with; the older "primary_sector" name - # (a misnomer — it was always the multi-select additional field) and the legacy - # "service area" name are both still accepted so existing form data keeps - # resolving. - ADDITIONAL_SECTOR_FIELD_IDENTIFIERS = %w[additional_sectors primary_sector primary_service_area].freeze - - # Single-select "primary sector" field identifiers (current + legacy). Unlike - # the multi-select "additional" field, these omit the catch-all "Other" sector - # — a respondent's primary sector must be a concrete sector. - PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector_single primary_service_area_single].freeze + # Multi-select "additional sectors" field identifier. + ADDITIONAL_SECTOR_FIELD_IDENTIFIERS = %w[additional_sectors].freeze + + # Single-select "primary sector" field identifier. Unlike the multi-select + # "additional" field, it omits the catch-all "Other" sector — a respondent's + # primary sector must be a concrete sector. + PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector].freeze # Field identifiers whose selectable options are sourced dynamically from # Sector records rather than the field's own stored answer options. The diff --git a/app/services/form_builder_service.rb b/app/services/form_builder_service.rb index 2d72897d9c..ea757612fb 100644 --- a/app/services/form_builder_service.rb +++ b/app/services/form_builder_service.rb @@ -50,7 +50,7 @@ def call agency_street agency_city agency_state agency_zip agency_country ], person_background: %w[racial_ethnic_identity], - professional_info: %w[primary_sector_single additional_sectors primary_age_group additional_age_group], + professional_info: %w[primary_sector additional_sectors primary_age_group additional_age_group], marketing: %w[referral_source training_motivation interested_in_more], continuing_education: %w[ce_credit_interest ce_license_number], scholarship: %w[scholarship_eligibility scholarship_contribution impact_description implementation_plan additional_comments], @@ -455,7 +455,7 @@ def build_professional_info_fields(form, position) position = add_header(form, position, "Professional Information", group: "professional") position = add_field(form, position, "Primary sector", :single_select_dropdown, - key: "primary_sector_single", group: "professional", required: false, + key: "primary_sector", group: "professional", required: false, subtitle: "Select the option that best represents those you primarily intend to serve through the art workshops") position = add_field(form, position, "Additional sectors", :multi_select_checkbox, key: "additional_sectors", group: "professional", required: false, diff --git a/app/services/smart_form_fields.rb b/app/services/smart_form_fields.rb index 024b1ba97c..15209d28bc 100644 --- a/app/services/smart_form_fields.rb +++ b/app/services/smart_form_fields.rb @@ -108,13 +108,10 @@ class SmartFormFields "records rather than from options typed into the form editor. The tags are applied to " \ "the registrant and to their linked organization.", fields: [ - [ "primary_sector_single", "Primary sector", "Tags the person and organization with one primary sector. Offers no \"Other\" — a primary sector must be a real sector." ], + [ "primary_sector", "Primary sector", "Tags the person and organization with one primary sector. Offers no \"Other\" — a primary sector must be a real sector." ], [ "additional_sectors", "Additional sectors", "Tags the person and organization with any number of additional sectors. An \"Other\" answer goes to the Other responses review queue, where it can be promoted into a real sector." ], [ "primary_age_group", "Primary age group(s) served", "Tags the person and organization with the primary age range served." ], - [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ], - [ "primary_sector", "Additional sectors (legacy)", "Older name for the additional sectors question. Still honored so existing forms keep working; use additional_sectors on new forms." ], - [ "primary_service_area", "Additional sectors (legacy)", "Older name for the additional sectors question. Still honored; use additional_sectors on new forms." ], - [ "primary_service_area_single", "Primary sector (legacy)", "Older name for the primary sector question. Still honored; use primary_sector_single on new forms." ] + [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ] ] }, { diff --git a/app/views/events/public_registrations/_form_field.html.erb b/app/views/events/public_registrations/_form_field.html.erb index d71ac5279e..755ce74731 100644 --- a/app/views/events/public_registrations/_form_field.html.erb +++ b/app/views/events/public_registrations/_form_field.html.erb @@ -114,7 +114,7 @@ <% end %> <% when "single_select_dropdown" %> - <%# Dynamic fields (e.g. primary_sector_single) source options from + <%# Dynamic fields (e.g. primary_sector) source options from Sector/Category data; everything else uses the field's own stored options. %> <%# Dropdowns have no free-text input, so the "Other" option is omitted here. Named specify options keep meaning as a bare choice, so they stay. %> diff --git a/db/seeds/dev/events_management.rb b/db/seeds/dev/events_management.rb index d957e4e7eb..725d2f6012 100644 --- a/db/seeds/dev/events_management.rb +++ b/db/seeds/dev/events_management.rb @@ -1185,7 +1185,7 @@ # Mirror the registration form's two sector fields: the single-select primary # sector dropdown and the multi-select additional sectors checkboxes. - primary_field = form.form_fields.find_by(field_identifier: "primary_sector_single") + primary_field = form.form_fields.find_by(field_identifier: "primary_sector") if primary_field && primary_sector && submission.form_answers.where(form_field: primary_field).none? submission.form_answers.create!(form_field: primary_field, submitted_answer: primary_sector.id.to_s, diff --git a/db/seeds/dev/legacy_form_identifiers.rb b/db/seeds/dev/legacy_form_identifiers.rb deleted file mode 100644 index 222d299979..0000000000 --- a/db/seeds/dev/legacy_form_identifiers.rb +++ /dev/null @@ -1,96 +0,0 @@ -# Legacy field-identifier registration forms (dev-only). -# -# The four professional questions — primary/additional sector and -# primary/additional age group — resolve their options dynamically from Sector / -# AgeRange records, and the code still accepts several *legacy* field_identifiers -# for the sector fields so older forms keep working (see -# FormField::PRIMARY_SECTOR_FIELD_IDENTIFIERS / ADDITIONAL_SECTOR_FIELD_IDENTIFIERS). -# The canonical combination already ships as "Training Registration Form"; these -# extra standalone forms each carry a different legacy combination, with a demo -# registrant + submission + tags, so every surface (form rendering, the form -# submission show, and the registrant's profile/edit pages) can be eyeballed on -# real data for every identifier scheme. -# -# Loaded last in the dev seed order so its extra role: "registration" forms can't -# shadow the canonical one that events_management.rb / scholarships.rb look up via -# `Form.standalone.find_by(role: "registration")`. Idempotent throughout. - -puts "Creating legacy field-identifier registration forms…" - -# The dynamic option pools, mirroring the canonical lists. The single-select -# primary sector field omits the catch-all "Other"; the additional sector field -# keeps it (offered as a folded "Other: "). Age groups have no catch-all. -concrete_sectors = Sector.published.excluding_other.order(:name).to_a -other_sector = Sector.published.find_by(name: Sector::OTHER_SECTOR_NAME) -age_categories = Category.age_ranges.published.order(:position, :name).to_a - -# Each scheme renames the two canonical sector fields onto a legacy combination. -# The age-group fields have never been renamed, so they stay canonical. -legacy_schemes = [ - { suffix: "legacy service area", - primary_sector: "primary_service_area_single", additional_sector: "primary_service_area", - first_name: "Lee", last_name: "Servicearea", email: "legacy.servicearea@example.com" }, - { suffix: "legacy additional name", - primary_sector: "primary_sector_single", additional_sector: "primary_sector", - first_name: "Morgan", last_name: "Additionalname", email: "legacy.additional@example.com" }, - { suffix: "legacy mixed", - primary_sector: "primary_service_area_single", additional_sector: "additional_sectors", - first_name: "Rae", last_name: "Mixedscheme", email: "legacy.mixed@example.com" } -] - -legacy_schemes.each_with_index do |scheme, i| - form_name = "Training Registration Form (#{scheme[:suffix]})" - - form = Form.standalone.find_by(name: form_name) - unless form - form = FormBuilderService.new( - name: form_name, - sections: %i[person_identifier professional_info], - role: "registration" - ).call - { "primary_sector_single" => scheme[:primary_sector], - "additional_sectors" => scheme[:additional_sector] }.each do |canonical, legacy| - next if canonical == legacy - form.form_fields.find_by(field_identifier: canonical)&.update!(field_identifier: legacy) - end - end - - # A demo registrant whose submission + tags back the form, so the profile and - # submission pages have data to render for this scheme. - person = Person.find_or_create_by!(email: scheme[:email]) do |p| - p.first_name = scheme[:first_name] - p.last_name = scheme[:last_name] - end - - submission = FormSubmission.find_or_create_by!(form: form, person: person, role: "registration") - - primary_sector = concrete_sectors[i % concrete_sectors.size] if concrete_sectors.any? - additional_sectors = concrete_sectors.rotate(i + 1).reject { |sector| sector == primary_sector }.first(2) - primary_age = age_categories[i % age_categories.size] if age_categories.any? - additional_ages = age_categories.rotate(i + 1).reject { |age| age == primary_age }.first(2) - - # Mirror how public registration stores the answers: a single id for the - # dropdowns, ", "-joined ids for the checkboxes, and a folded "Other: " for - # the additional sectors so the free-text path is represented too. - additional_sector_value = (additional_sectors.map(&:id) + - [ ("Other: Equine-assisted therapy" if other_sector) ].compact).join(", ") - { - scheme[:primary_sector] => primary_sector&.id&.to_s, - scheme[:additional_sector] => additional_sector_value.presence, - "primary_age_group" => primary_age&.id&.to_s, - "additional_age_group" => additional_ages.map(&:id).join(", ").presence - }.each do |identifier, value| - next if value.blank? - field = form.form_fields.find_by(field_identifier: identifier) - next unless field - next if submission.form_answers.where(form_field: field).any? - submission.form_answers.create!(form_field: field, - submitted_answer: value, - question_name_when_answered: field.name) - end - - # Tag the registrant with the same primary/additional split assign_tags applies, - # so the profile/edit pages crown the right primary and list the additional ones. - person.tag_sectors(primary_ids: [ primary_sector&.id ].compact, additional_ids: additional_sectors.map(&:id)) - person.tag_age_groups(primary_ids: [ primary_age&.id ].compact, additional_ids: additional_ages.map(&:id)) -end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 3da230837f..4c3a81647b 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -23,7 +23,6 @@ namespace :db do scholarships membership bulk_payments - legacy_form_identifiers public_forms ] @@ -125,11 +124,6 @@ namespace :db do load Rails.root.join("db/seeds/dev/bulk_payments.rb") end - desc "Seed registration forms using legacy professional-field identifiers (dev only)" - task legacy_form_identifiers: :environment do - load Rails.root.join("db/seeds/dev/legacy_form_identifiers.rb") - end - desc "Seed standalone public forms with submissions and answers (dev only)" task public_forms: :environment do load Rails.root.join("db/seeds/dev/public_forms.rb") From b07fefcb916826720ade96bf057e0ca3001ef2fa Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 06:03:49 -0400 Subject: [PATCH 2/6] Update specs and dev seed for the consolidated sector identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename primary_sector_single → primary_sector in the specs that exercise it, collapse the now-single-scheme professional-fields request spec, and retire the legacy service-area test cases (converted to canonical additional_sectors / primary_sector). Remove the legacy_form_identifiers dev seed and its rake task. Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/helpers/application_helper_spec.rb | 10 +++---- spec/models/form_field_spec.rb | 29 +++++++------------ spec/models/person_spec.rb | 4 +-- .../professional_field_identifiers_spec.rb | 15 +++------- .../events/public_registrations_spec.rb | 6 ++-- spec/requests/events_spec.rb | 2 +- spec/services/event_dashboard_spec.rb | 14 ++++----- .../public_registration_spec.rb | 6 ++-- spec/services/form_builder_service_spec.rb | 2 +- ...ublic_registration_form_submission_spec.rb | 8 ++--- 10 files changed, 39 insertions(+), 57 deletions(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 33de9324c1..9172011f54 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -587,19 +587,19 @@ describe "#dynamic_form_field_options" do let(:form) { create(:form) } - it "omits the Other sector for the primary service-area dropdown" do + it "omits the Other sector for the primary sector dropdown" do create(:sector, :published, name: "Domestic Violence") create(:sector, :published, name: "Other") - field = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_service_area_single") + field = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_sector") labels = helper.dynamic_form_field_options(field).map(&:first) expect(labels).to include("Domestic Violence") expect(labels).not_to include("Other") end - it "includes the Other sector for the additional service-areas field" do + it "includes the Other sector for the additional sectors field" do create(:sector, :published, name: "Other") - field = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "primary_service_area") + field = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "additional_sectors") labels = helper.dynamic_form_field_options(field).map(&:first) expect(labels).to include("Other") @@ -608,7 +608,7 @@ it "carries each sector's description as the third tuple element" do create(:sector, :published, name: "Domestic Violence", description: "DV services") create(:sector, :published, name: "Mental Health", description: nil) - field = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "primary_service_area") + field = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "additional_sectors") descriptions = helper.dynamic_form_field_options(field).to_h { |name, _id, desc| [ name, desc ] } expect(descriptions["Domestic Violence"]).to eq("DV services") diff --git a/spec/models/form_field_spec.rb b/spec/models/form_field_spec.rb index 2bdda30643..8749919ff6 100644 --- a/spec/models/form_field_spec.rb +++ b/spec/models/form_field_spec.rb @@ -441,8 +441,8 @@ def selectable_field(type:, option_names:) end context "with dynamically-sourced options" do - it "accepts a published Sector id and rejects others for a service-area field" do - field = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_service_area_single") + it "accepts a published Sector id and rejects others for a primary sector field" do + field = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_sector") offered = create(:sector, :published) unpublished = create(:sector, :unpublished) @@ -451,22 +451,13 @@ def selectable_field(type:, option_names:) expect(field.answer_inclusion_error("999999")).to eq("has an invalid selection") end - # The canonical identifiers, the legacy "primary_sector" additional name, - # and the legacy "service area" names must all behave identically so - # existing form data keeps resolving. - { - "sector" => %w[primary_sector_single additional_sectors], - "sector (legacy additional name)" => %w[primary_sector_single primary_sector], - "service area (legacy)" => %w[primary_service_area_single primary_service_area] - }.each do |scheme, (primary_id, additional_id)| - it "rejects the Other sector for the primary #{scheme} field but accepts it for additional" do - other = create(:sector, :published, name: "Other") - primary = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: primary_id) - additional = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: additional_id) - - expect(primary.answer_inclusion_error(other.id.to_s)).to eq("has an invalid selection") - expect(additional.answer_inclusion_error([ other.id.to_s ])).to be_nil - end + it "rejects the Other sector for the primary sector field but accepts it for additional" do + other = create(:sector, :published, name: "Other") + primary = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_sector") + additional = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "additional_sectors") + + expect(primary.answer_inclusion_error(other.id.to_s)).to eq("has an invalid selection") + expect(additional.answer_inclusion_error([ other.id.to_s ])).to be_nil end it "accepts a published Category id from the backing type for a category field" do @@ -498,7 +489,7 @@ def selectable_field(type:, option_names:) create(:sector, :published, name: "Other") mental_health = create(:sector, :published, name: "Mental Health") additional = create(:form_field, form: form, answer_type: :multi_select_checkbox, field_identifier: "additional_sectors") - primary = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_sector_single") + primary = create(:form_field, form: form, answer_type: :single_select_dropdown, field_identifier: "primary_sector") # The "Other" Sector renders a free-text box on the additional checkboxes, # which submits the folded "Other: " (or a bare "Other"); both pass. diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index d0dd1d4f28..1ac398e8c5 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -596,8 +596,8 @@ def answer(identifier, value) .to contain_exactly("Toddlers") end - it "does not pull from the service area fields" do - answer("primary_service_area", "Other: Equine therapy") + it "does not pull from the sector fields" do + answer("additional_sectors", "Other: Equine therapy") expect(person.other_workshop_setting_responses).to be_empty end diff --git a/spec/requests/events/professional_field_identifiers_spec.rb b/spec/requests/events/professional_field_identifiers_spec.rb index 47dab03b15..5af5d3e5c6 100644 --- a/spec/requests/events/professional_field_identifiers_spec.rb +++ b/spec/requests/events/professional_field_identifiers_spec.rb @@ -2,8 +2,7 @@ # The four professional registration questions (primary/additional sector and # primary/additional age group) resolve their options dynamically from Sector / -# AgeRange records and must keep working across every legacy field_identifier a -# form might still carry. For each identifier scheme this exercises, end to end: +# AgeRange records. This exercises the canonical field_identifiers end to end: # # * option rendering — the primary sector field is a dropdown of the published # sectors minus the catch-all "Other"; the additional sector field keeps @@ -29,12 +28,8 @@ let!(:sector_other) { create(:sector, :published, name: Sector::OTHER_SECTOR_NAME) } let!(:sector_hidden) { create(:sector, name: "Hidden sector") } - # Each scheme maps the two sector fields onto a canonical or legacy identifier; - # the age-group fields have never been renamed, so they stay constant. { - "canonical identifiers" => { primary_sector: "primary_sector_single", additional_sector: "additional_sectors" }, - "legacy additional-sector name" => { primary_sector: "primary_sector_single", additional_sector: "primary_sector" }, - "legacy service-area names" => { primary_sector: "primary_service_area_single", additional_sector: "primary_service_area" } + "canonical identifiers" => { primary_sector: "primary_sector", additional_sector: "additional_sectors" } }.each do |scheme_name, ids| context "with #{scheme_name}" do let(:form) { build_professional_form(ids) } @@ -149,16 +144,14 @@ # ---- Form construction ---- - # Builds a registration form with just the identity + professional sections, - # then renames the two sector fields onto the scheme's identifiers (the age - # fields keep their canonical names — they were never renamed). + # Builds a registration form with just the identity + professional sections. def build_professional_form(ids) form = FormBuilderService.new( name: "Reg #{ids[:primary_sector]} / #{ids[:additional_sector]}", sections: %i[person_identifier professional_info], role: "registration" ).call - rename_field(form, "primary_sector_single", ids[:primary_sector]) + rename_field(form, "primary_sector", ids[:primary_sector]) rename_field(form, "additional_sectors", ids[:additional_sector]) form end diff --git a/spec/requests/events/public_registrations_spec.rb b/spec/requests/events/public_registrations_spec.rb index 19e09c3aa6..cf5bce0d55 100644 --- a/spec/requests/events/public_registrations_spec.rb +++ b/spec/requests/events/public_registrations_spec.rb @@ -386,14 +386,14 @@ def post_with_scholarship(scholarship_answer) end it "renders a dynamic-option field switched to single choice as radio buttons" do - # primary_service_area sources its options dynamically from Sector + # additional_sectors sources its options dynamically from Sector # (it stores no answer options of its own). When such a field is changed # from checkbox to single-choice radio, the public form must still render # the dynamic options — otherwise the question shows up blank. sector_a = create(:sector, :published, name: "Healthcare") sector_b = create(:sector, :published, name: "Education") create(:form_field, form: form, answer_type: :single_select_radio, - field_identifier: "primary_service_area", name: "Primary sector", + field_identifier: "additional_sectors", name: "Additional sectors", required: false) get new_event_public_registration_path(event) @@ -407,7 +407,7 @@ def post_with_scholarship(scholarship_answer) it "still renders a dynamic-option field as checkboxes" do create(:sector, :published, name: "Healthcare") create(:form_field, form: form, answer_type: :multi_select_checkbox, - field_identifier: "primary_service_area", name: "Primary sector", + field_identifier: "additional_sectors", name: "Additional sectors", required: false) get new_event_public_registration_path(event) diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index da20cbece9..445002cda6 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -2848,7 +2848,7 @@ def ce_chip_text submission = create(:form_submission, person: person, form: registration_form) sector = create(:sector, name: "Sexual Assault") - sector_field = create(:form_field, form: registration_form, field_identifier: "primary_sector_single") + sector_field = create(:form_field, form: registration_form, field_identifier: "primary_sector") create(:form_answer, form_submission: submission, form_field: sector_field, submitted_answer: sector.id.to_s) age_range = create(:category_type, name: "AgeRange") diff --git a/spec/services/event_dashboard_spec.rb b/spec/services/event_dashboard_spec.rb index 198e3a873a..2fa414eb41 100644 --- a/spec/services/event_dashboard_spec.rb +++ b/spec/services/event_dashboard_spec.rb @@ -297,11 +297,11 @@ # single-select dropdown. Both also carry sector1 as a tag, so sector1 is # primary for person1 AND an additional sector for person2 — the counts # overlap (don't partition). - service_field = create(:form_field, form: registration_form, field_identifier: "primary_service_area_single", - answer_type: :single_select_dropdown) - create(:form_answer, form_field: service_field, submitted_answer: sector1.id.to_s, + primary_field = create(:form_field, form: registration_form, field_identifier: "primary_sector", + answer_type: :single_select_dropdown) + create(:form_answer, form_field: primary_field, submitted_answer: sector1.id.to_s, form_submission: FormSubmission.find_by!(person: person1, form: registration_form)) - create(:form_answer, form_field: service_field, submitted_answer: sector2.id.to_s, + create(:form_answer, form_field: primary_field, submitted_answer: sector2.id.to_s, form_submission: FormSubmission.find_by!(person: person2, form: registration_form)) end @@ -569,11 +569,9 @@ end it "gathers header (sector / age group) answers keyed by applicant, sector answers under the normalized sector key" do - # Use a legacy "service area" identifier to confirm it still resolves under - # the normalized sector key alongside the current "sector" identifiers. - service_field = create(:form_field, form: registration_form, name: "Primary sector", field_identifier: "primary_service_area") + sector_field = create(:form_field, form: registration_form, name: "Additional sectors", field_identifier: "additional_sectors") reg_submission = FormSubmission.find_by(person: embedded_applicant, form: registration_form) - create(:form_answer, form_submission: reg_submission, form_field: service_field, submitted_answer: "5") + create(:form_answer, form_submission: reg_submission, form_field: sector_field, submitted_answer: "5") header = dashboard.header_answers_by_applicant diff --git a/spec/services/event_registration_services/public_registration_spec.rb b/spec/services/event_registration_services/public_registration_spec.rb index 49c66aa547..27e925b749 100644 --- a/spec/services/event_registration_services/public_registration_spec.rb +++ b/spec/services/event_registration_services/public_registration_spec.rb @@ -582,7 +582,7 @@ def register_with_agency_type(value) event: event, registration_form: form, form_params: base_form_params(first_name: "Pat", last_name: "Lee", email: "pat@example.com").merge( - field_id("primary_sector_single") => primary_sector.id.to_s, + field_id("primary_sector") => primary_sector.id.to_s, field_id("additional_sectors") => [ additional_sector.id.to_s ] ) ) @@ -601,7 +601,7 @@ def register_with_agency_type(value) event: event, registration_form: form, form_params: base_form_params(first_name: "Pat", last_name: "Lee", email: "pat@example.com").merge( - field_id("primary_sector_single") => primary_sector.id.to_s + field_id("primary_sector") => primary_sector.id.to_s ) ) @@ -616,7 +616,7 @@ def register_with_agency_type(value) event: event, registration_form: form, form_params: base_form_params(first_name: "Pat", last_name: "Lee", email: "pat@example.com").merge( - field_id("primary_sector_single") => primary_sector.id.to_s + field_id("primary_sector") => primary_sector.id.to_s ) ) diff --git a/spec/services/form_builder_service_spec.rb b/spec/services/form_builder_service_spec.rb index 8f4edf6d44..688f6aca98 100644 --- a/spec/services/form_builder_service_spec.rb +++ b/spec/services/form_builder_service_spec.rb @@ -153,7 +153,7 @@ let(:form) { described_class.new(name: "Test", sections: %i[professional_info]).call } it "asks for a single primary sector via a dropdown before the additional sectors checkboxes" do - primary = form.form_fields.find_by(field_identifier: "primary_sector_single") + primary = form.form_fields.find_by(field_identifier: "primary_sector") additional = form.form_fields.find_by(field_identifier: "additional_sectors") expect(primary).to have_attributes(name: "Primary sector", answer_type: "single_select_dropdown") diff --git a/spec/system/public_registration_form_submission_spec.rb b/spec/system/public_registration_form_submission_spec.rb index e43aa8e8da..84e8d13401 100644 --- a/spec/system/public_registration_form_submission_spec.rb +++ b/spec/system/public_registration_form_submission_spec.rb @@ -103,7 +103,7 @@ "agency_website" => "https://hope.example.org", "agency_type" => "501c3/nonprofit", "agency_country" => "United States", - "primary_sector_single" => sector_education.id.to_s, + "primary_sector" => sector_education.id.to_s, "additional_sectors" => sector_mental_health.id.to_s, "primary_age_group" => age_adults.id.to_s, "additional_age_group" => age_teens.id.to_s, @@ -141,7 +141,7 @@ expect(page).not_to have_selector("##{pr_dom_id(reg_field('first_name'))}") expect(page).not_to have_selector("##{pr_dom_id(reg_field('mailing_city'))}") - select_pr reg_field("primary_sector_single"), "Education" + select_pr reg_field("primary_sector"), "Education" check_pr_box reg_field("additional_sectors"), sector_dv.id.to_s select_pr reg_field("primary_age_group"), "Teens (13-17)" choose_pr_radio reg_field("racial_ethnic_identity"), "Asian" @@ -159,7 +159,7 @@ answers = answers_by_identifier(registration_form.form_submissions.find_by!(person: logged_in_person)) expect(answers).to include( - "primary_sector_single" => sector_education.id.to_s, + "primary_sector" => sector_education.id.to_s, "additional_sectors" => sector_dv.id.to_s, "primary_age_group" => age_teens.id.to_s, "racial_ethnic_identity" => "Asian", @@ -409,7 +409,7 @@ def fill_full_registration fill_pr_text reg_field("agency_zip"), with: "91101" fill_pr_text reg_field("agency_country"), with: "United States" - select_pr reg_field("primary_sector_single"), "Education" + select_pr reg_field("primary_sector"), "Education" check_pr_box reg_field("additional_sectors"), sector_mental_health.id.to_s select_pr reg_field("primary_age_group"), "Adults (18+)" check_pr_box reg_field("additional_age_group"), age_teens.id.to_s From 718e2073e4c0b562a37ff3a6df24ede9f113143a Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 06:19:40 -0400 Subject: [PATCH 3/6] Drop stale "service area" wording from comments and a spec label The service-area field identifiers are retired; update the two comment examples and the dashboard spec description to say "sector(s)" so no stale terminology remains. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/models/form_field.rb | 2 +- app/views/forms/_form_field_fields.html.erb | 2 +- spec/services/event_dashboard_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/form_field.rb b/app/models/form_field.rb index d45883dac2..6d1f37ffff 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -87,7 +87,7 @@ class FormField < ApplicationRecord # Keeps an over-long name as a friendly validation error instead of a # database ValueTooLong exception (the column is text, this is the UX cap). validates :name, length: { maximum: 1000 } - # A field_identifier wires a field to backend logic (Stripe, service areas, + # A field_identifier wires a field to backend logic (Stripe, sectors, # email checks, etc.), so the same identifier must not appear twice on one form # or that logic would target an ambiguous field. Ordinary fields carry no # identifier, so blanks are exempt — any number of them may coexist. diff --git a/app/views/forms/_form_field_fields.html.erb b/app/views/forms/_form_field_fields.html.erb index 82990b1c96..f8679d3320 100644 --- a/app/views/forms/_form_field_fields.html.erb +++ b/app/views/forms/_form_field_fields.html.erb @@ -151,7 +151,7 @@ class: "w-24 shrink-0 rounded border-gray-300 shadow-sm px-2 py-1 text-sm", title: "Maximum number of characters allowed (free-form text fields only)" %> <%# Smart field name + Remove stay pinned together so Remove never lands on its own - line. The name wires this field to backend logic (Stripe, service areas, email + line. The name wires this field to backend logic (Stripe, sectors, email checks, etc.); most fields need none, and the linked hint explains each one. %>
diff --git a/spec/services/event_dashboard_spec.rb b/spec/services/event_dashboard_spec.rb index 2fa414eb41..0651105095 100644 --- a/spec/services/event_dashboard_spec.rb +++ b/spec/services/event_dashboard_spec.rb @@ -305,7 +305,7 @@ form_submission: FormSubmission.find_by!(person: person2, form: registration_form)) end - it "counts distinct sectors named as a primary service area" do + it "counts distinct sectors named as a primary sector" do expect(dashboard.primary_sector_count).to eq(2) end From f529ec2da383975ae9fd1d3a06f60a98e421a249 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 06:26:43 -0400 Subject: [PATCH 4/6] Keep primary_sector_single as a legacy alias for the primary sector field Re-accept the older primary_sector_single identifier alongside the canonical primary_sector so forms still carrying it keep resolving, and document it on the Smart form settings reference page (the drift spec requires every sector identifier to be catalogued). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/models/form_field.rb | 10 ++++++---- app/services/smart_form_fields.rb | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/models/form_field.rb b/app/models/form_field.rb index 6d1f37ffff..9d20f10dfb 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -18,10 +18,12 @@ class FormField < ApplicationRecord # Multi-select "additional sectors" field identifier. ADDITIONAL_SECTOR_FIELD_IDENTIFIERS = %w[additional_sectors].freeze - # Single-select "primary sector" field identifier. Unlike the multi-select - # "additional" field, it omits the catch-all "Other" sector — a respondent's - # primary sector must be a concrete sector. - PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector].freeze + # Single-select "primary sector" field identifiers. "primary_sector" is the + # canonical name; the legacy "primary_sector_single" is still accepted so + # existing forms keep resolving. Unlike the multi-select "additional" field, + # these omit the catch-all "Other" sector — a respondent's primary sector must + # be a concrete sector. + PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector primary_sector_single].freeze # Field identifiers whose selectable options are sourced dynamically from # Sector records rather than the field's own stored answer options. The diff --git a/app/services/smart_form_fields.rb b/app/services/smart_form_fields.rb index 15209d28bc..2dc70139c0 100644 --- a/app/services/smart_form_fields.rb +++ b/app/services/smart_form_fields.rb @@ -111,7 +111,8 @@ class SmartFormFields [ "primary_sector", "Primary sector", "Tags the person and organization with one primary sector. Offers no \"Other\" — a primary sector must be a real sector." ], [ "additional_sectors", "Additional sectors", "Tags the person and organization with any number of additional sectors. An \"Other\" answer goes to the Other responses review queue, where it can be promoted into a real sector." ], [ "primary_age_group", "Primary age group(s) served", "Tags the person and organization with the primary age range served." ], - [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ] + [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ], + [ "primary_sector_single", "Primary sector (legacy)", "Older name for the primary sector question. Still honored; use primary_sector on new forms." ] ] }, { From 63be600e0abdd6f263df157355bb022f6a15b9ab Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 06:30:47 -0400 Subject: [PATCH 5/6] Restore the legacy-identifier dev seed for the retained primary_sector_single Deleting it was premature: primary_sector_single is still an accepted legacy identifier for the primary sector field, so the dev seed that demonstrates a form carrying a legacy identifier is still useful. Trim it to that one surviving scheme (additional sectors and both age-group fields were never renamed, so they stay canonical) and re-wire its rake task. Co-Authored-By: Claude Opus 4.8 (1M context) --- db/seeds/dev/legacy_form_identifiers.rb | 77 +++++++++++++++++++++++++ lib/tasks/dev.rake | 6 ++ 2 files changed, 83 insertions(+) create mode 100644 db/seeds/dev/legacy_form_identifiers.rb diff --git a/db/seeds/dev/legacy_form_identifiers.rb b/db/seeds/dev/legacy_form_identifiers.rb new file mode 100644 index 0000000000..526b7f231e --- /dev/null +++ b/db/seeds/dev/legacy_form_identifiers.rb @@ -0,0 +1,77 @@ +# Legacy field-identifier registration form (dev-only). +# +# The professional sector questions still accept one *legacy* field_identifier — +# "primary_sector_single" for the single-select primary sector — so older forms +# keep working (see FormField::PRIMARY_SECTOR_FIELD_IDENTIFIERS). The canonical +# combination already ships as "Training Registration Form"; this extra standalone +# form carries the legacy primary identifier, with a demo registrant + submission +# + tags, so every surface (form rendering, the form submission show, and the +# registrant's profile/edit pages) can be eyeballed on real data. +# +# The additional sector field and both age-group fields have never been renamed, +# so they stay canonical. +# +# Loaded last in the dev seed order so its extra role: "registration" form can't +# shadow the canonical one that events_management.rb / scholarships.rb look up via +# `Form.standalone.find_by(role: "registration")`. Idempotent throughout. + +puts "Creating legacy field-identifier registration form…" + +# The dynamic option pools, mirroring the canonical lists. The single-select +# primary sector field omits the catch-all "Other"; the additional sector field +# keeps it (offered as a folded "Other: "). Age groups have no catch-all. +concrete_sectors = Sector.published.excluding_other.order(:name).to_a +other_sector = Sector.published.find_by(name: Sector::OTHER_SECTOR_NAME) +age_categories = Category.age_ranges.published.order(:position, :name).to_a + +form_name = "Training Registration Form (legacy primary sector name)" + +form = Form.standalone.find_by(name: form_name) +unless form + form = FormBuilderService.new( + name: form_name, + sections: %i[person_identifier professional_info], + role: "registration" + ).call + # Rename the canonical primary sector field onto its still-accepted legacy name. + form.form_fields.find_by(field_identifier: "primary_sector")&.update!(field_identifier: "primary_sector_single") +end + +# A demo registrant whose submission + tags back the form, so the profile and +# submission pages have data to render for this scheme. +person = Person.find_or_create_by!(email: "legacy.primary@example.com") do |p| + p.first_name = "Morgan" + p.last_name = "Legacyprimary" +end + +submission = FormSubmission.find_or_create_by!(form: form, person: person, role: "registration") + +primary_sector = concrete_sectors.first +additional_sectors = concrete_sectors.drop(1).first(2) +primary_age = age_categories.first +additional_ages = age_categories.drop(1).first(2) + +# Mirror how public registration stores the answers: a single id for the +# dropdowns, ", "-joined ids for the checkboxes, and a folded "Other: " for +# the additional sectors so the free-text path is represented too. +additional_sector_value = (additional_sectors.map(&:id) + + [ ("Other: Equine-assisted therapy" if other_sector) ].compact).join(", ") +{ + "primary_sector_single" => primary_sector&.id&.to_s, + "additional_sectors" => additional_sector_value.presence, + "primary_age_group" => primary_age&.id&.to_s, + "additional_age_group" => additional_ages.map(&:id).join(", ").presence +}.each do |identifier, value| + next if value.blank? + field = form.form_fields.find_by(field_identifier: identifier) + next unless field + next if submission.form_answers.where(form_field: field).any? + submission.form_answers.create!(form_field: field, + submitted_answer: value, + question_name_when_answered: field.name) +end + +# Tag the registrant with the same primary/additional split assign_tags applies, +# so the profile/edit pages crown the right primary and list the additional ones. +person.tag_sectors(primary_ids: [ primary_sector&.id ].compact, additional_ids: additional_sectors.map(&:id)) +person.tag_age_groups(primary_ids: [ primary_age&.id ].compact, additional_ids: additional_ages.map(&:id)) diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 4c3a81647b..476d7fa357 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -23,6 +23,7 @@ namespace :db do scholarships membership bulk_payments + legacy_form_identifiers public_forms ] @@ -124,6 +125,11 @@ namespace :db do load Rails.root.join("db/seeds/dev/bulk_payments.rb") end + desc "Seed a registration form using the legacy primary-sector identifier (dev only)" + task legacy_form_identifiers: :environment do + load Rails.root.join("db/seeds/dev/legacy_form_identifiers.rb") + end + desc "Seed standalone public forms with submissions and answers (dev only)" task public_forms: :environment do load Rails.root.join("db/seeds/dev/public_forms.rb") From cc537dfc5775ebea90a20b5998f9ebb94a403d81 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 21 Aug 2026 06:35:09 -0400 Subject: [PATCH 6/6] Remove the primary_sector_single legacy field identifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the legacy alias now that primary_sector is the canonical primary sector identifier. Merge only after prod form data is migrated from primary_sector_single → primary_sector (see the connected issue). With no legacy identifiers left, the legacy_form_identifiers dev seed and its rake task go too. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/models/form_field.rb | 10 ++-- app/services/smart_form_fields.rb | 3 +- db/seeds/dev/legacy_form_identifiers.rb | 77 ------------------------- lib/tasks/dev.rake | 6 -- 4 files changed, 5 insertions(+), 91 deletions(-) delete mode 100644 db/seeds/dev/legacy_form_identifiers.rb diff --git a/app/models/form_field.rb b/app/models/form_field.rb index 9d20f10dfb..6d1f37ffff 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -18,12 +18,10 @@ class FormField < ApplicationRecord # Multi-select "additional sectors" field identifier. ADDITIONAL_SECTOR_FIELD_IDENTIFIERS = %w[additional_sectors].freeze - # Single-select "primary sector" field identifiers. "primary_sector" is the - # canonical name; the legacy "primary_sector_single" is still accepted so - # existing forms keep resolving. Unlike the multi-select "additional" field, - # these omit the catch-all "Other" sector — a respondent's primary sector must - # be a concrete sector. - PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector primary_sector_single].freeze + # Single-select "primary sector" field identifier. Unlike the multi-select + # "additional" field, it omits the catch-all "Other" sector — a respondent's + # primary sector must be a concrete sector. + PRIMARY_SECTOR_FIELD_IDENTIFIERS = %w[primary_sector].freeze # Field identifiers whose selectable options are sourced dynamically from # Sector records rather than the field's own stored answer options. The diff --git a/app/services/smart_form_fields.rb b/app/services/smart_form_fields.rb index 2dc70139c0..15209d28bc 100644 --- a/app/services/smart_form_fields.rb +++ b/app/services/smart_form_fields.rb @@ -111,8 +111,7 @@ class SmartFormFields [ "primary_sector", "Primary sector", "Tags the person and organization with one primary sector. Offers no \"Other\" — a primary sector must be a real sector." ], [ "additional_sectors", "Additional sectors", "Tags the person and organization with any number of additional sectors. An \"Other\" answer goes to the Other responses review queue, where it can be promoted into a real sector." ], [ "primary_age_group", "Primary age group(s) served", "Tags the person and organization with the primary age range served." ], - [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ], - [ "primary_sector_single", "Primary sector (legacy)", "Older name for the primary sector question. Still honored; use primary_sector on new forms." ] + [ "additional_age_group", "Additional age group(s) served", "Tags the person and organization with additional age ranges served." ] ] }, { diff --git a/db/seeds/dev/legacy_form_identifiers.rb b/db/seeds/dev/legacy_form_identifiers.rb deleted file mode 100644 index 526b7f231e..0000000000 --- a/db/seeds/dev/legacy_form_identifiers.rb +++ /dev/null @@ -1,77 +0,0 @@ -# Legacy field-identifier registration form (dev-only). -# -# The professional sector questions still accept one *legacy* field_identifier — -# "primary_sector_single" for the single-select primary sector — so older forms -# keep working (see FormField::PRIMARY_SECTOR_FIELD_IDENTIFIERS). The canonical -# combination already ships as "Training Registration Form"; this extra standalone -# form carries the legacy primary identifier, with a demo registrant + submission -# + tags, so every surface (form rendering, the form submission show, and the -# registrant's profile/edit pages) can be eyeballed on real data. -# -# The additional sector field and both age-group fields have never been renamed, -# so they stay canonical. -# -# Loaded last in the dev seed order so its extra role: "registration" form can't -# shadow the canonical one that events_management.rb / scholarships.rb look up via -# `Form.standalone.find_by(role: "registration")`. Idempotent throughout. - -puts "Creating legacy field-identifier registration form…" - -# The dynamic option pools, mirroring the canonical lists. The single-select -# primary sector field omits the catch-all "Other"; the additional sector field -# keeps it (offered as a folded "Other: "). Age groups have no catch-all. -concrete_sectors = Sector.published.excluding_other.order(:name).to_a -other_sector = Sector.published.find_by(name: Sector::OTHER_SECTOR_NAME) -age_categories = Category.age_ranges.published.order(:position, :name).to_a - -form_name = "Training Registration Form (legacy primary sector name)" - -form = Form.standalone.find_by(name: form_name) -unless form - form = FormBuilderService.new( - name: form_name, - sections: %i[person_identifier professional_info], - role: "registration" - ).call - # Rename the canonical primary sector field onto its still-accepted legacy name. - form.form_fields.find_by(field_identifier: "primary_sector")&.update!(field_identifier: "primary_sector_single") -end - -# A demo registrant whose submission + tags back the form, so the profile and -# submission pages have data to render for this scheme. -person = Person.find_or_create_by!(email: "legacy.primary@example.com") do |p| - p.first_name = "Morgan" - p.last_name = "Legacyprimary" -end - -submission = FormSubmission.find_or_create_by!(form: form, person: person, role: "registration") - -primary_sector = concrete_sectors.first -additional_sectors = concrete_sectors.drop(1).first(2) -primary_age = age_categories.first -additional_ages = age_categories.drop(1).first(2) - -# Mirror how public registration stores the answers: a single id for the -# dropdowns, ", "-joined ids for the checkboxes, and a folded "Other: " for -# the additional sectors so the free-text path is represented too. -additional_sector_value = (additional_sectors.map(&:id) + - [ ("Other: Equine-assisted therapy" if other_sector) ].compact).join(", ") -{ - "primary_sector_single" => primary_sector&.id&.to_s, - "additional_sectors" => additional_sector_value.presence, - "primary_age_group" => primary_age&.id&.to_s, - "additional_age_group" => additional_ages.map(&:id).join(", ").presence -}.each do |identifier, value| - next if value.blank? - field = form.form_fields.find_by(field_identifier: identifier) - next unless field - next if submission.form_answers.where(form_field: field).any? - submission.form_answers.create!(form_field: field, - submitted_answer: value, - question_name_when_answered: field.name) -end - -# Tag the registrant with the same primary/additional split assign_tags applies, -# so the profile/edit pages crown the right primary and list the additional ones. -person.tag_sectors(primary_ids: [ primary_sector&.id ].compact, additional_ids: additional_sectors.map(&:id)) -person.tag_age_groups(primary_ids: [ primary_age&.id ].compact, additional_ids: additional_ages.map(&:id)) diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 476d7fa357..4c3a81647b 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -23,7 +23,6 @@ namespace :db do scholarships membership bulk_payments - legacy_form_identifiers public_forms ] @@ -125,11 +124,6 @@ namespace :db do load Rails.root.join("db/seeds/dev/bulk_payments.rb") end - desc "Seed a registration form using the legacy primary-sector identifier (dev only)" - task legacy_form_identifiers: :environment do - load Rails.root.join("db/seeds/dev/legacy_form_identifiers.rb") - end - desc "Seed standalone public forms with submissions and answers (dev only)" task public_forms: :environment do load Rails.root.join("db/seeds/dev/public_forms.rb")