diff --git a/app/controllers/contributors_controller.rb b/app/controllers/contributors_controller.rb index 410b95fd37..96a39f4664 100644 --- a/app/controllers/contributors_controller.rb +++ b/app/controllers/contributors_controller.rb @@ -114,13 +114,16 @@ def translate_roles(hash:) def process_org(hash:) return hash unless hash.present? && hash[:org_id].present? - allow = !Rails.configuration.x.application.restrict_orgs - org = org_from_params(params_in: hash, - allow_create: allow) + org = org_from_params(params_in: hash, allow_create: true) + + if org.nil? + flash[:alert] = + _('Contributor saved without affiliation. If you intended to add an affiliation, please check ' \ + 'if the organisation appears in the list in a different form.') + end hash = remove_org_selection_params(params_in: hash) - return hash if org.blank? && !allow return hash unless org.present? hash[:org_id] = org.id diff --git a/app/javascript/src/utils/autoComplete.js b/app/javascript/src/utils/autoComplete.js index 49f8b37d50..ecdb106fc6 100644 --- a/app/javascript/src/utils/autoComplete.js +++ b/app/javascript/src/utils/autoComplete.js @@ -1,6 +1,7 @@ import 'jquery-ui/ui/widgets/autocomplete'; import getConstant from './constants'; import { isObject, isString, isArray } from './isType'; +import debounce from '../utils/debounce'; // Updates the ARIA help text that lets the user know how many suggestions const updateAriaHelper = (autocomplete, suggestionCount) => { @@ -94,6 +95,12 @@ const toggleWarning = (autocomplete, displayIt) => { } }; +// Delayed warning display (fires only after typing pauses) +const debouncedToggleWarning = debounce((autocomplete, displayIt) => { + toggleWarning(autocomplete, displayIt); +}, 1000); + + // Looks up the value in the crosswalk const findInCrosswalk = (selection, crosswalk) => { // Default to the name only @@ -123,7 +130,11 @@ const warnableSelection = (selection) => { const handleSelection = (autocomplete, hidden, crosswalk, selection) => { const out = findInCrosswalk(selection, crosswalk); - toggleWarning(autocomplete, warnableSelection(out)); + // When user types, 'displayIt = false' hides warning initially + toggleWarning(autocomplete, false); + + // After user pauses for 1 second, show warning + debouncedToggleWarning(autocomplete, warnableSelection(out)); // Set the ID and trigger the onChange event for any view specific // JS to trigger events diff --git a/app/services/org_selection/hash_to_org_service.rb b/app/services/org_selection/hash_to_org_service.rb index a301d5faaa..6769363307 100644 --- a/app/services/org_selection/hash_to_org_service.rb +++ b/app/services/org_selection/hash_to_org_service.rb @@ -63,6 +63,13 @@ def to_identifiers(hash:) private + def match_hash_to_ror_org(hash:) + return nil unless hash[:ror].present? + + ror_results = OrgSelection::SearchService.search_externally(search_term: hash[:name]) + ror_results&.find { |r| r[:ror] == hash[:ror] } + end + # Lookup the Org by it's :id and return if the name matches the search def lookup_org_by_id(hash:) org = Org.where(id: hash[:id]).first if hash[:id].present? @@ -92,14 +99,18 @@ def lookup_org_by_name(hash:) def initialize_org(hash:) return nil unless hash.present? && hash[:name].present? + # Attempt to find an ROR match to the hash + ror_hash = match_hash_to_ror_org(hash: hash) + return nil unless ror_hash + Org.new( - name: hash[:name], - links: links_from_hash(name: hash[:name], website: hash[:url]), - language: language_from_hash(hash: hash), - target_url: hash[:url], + name: ror_hash[:name], + links: links_from_hash(name: ror_hash[:name], website: ror_hash[:url]), + language: language_from_hash(hash: ror_hash), + target_url: ror_hash[:url], institution: true, is_other: false, - abbreviation: abbreviation_from_hash(hash: hash) + abbreviation: abbreviation_from_hash(hash: ror_hash) ) end diff --git a/app/views/contributors/_form.html.erb b/app/views/contributors/_form.html.erb index 4c9ef30562..b70de31191 100644 --- a/app/views/contributors/_form.html.erb +++ b/app/views/contributors/_form.html.erb @@ -56,9 +56,8 @@ roles_tooltip = _("Select each role that applies to the contributor.")
- <%= render partial: org_partial, + <%= render partial:'shared/org_selectors/combined', locals: { form: form, - orgs: orgs, default_org: contributor.org, required: false, label: _("Affiliation") } %> diff --git a/app/views/shared/org_selectors/_combined.html.erb b/app/views/shared/org_selectors/_combined.html.erb index 1502d25661..0a589dde9f 100644 --- a/app/views/shared/org_selectors/_combined.html.erb +++ b/app/views/shared/org_selectors/_combined.html.erb @@ -47,5 +47,5 @@ placeholder = _("Begin typing to see a list of suggestions.") class: "autocomplete-result" %>

- <%= _("A new entry will be created for the organisation you have named above. Please double check that your organisation does not appear in the list in a slightly different form.").html_safe %> + <%= _("Please double check that your organisation does not appear in the list in a slightly different form.").html_safe %>

