-
Notifications
You must be signed in to change notification settings - Fork 24
COORDINATE: Populate an org from the registrant's form on the linking page #1810
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
base: maebeale/organization-type-model
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -215,6 +215,12 @@ def select_organization | |
| @person = @event_registration.registrant | ||
| organization = Organization.find(params[:organization_id]) | ||
|
|
||
| # Apply the org details the form captured only when the admin opted in — linking an | ||
| # established org shouldn't silently rewrite its address/website. | ||
| entries = registration_submission_entries(@event_registration) | ||
| answers = submitted_agency_answers(entries, organization.name) | ||
| apply_submitted_org_details(organization, answers) if params[:apply_form_details] == "1" | ||
|
|
||
| AffiliationServices::CreateFromRegistration.call( | ||
| person: @person, | ||
| organization: organization, | ||
|
|
@@ -236,17 +242,33 @@ def create_organization | |
| # button can't be used to create an arbitrary org — it only resolves a pending | ||
| # submitted name. A specific name is passed when there are several submissions; | ||
| # otherwise default to the first submitted name. | ||
| submitted_names = submitted_agency_names(@event_registration) | ||
| entries = registration_submission_entries(@event_registration) | ||
| submitted_names = entries.filter_map { |entry| entry[:org_name].presence&.strip }.uniq { |name| name.downcase } | ||
| requested = params[:organization_name].presence | ||
| name = requested ? submitted_names.find { |submitted| submitted.casecmp?(requested) } : submitted_names.first | ||
| if name.blank? | ||
| redirect_to link_organization_event_registration_path(@event_registration, return_to: params[:return_to].presence), alert: "No submitted organization name to create from." | ||
| return | ||
| end | ||
|
|
||
| # Reuse an existing org with that name rather than creating a duplicate. | ||
| # 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" | ||
|
|
||
| # Reuse an existing org with that name rather than creating a duplicate. A new org is | ||
| # built from the form when populating, else created bare; an existing org is populated | ||
| # only when the toggle is on (never silently). | ||
| existing = Organization.where("LOWER(name) = ?", name.strip.downcase).first | ||
| organization = existing || Organization.create!(name: name.strip, organization_status: OrganizationStatus.find_by(name: "Active")) | ||
| organization = | ||
| if existing | ||
| existing | ||
| elsif apply | ||
| build_organization_from_answers(name, answers) | ||
| else | ||
| Organization.create!(name: name.strip, organization_status: OrganizationStatus.find_by(name: "Active")) | ||
| end | ||
| apply_submitted_org_details(organization, answers) if existing && apply | ||
|
|
||
| AffiliationServices::CreateFromRegistration.call( | ||
| person: @person, | ||
|
|
@@ -414,12 +436,15 @@ def registration_submission_entries(registration) | |
| entries | ||
| end | ||
|
|
||
| # Distinct, non-blank org names the registrant typed across their registration-form | ||
| # submissions (case-insensitive dedupe, first spelling wins). | ||
| def submitted_agency_names(registration) | ||
| registration_submission_entries(registration) | ||
| .filter_map { |entry| entry[:org_name].presence&.strip } | ||
| .uniq { |name| name.downcase } | ||
| # The agency answers (org details + position) the registrant submitted for this org, | ||
| # keyed by field_identifier. Uses the submission that named this org, else the first | ||
| # that named any org (registrants normally have a single submission), so a resolved | ||
| # org and affiliation can carry what they typed. {} when nothing was submitted. | ||
| 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
+443
to
448
Comment on lines
+443
to
448
|
||
|
|
||
| # The job title/position the registrant typed for their organization on the | ||
|
|
@@ -431,4 +456,108 @@ def submitted_position(registration) | |
| primary = entries.find { |entry| entry[:org_name].present? } || entries.first | ||
| primary && primary[:position] | ||
| end | ||
|
|
||
| # Apply the org info the registrant submitted to an org we already have, when the | ||
| # admin opts in (the apply_form_details checkbox). Website and organization type | ||
| # overwrite the org's values (each logged to Ahoy). The address is additive — a new | ||
| # record — unless one with the same street exists, which we refresh instead of duplicating. | ||
| def apply_submitted_org_details(organization, answers) | ||
| return if answers.blank? | ||
|
|
||
| overwrite_org_field(organization, :website_url, answers["agency_website"]) | ||
| overwrite_org_type(organization, answers["agency_type"]) | ||
| apply_submitted_address(organization, answers) | ||
| end | ||
|
|
||
| # Overwrite a single org column with the submitted value — an explicit admin opt-in — | ||
| # and log the change to Ahoy. No-op when the form had no value or it already matches. | ||
| def overwrite_org_field(organization, field, submitted) | ||
| value = submitted.to_s.strip | ||
| return if value.blank? || organization.public_send(field).to_s == value | ||
|
|
||
| previous = organization.public_send(field) | ||
| organization.update!(field => value) | ||
| Ahoy::Tracker.new(user: current_user).track( | ||
| "update.organization", | ||
| resource_type: "Organization", | ||
| resource_id: organization.id, | ||
| resource_title: organization.name, | ||
| change: field.to_s, | ||
| from: previous, | ||
| to: value, | ||
| reason: "registration_form_apply" | ||
| ) | ||
| end | ||
|
|
||
| # Overwrite the org's type from the submitted type name, resolving it to an | ||
| # OrganizationType record and logging the change to Ahoy. No-op when the form had no | ||
| # type, it names no known type, or it already matches. | ||
| def overwrite_org_type(organization, submitted) | ||
| type = submitted_organization_type(submitted) | ||
| return if type.nil? || organization.organization_type_id == type.id | ||
|
|
||
| previous = organization.organization_type&.name | ||
| organization.update!(organization_type: type) | ||
| Ahoy::Tracker.new(user: current_user).track( | ||
| "update.organization", | ||
| resource_type: "Organization", | ||
| resource_id: organization.id, | ||
| resource_title: organization.name, | ||
| change: "organization_type", | ||
| from: previous, | ||
| to: type.name, | ||
| reason: "registration_form_apply" | ||
| ) | ||
| end | ||
|
|
||
| # The OrganizationType matching a submitted type name (case-insensitive), or nil when | ||
| # blank or unrecognized. The form submits a type name from OrganizationType.published_names. | ||
| def submitted_organization_type(submitted) | ||
| name = submitted.to_s.strip | ||
| return if name.blank? | ||
|
|
||
| OrganizationType.where("LOWER(name) = ?", name.downcase).first | ||
| end | ||
|
|
||
| # Creates an Active org from a submitted name, carrying over the org-level details | ||
| # the registrant typed: type, website (stored as typed; see Organization#website_link_url), | ||
| # and a primary work address. | ||
| def build_organization_from_answers(name, answers) | ||
| organization = Organization.create!( | ||
| name: name.strip, | ||
| organization_status: OrganizationStatus.find_by(name: "Active"), | ||
| organization_type: submitted_organization_type(answers["agency_type"]), | ||
| website_url: answers["agency_website"].presence&.strip | ||
| ) | ||
| apply_submitted_address(organization, answers) | ||
| organization | ||
| end | ||
|
|
||
| # Add the submitted address as a new primary "work" record with an "Unknown" locality | ||
| # (the form doesn't capture one). Additive — but if the org already has an address with | ||
| # the same street, refresh that one (e.g. fill in a city it was missing) rather than | ||
| # duplicating it. The chosen record becomes primary and any others are demoted, leaving | ||
| # 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) | ||
|
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: 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. |
||
| city = answers["agency_city"].presence&.strip | ||
| state = answers["agency_state"].presence&.strip | ||
| return if city.blank? || state.blank? | ||
|
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: Address needs both city and state (the model validates presence of both + locality), so a partial address is skipped instead of raising — matching |
||
|
|
||
| street = answers["agency_street"].to_s.strip | ||
| zip = answers["agency_zip"].to_s.strip | ||
| match = street.present? && organization.addresses.find { |a| a.street_address.to_s.strip.casecmp?(street) } | ||
|
|
||
| address = if match | ||
| match.update!(city: city, state: state, zip_code: zip, primary: true, inactive: false) | ||
| match | ||
| else | ||
| organization.addresses.create!( | ||
| street_address: street, city: city, state: state, zip_code: zip, | ||
| locality: "Unknown", address_type: "work", primary: true | ||
| ) | ||
| end | ||
| organization.addresses.where.not(id: address.id).update_all(primary: false) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -234,8 +234,8 @@ def create_mailing_address(person) | |
|
|
||
| if existing | ||
| existing.update!( | ||
| street_address: field_value("mailing_street"), | ||
| zip_code: field_value("mailing_zip"), | ||
| street_address: field_value("mailing_street").to_s, | ||
| zip_code: field_value("mailing_zip").to_s, | ||
| primary: true, | ||
| inactive: false | ||
| ) | ||
|
|
@@ -246,10 +246,10 @@ def create_mailing_address(person) | |
| person.addresses.where(primary: true).update_all(primary: false, inactive: true) | ||
|
|
||
| person.addresses.create!( | ||
| street_address: field_value("mailing_street"), | ||
| street_address: field_value("mailing_street").to_s, | ||
| city: new_city, | ||
| state: new_state, | ||
| zip_code: field_value("mailing_zip"), | ||
| zip_code: field_value("mailing_zip").to_s, | ||
| country: field_value("mailing_country")&.strip, | ||
| locality: "Unknown", | ||
| address_type: field_value("mailing_address_type")&.downcase || "unknown", | ||
|
|
@@ -327,8 +327,8 @@ def create_agency_address(organization) | |
|
|
||
| if existing | ||
| existing.update!( | ||
| street_address: field_value("agency_street"), | ||
| zip_code: field_value("agency_zip"), | ||
| street_address: field_value("agency_street").to_s, | ||
|
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: 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. |
||
| zip_code: field_value("agency_zip").to_s, | ||
| primary: existing.primary? || make_primary, | ||
| inactive: false | ||
| ) | ||
|
|
@@ -337,10 +337,10 @@ def create_agency_address(organization) | |
| end | ||
|
|
||
| organization.addresses.create!( | ||
| street_address: field_value("agency_street"), | ||
| street_address: field_value("agency_street").to_s, | ||
| city: new_city, | ||
| state: new_state, | ||
| zip_code: field_value("agency_zip"), | ||
| zip_code: field_value("agency_zip").to_s, | ||
| country: field_value("agency_country")&.strip, | ||
| locality: "Unknown", | ||
| address_type: "work", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <%# Toggle: when checked, linking this org also applies the org info from the | ||
| registrant's form — overwrites the website and type, and adds the submitted address | ||
| as the new primary (refreshing a same-street one instead of duplicating). Defaults | ||
| on for a create-and-link or a suggested match (very likely the same org), off for the | ||
| manual search box (often an unrelated established org). The caller passes `checked`. | ||
| Lives inside each link form so it submits with that form without any JS. %> | ||
| <% checked = local_assigns.fetch(:checked, true) %> | ||
| <label class="flex items-center gap-2 text-xs text-gray-600 cursor-pointer"> | ||
| <%= check_box_tag :apply_form_details, "1", checked, class: "rounded border-gray-300 text-blue-900 focus:ring-blue-500" %> | ||
| Also populate this org's address, website, and type from the form answers | ||
| </label> |
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: 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.