Skip to content

COORDINATE: Populate an org from the registrant's form on the linking page#1810

Open
maebeale wants to merge 1 commit into
maebeale/organization-type-modelfrom
maebeale/org-popup-form-data
Open

COORDINATE: Populate an org from the registrant's form on the linking page#1810
maebeale wants to merge 1 commit into
maebeale/organization-type-modelfrom
maebeale/org-popup-form-data

Conversation

@maebeale

@maebeale maebeale commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 overwrites org fields (website + type, Ahoy-logged) and adds/refreshes address records behind an opt-in; depends on #1886

Stacked on #1886 (base branch maebeale/organization-type-model). Merge #1886 first, then this retargets to main.

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.

  • Create and link (typed org not in the DB) — toggle on by default: builds/populates the new org with organization type, website (stored as typed), and a primary work address; off creates the org bare. (Existing name → links, populating only when on.)
  • Suggested matches (fuzzy + exact name matches) — toggle on by default: very likely the registrant's org, so populate it.
  • Manual search box — toggle off by default: often an unrelated established org, so link only unless the admin opts in.
  • Linking always creates the job + Facilitator affiliations (via 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?

  • One controller, gated on params[:apply_form_details]; the toggle is a shared partial rendered inside each link form with a per-form checked default (no JS).
  • Organization type is set via the new OrganizationType association from COORDINATE: Add OrganizationType base-data model backing org and registration forms #1886: the submitted type name is resolved to an OrganizationType (case-insensitive) and assigned; overwrites are Ahoy-logged.
  • Overwrites go through a shared overwrite_org_field / overwrite_org_type.
  • Website stored as typed per Accept bare domains for organization website URLs #1765 (Organization#website_link_url renders the link).
  • Fixes a pre-existing latent bug: PublicRegistration passed nil street/zip to NOT NULL columns, 500-ing when a registrant filled a city but no zip — now coerced to "".

Anything else to add?

  • Phone is intentionally not populated — the form has no org phone field (only the registrant's personal phone).

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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: The block only runs when the affiliation is newly created, so an existing affiliation's title is left untouched.

@maebeale maebeale marked this pull request as ready for review June 21, 2026 15:03
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|

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

@maebeale maebeale force-pushed the maebeale/org-popup-form-data branch from 9def672 to cce3e6c Compare June 21, 2026 21:39
# 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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

@maebeale maebeale changed the title Carry submitted org details into popup-created orgs WAIT: Carry submitted org details into popup-created orgs Jun 21, 2026
# 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"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

@maebeale maebeale force-pushed the maebeale/org-popup-form-data branch from 8d7f371 to 522e7e5 Compare June 22, 2026 03:53
Copilot AI review requested due to automatic review settings July 4, 2026 15:14
@maebeale maebeale force-pushed the maebeale/org-popup-form-data branch from 522e7e5 to 4f12690 Compare July 4, 2026 15:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_details into the create/link controller actions.
  • Implement org overwrite + Ahoy logging for website/type changes, and additive/refreshing address application logic.
  • Coerce street/zip to "" in PublicRegistration address 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.

Comment on lines +443 to 448
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
Comment on lines +499 to +500
agency_type: answers["agency_type"].presence,
website_url: answers["agency_website"].presence&.strip
Comment on lines +664 to +667
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
Comment on lines +674 to +695
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
@maebeale maebeale force-pushed the maebeale/org-popup-form-data branch from 4f12690 to 79482fe Compare July 4, 2026 15:57
@maebeale maebeale changed the title WAIT: Carry submitted org details into popup-created orgs Populate an org from the registrant's form on the linking page Jul 4, 2026
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>
Copilot AI review requested due to automatic review settings July 4, 2026 16:48
@maebeale maebeale force-pushed the maebeale/org-popup-form-data branch from 79482fe to b31e0d8 Compare July 4, 2026 16:48
@maebeale maebeale changed the base branch from main to maebeale/organization-type-model July 4, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comment on lines +443 to 448
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
Comment on lines +664 to +667
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"]')
@maebeale maebeale changed the title Populate an org from the registrant's form on the linking page COORDINATE: Populate an org from the registrant's form on the linking page Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants