COORDINATE: Populate an org from the registrant's form on the linking page#1810
COORDINATE: Populate an org from the registrant's form on the linking page#1810maebeale wants to merge 1 commit into
Conversation
| url = value.to_s.strip | ||
| return if url.blank? | ||
| url = "https://#{url}" unless url.match?(%r{\Ahttps?://}i) | ||
| url if url.match?(/\Ahttps?:\/\/\S+\z/i) |
There was a problem hiding this comment.
🤖 From Claude: A malformed website (e.g. with spaces) returns nil rather than raising, so a bad answer can never block the whole "Create and link". The regex matches Organization#website_url's own validation.
| def build_agency_address(organization, answers) | ||
| city = answers["agency_city"].presence&.strip | ||
| state = answers["agency_state"].presence&.strip | ||
| return if city.blank? || state.blank? |
There was a problem hiding this comment.
🤖 From Claude: Address needs both city and state (the model validates presence of both + locality), so a partial address is skipped instead of raising — matching PublicRegistration's hardcoded locality: "Unknown", address_type: "work".
|
|
||
| Affiliation.find_or_create_by!(person: @person, organization: organization) | ||
| Affiliation.find_or_create_by!(person: @person, organization: organization) do |affiliation| | ||
| affiliation.title = answers["agency_position"].presence |
There was a problem hiding this comment.
🤖 From Claude: The block only runs when the affiliation is newly created, so an existing affiliation's title is left untouched.
| Affiliation.find_or_create_by!(person: @person, organization: organization) | ||
| # Linking an existing org leaves its own details alone, but a new affiliation | ||
| # should still carry the role the registrant typed — same as the create path. | ||
| Affiliation.find_or_create_by!(person: @person, organization: organization) do |affiliation| |
There was a problem hiding this comment.
🤖 From Claude: Linking a matched/searched org leaves the org's own details alone, but a new affiliation now carries the submitted position (the block only runs on create, so an existing affiliation's title is preserved). Keeps this path consistent with create-and-link.
9def672 to
cce3e6c
Compare
| # Backfill onto an org we already have the details the registrant typed but the org | ||
| # is missing — only where its own value is blank, never overwriting. Covers type, | ||
| # website, and (when the org has no current address) a work address. | ||
| def fill_missing_org_details(organization, answers) |
There was a problem hiding this comment.
🤖 From Claude: Backfills only blank fields on an org we already have (matched/searched/reused), so an existing record is never overwritten — the address is added only when the org has no current one. Used by both select_organization and the reuse branch of create_organization.
| # Linking an established org shouldn't silently rewrite its canonical fields. | ||
| entries = registration_submission_entries(@event_registration) | ||
| answers = submitted_agency_answers(entries, organization.name) | ||
| fill_missing_org_details(organization, answers) if params[:apply_form_details] == "1" |
There was a problem hiding this comment.
🤖 From Claude: Opt-in gate (default off): linking an existing org only backfills its missing type/website/address when the admin checks the box, so an established org is never silently rewritten from one registrants form. The checkbox rides along in each link form — zero JS. Affiliation title still carries over unconditionally.
| return nil unless form | ||
| # Overwrite the org's website with the submitted one — an explicit admin opt-in — and | ||
| # log the change to Ahoy. No-op when the form had no website or it already matches. | ||
| def overwrite_org_website(organization, submitted) |
There was a problem hiding this comment.
🤖 From Claude: Website is the one field that truly overwrites (per request), so it logs an update.organization Ahoy event with from/to. Address adds a new primary (old one demoted, history kept); type only fills when blank. All three only run on the explicit opt-in.
| existing.update!( | ||
| street_address: field_value("agency_street"), | ||
| zip_code: field_value("agency_zip"), | ||
| street_address: field_value("agency_street").to_s, |
There was a problem hiding this comment.
🤖 From Claude: Latent-bug fix: street/zip are NOT NULL columns but the form leaves them optional, so a city-only address answer used to 500 with a NotNullViolation. .to_s stores "" instead of nil (the model only validates city/state/locality).
| # them active so prior addresses aren't lost. Skipped unless city and state are present | ||
| # (the Address model requires them); street_address and zip_code are NOT NULL columns, | ||
| # so a missing one is stored as "" rather than nil. | ||
| def apply_submitted_address(organization, answers) |
There was a problem hiding this comment.
🤖 From Claude: Address is additive (new primary) but de-duped by street — a same-street record is refreshed (city/zip filled in) rather than duplicated. Demoted prior addresses stay active so history is kept. Website + type instead overwrite via overwrite_org_field, each Ahoy-logged.
| # The other org details the registrant typed on the same submission, applied when the | ||
| # admin leaves the populate toggle on (checked by default for create-and-link). | ||
| answers = submitted_agency_answers(entries, name) | ||
| apply = params[:apply_form_details] == "1" |
There was a problem hiding this comment.
🤖 From Claude: Create-and-link now honors the populate toggle: on (default) builds the org from the form (or populates an existing same-name org); off creates it bare / links only. Suggested-match and create-and-link toggles default on in the view, the manual search box defaults off.
8d7f371 to
522e7e5
Compare
522e7e5 to
4f12690
Compare
There was a problem hiding this comment.
Pull request overview
This PR enhances the event registration “link organization” flow so the organization details a registrant typed on the registration form (website, type, and address) can be carried into newly-created organizations and optionally applied to existing organizations via an apply_form_details checkbox, while also fixing a latent NOT NULL failure when street/zip are blank.
Changes:
- Add an opt-in “populate details” toggle to org-linking forms and thread
apply_form_detailsinto the create/link controller actions. - Implement org overwrite + Ahoy logging for website/type changes, and additive/refreshing address application logic.
- Coerce street/zip to
""inPublicRegistrationaddress writes to avoid NOT NULL violations; add/extend request/service specs for the new behaviors.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
app/controllers/event_registrations_controller.rb |
Applies submitted org answers conditionally; adds overwrite + address application helpers and build-from-answers path. |
app/services/event_registration_services/public_registration.rb |
Coerces address street/zip values to strings to prevent NOT NULL errors. |
app/views/event_registrations/link_organization.html.erb |
Adds apply_form_details checkbox to create/link/suggested/manual link forms. |
app/views/event_registrations/_apply_form_details_checkbox.html.erb |
New shared partial for the populate toggle (default varies by form). |
spec/requests/event_registrations_spec.rb |
Adds request coverage for checkbox defaults + opt-in population/overwrite behavior. |
spec/services/event_registration_services/public_registration_spec.rb |
Adds regression coverage for blank street/zip NOT NULL violation avoidance. |
| def submitted_agency_answers(entries, name) | ||
| entry = entries.find { |e| e[:org_name].present? && e[:org_name].strip.casecmp?(name.to_s.strip) } || | ||
| entries.find { |e| e[:org_name].present? } || | ||
| entries.first | ||
| entry&.fetch(:submission)&.answers_by_identifier || {} | ||
| end |
| agency_type: answers["agency_type"].presence, | ||
| website_url: answers["agency_website"].presence&.strip |
| def populate_toggle_in_form(body, marker) | ||
| form = Nokogiri::HTML(body).css("form").find { |f| f.at_css(marker) } | ||
| form&.at_css('input[type="checkbox"][name="apply_form_details"]') | ||
| end |
| checkbox = populate_toggle_in_form(response.body, 'input[name="organization_name"]') | ||
| expect(checkbox).to be_present | ||
| expect(checkbox["checked"]).to be_present | ||
| end | ||
|
|
||
| it "checks the populate toggle by default on a suggested match" do | ||
| create(:organization, name: "Helpers United") | ||
| submit_form(org_name: "Helpers United") | ||
|
|
||
| get link_organization_event_registration_path(existing_registration) | ||
|
|
||
| checkbox = populate_toggle_in_form(response.body, 'input[type="hidden"][name="organization_id"]') | ||
| expect(checkbox).to be_present | ||
| expect(checkbox["checked"]).to be_present | ||
| end | ||
|
|
||
| it "leaves the populate toggle unchecked on the manual search form" do | ||
| get link_organization_event_registration_path(existing_registration) | ||
|
|
||
| checkbox = populate_toggle_in_form(response.body, 'select[name="organization_id"]') | ||
| expect(checkbox).to be_present | ||
| expect(checkbox["checked"]).to be_nil |
4f12690 to
79482fe
Compare
When linking/creating an org from a registrant's submission on the org-linking page, optionally apply the org info the registrant typed, gated by a per-form "populate" toggle (zero JS): - Create-and-link (new org) and suggested matches default the toggle on (very likely the registrant's org); the manual search box defaults off (often an unrelated established org). - When applying: website and organization type overwrite the org's values (each logged to Ahoy update.organization); the address is additive — added as a new primary, or a same-street record refreshed instead of duplicated; demoted addresses stay active. - Create-and-link with the toggle off creates the org bare; an existing same-name org is only linked unless the toggle is on. Organization type is set via the OrganizationType association (PR #1886): the submitted type name is resolved to an OrganizationType record (case-insensitive). Website stored as typed (#1765). Phone is not populated (no org phone field). Also fixes a latent bug: PublicRegistration passed nil street/zip to NOT NULL address columns, 500-ing when a registrant filled a city but no zip; coerce to "". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79482fe to
b31e0d8
Compare
| def submitted_agency_answers(entries, name) | ||
| entry = entries.find { |e| e[:org_name].present? && e[:org_name].strip.casecmp?(name.to_s.strip) } || | ||
| entries.find { |e| e[:org_name].present? } || | ||
| entries.first | ||
| entry&.fetch(:submission)&.answers_by_identifier || {} | ||
| end |
| def populate_toggle_in_form(body, marker) | ||
| form = Nokogiri::HTML(body).css("form").find { |f| f.at_css(marker) } | ||
| form&.at_css('input[type="checkbox"][name="apply_form_details"]') | ||
| end |
|
|
||
| get link_organization_event_registration_path(existing_registration) | ||
|
|
||
| checkbox = populate_toggle_in_form(response.body, 'input[name="organization_name"]') |
|
|
||
| get link_organization_event_registration_path(existing_registration) | ||
|
|
||
| checkbox = populate_toggle_in_form(response.body, 'input[type="hidden"][name="organization_id"]') |
| it "leaves the populate toggle unchecked on the manual search form" do | ||
| get link_organization_event_registration_path(existing_registration) | ||
|
|
||
| checkbox = populate_toggle_in_form(response.body, 'select[name="organization_id"]') |
🤖 suggested review level: 5 Inspect 🔬 overwrites org fields (website + type, Ahoy-logged) and adds/refreshes address records behind an opt-in; depends on #1886
What is the goal of this PR and why is this important?
Resolving a registrant's submitted org on the org-linking page used to save only the name. Now the form's org info flows through, with a per-form populate toggle controlling when an org is written from the form.
AffiliationServices::CreateFromRegistration), regardless of the toggle.When the toggle applies to an existing org: website + organization type overwrite the org's values (each logged to Ahoy
update.organization); the address is additive — a new primary, or a refresh of a same-street record instead of a duplicate; demoted prior addresses stay active.How did you approach the change?
params[:apply_form_details]; the toggle is a shared partial rendered inside each link form with a per-formcheckeddefault (no JS).OrganizationTypeassociation from COORDINATE: Add OrganizationType base-data model backing org and registration forms #1886: the submitted type name is resolved to anOrganizationType(case-insensitive) and assigned; overwrites are Ahoy-logged.overwrite_org_field/overwrite_org_type.Organization#website_link_urlrenders the link).PublicRegistrationpassednilstreet/zip to NOT NULL columns, 500-ing when a registrant filled a city but no zip — now coerced to"".Anything else to add?