diff --git a/spec/controllers/contributors_controller_spec.rb b/spec/controllers/contributors_controller_spec.rb index 1545c6a901..71f1b218a2 100644 --- a/spec/controllers/contributors_controller_spec.rb +++ b/spec/controllers/contributors_controller_spec.rb @@ -134,6 +134,12 @@ end describe '#process_org(hash:)' do + before do + @request = ActionDispatch::TestRequest.create + @response = ActionDispatch::TestResponse.create + @controller.request = @request + @controller.response = @response + end it 'returns the hash as is if no :org_id is present' do @params_hash[:contributor].delete(:org_id) hash = @controller.send(:process_org, hash: @params_hash[:contributor]) @@ -159,6 +165,10 @@ end it 'sets the org_id to the idea of the org' do new_org = create(:org) + # Clear name, id, and ror to prevent matching any existing org + @params_hash[:contributor][:org_name] = nil + @params_hash[:contributor][:org_id] = { id: nil, name: nil, ror: nil }.to_json + @controller.stubs(:org_from_params).returns(new_org) hash = @controller.send(:process_org, hash: @params_hash[:contributor]) expect(hash[:org_id]).to eql(new_org.id) diff --git a/spec/services/org_selection/hash_to_org_service_spec.rb b/spec/services/org_selection/hash_to_org_service_spec.rb index 62e3794503..e58fb70dc1 100644 --- a/spec/services/org_selection/hash_to_org_service_spec.rb +++ b/spec/services/org_selection/hash_to_org_service_spec.rb @@ -44,8 +44,63 @@ org = create(:org, name: @name) expect(described_class.to_org(hash: @hash)).to eql(org) end - it 'returns a new Org instance' do - expect(described_class.to_org(hash: @hash).new_record?).to eql(true) + it 'returns a new Org instance when no existing DB matches exist but an ROR match does' do + ror_id = Faker::Alphanumeric.alphanumeric(number: 9) + + hash_with_ror = @hash.merge(ror: ror_id) + + # heartbeat + stub_request( + :get, + "#{ExternalApis::RorService.api_base_url}#{ExternalApis::RorService.heartbeat_path}" + ).with(headers: ExternalApis::RorService.headers) + .to_return(status: 200, body: '', headers: {}) + + # search + search_uri = + "#{ExternalApis::RorService.api_base_url}" \ + "#{ExternalApis::RorService.search_path}" \ + "?page=1&query=#{URI.encode_www_form_component(hash_with_ror[:name])}" + + ror_response = { + number_of_results: 1, + time_taken: 1, + items: [ + { + id: ror_id, + names: [ + { value: hash_with_ror[:name], types: ['ror_display'] }, + { value: hash_with_ror[:abbreviation], types: ['acronym'] } + ], + links: [{ type: 'website', value: hash_with_ror[:url] }], + types: ['Education'], + status: 'active', + locations: [ + { + geonames_id: 123, + geonames_details: { + country_name: 'United States', + country_code: 'US' + } + } + ], + external_ids: [] + } + ] + } + + stub_request(:get, search_uri) + .with(headers: ExternalApis::RorService.headers) + .to_return( + status: 200, + body: ror_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + org = described_class.to_org(hash: hash_with_ror) + + expect(org).not_to be_nil + expect(org).to be_new_record end end @@ -84,6 +139,39 @@ end context 'private methods' do + describe '#match_hash_to_ror_org' do + it 'returns nil if no ROR results are found' do + OrgSelection::SearchService.stubs(:search_externally) + .with(search_term: @hash[:name]) + .returns([]) + + result = described_class.send(:match_hash_to_ror_org, hash: @hash) + expect(result).to be_nil + end + + it 'returns the ROR-matching result when ror is present' do + ror_id = Faker::Alphanumeric.alphanumeric(number: 9) + + ror_results = [{ + ror: ror_id, + name: "#{@name} (#{@abbrev})", + sort_name: @name, + url: @url, + language: @lang.abbreviation, + fundref: '', + abbreviation: @abbrev, + score: @hash[:score], + weight: @hash[:weight] + }] + + OrgSelection::SearchService.stubs(:search_externally).returns(ror_results) + hash_with_ror = @hash.merge(ror: ror_id) + result = described_class.send(:match_hash_to_ror_org, hash: hash_with_ror) + + expect(result).to eq(ror_results.first) + end + end + describe '#initialize_org(hash:)' do it 'returns nil if the hash is nil' do rslt = described_class.send(:initialize_org, hash: nil) @@ -95,6 +183,21 @@ expect(rslt).to eql(nil) end it 'returns a new instance of Org' do + fake_ror = { + ror: Faker::Alphanumeric.alphanumeric(number: 9), + name: "#{@name} (#{@abbrev})", + sort_name: @name, + url: @url, + language: @lang.abbreviation, + fundref: '', + abbreviation: @abbrev, + score: @hash[:score], + weight: @hash[:weight] + } + # Stub the ROR matcher to return the fake ROR hash + # As valid ROR hash is needed for new org creation + described_class.stubs(:match_hash_to_ror_org).returns(fake_ror) + rslt = described_class.send(:initialize_org, hash: @hash) nm = "#{@name} (#{@abbrev})" lnks = JSON.parse({ org: [{ link: @url, text: nm }] }.to_json)