diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index bab9d6b1c..19d471bef 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -1,6 +1,9 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { "use strict"; + // Remove any trailing slashes from a URL + const NORMALIZE_URL = (url) => url.replace(/\/+$/, ""); + /** * @class AppModel * @classdesc A utility model that contains top-level configuration and storage for the application @@ -761,9 +764,9 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { /** * The base URL for the ORCID REST services * @type {string} - * @default "https:/orcid.org" + * @default "https://pub.orcid.org/" */ - orcidBaseUrl: "https:/orcid.org", + orcidBaseUrl: "https://pub.orcid.org/", /** * The URL for the ORCID search API, which can be used to search for information @@ -2577,13 +2580,13 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { d1CNBaseUrl + this.get("d1CNService") + this.get("formatsUrl"), ); } + } - //ORCID search - if (typeof this.get("orcidBaseUrl") != "undefined") - this.set( - "orcidSearchUrl", - this.get("orcidBaseUrl") + "/search/orcid-bio?q=", - ); + // ORCID search + const orcidBaseUrl = this.get("orcidBaseUrl"); + if (orcidBaseUrl) { + const searchUrl = `${NORMALIZE_URL(orcidBaseUrl)}/v3.0/expanded-search/?q=`; + this.set("orcidSearchUrl", searchUrl); } // Metadata quality report services diff --git a/src/js/models/LookupModel.js b/src/js/models/LookupModel.js index c07a3e8ae..6830033e2 100644 --- a/src/js/models/LookupModel.js +++ b/src/js/models/LookupModel.js @@ -264,77 +264,82 @@ define(["jquery", "jqueryui", "underscore", "backbone"], function ( }); }, - orcidGetConcepts: function (uri, callback) { - var people = this.get("concepts")[uri]; + /** @deprecated since 0.0.0 */ + orcidGetConcepts: function () {}, - if (people) { - callback(people); - return; - } else { - people = []; - } - - var query = - MetacatUI.appModel.get("orcidBaseUrl") + - uri.substring(uri.lastIndexOf("/")); - var model = this; - $.get(query, function (data, status, xhr) { - // get the orcid info - var profile = $(data).find("orcid-profile"); - - _.each(profile, function (obj) { - var choice = {}; - choice.label = - $(obj).find("orcid-bio > personal-details > given-names").text() + - " " + - $(obj).find("orcid-bio > personal-details > family-name").text(); - choice.value = $(obj).find("orcid-identifier > uri").text(); - choice.desc = $(obj).find("orcid-bio > personal-details").text(); - people.push(choice); - }); - - model.get("concepts")[uri] = people; - - // callback with answers - callback(people); - }); - }, - - /* + /** * Supplies search results for ORCiDs to autocomplete UI elements + * @param {jQuery} request - The jQuery UI autocomplete request object + * @param {function} response - The jQuery UI autocomplete response function + * @param {Array} [more=[]] - An array of additional results to include + * @param {Array} [ignore=[]] - An array of ORCiD IDs to ignore in the + * search results + * @param {number} [numResults=10] - The number of results to return + * @returns {Promise} */ - orcidSearch: function (request, response, more, ignore) { - var people = []; + async orcidSearch( + request, + response, + more = [], + ignore = [], + numResults = 10, + ) { + const baseUrl = MetacatUI.appModel.get("orcidSearchUrl"); + const searchTerm = request.term?.trim(); + const rowQuery = `&rows=${numResults || 10}&start=0`; - if (!ignore) var ignore = []; + if (!baseUrl || !searchTerm) return response(more); - var query = MetacatUI.appModel.get("orcidSearchUrl") + request.term; - $.get(query, function (data, status, xhr) { - // get the orcid info - var profile = $(data).find("orcid-profile"); + let ignoreQuery = ""; - _.each(profile, function (obj) { - var choice = {}; - choice.value = $(obj).find("orcid-identifier > uri").text(); + if (ignore.length > 0) { + const orcidRegex = /^(https?:\/\/orcid\.org\/)?/i; + // Expected format: +-orcid:(0000-0002-0879-455X+0000-0001-6238-4490) + const ignoreIds = ignore.map((id) => + id.replace(orcidRegex, "").trim(), + ); + ignoreQuery = `+-orcid:(${ignoreIds.join("+")})`; + } - if (_.contains(ignore, choice.value.toLowerCase())) return; + const url = `${baseUrl}${searchTerm}${ignoreQuery}${rowQuery}`; - choice.label = - $(obj).find("orcid-bio > personal-details > given-names").text() + - " " + - $(obj).find("orcid-bio > personal-details > family-name").text(); - choice.desc = $(obj).find("orcid-bio > personal-details").text(); - people.push(choice); - }); + let data; - // add more if called that way - if (more) { - people = more.concat(people); + try { + const orcidResponse = await fetch(url, { + headers: { "Content-Type": "application/json" }, + }); + if (!orcidResponse?.ok) { + const reason = await orcidResponse.text(); + throw new Error( + `ORCiD search request failed: ${orcidResponse.status} ${orcidResponse.statusText} - ${reason}`, + ); } + data = await orcidResponse.json(); + } catch (error) { + console.error("Error fetching ORCID data: ", error); + return response(more); + } - // callback with answers - response(people); + if ((data["num-found"] || 0) === 0) return response(more); + + const peopleFound = data["expanded-result"] || []; + + const choices = peopleFound.map((result) => { + const orcidId = result["orcid-id"]; + const givenNames = result["given-names"]; + const familyNames = result["family-names"]; + const institutionNames = result["institution-name"]; + return { + value: `https://orcid.org/${orcidId}`, + label: `${givenNames} ${familyNames}`, + desc: institutionNames.join(", "), + fullName: `${givenNames} ${familyNames}`, + }; }); + + const allResults = more ? more.concat(choices) : choices; + response(allResults); }, /** @deprecated since 2.36.0 */ diff --git a/test/js/specs/integration/models/LookupModel.js b/test/js/specs/integration/models/LookupModel.js index 5088aaff8..937b1dc51 100644 --- a/test/js/specs/integration/models/LookupModel.js +++ b/test/js/specs/integration/models/LookupModel.js @@ -1,6 +1,4 @@ -define(["../../../../../../../../src/js/models/LookupModel"], function ( - LookupModel, -) { +define(["models/LookupModel"], function (LookupModel) { // Configure the Chai assertion library var should = chai.should(); var expect = chai.expect; @@ -11,6 +9,11 @@ define(["../../../../../../../../src/js/models/LookupModel"], function ( "grantsUrl", "https://arcticdata.io/research.gov/awardapi-service/v1/awards.json", ); + MetacatUI.appModel.set("orcidBaseUrl", "https://pub.orcid.org"); + MetacatUI.appModel.set( + "orcidSearchUrl", + "https://pub.orcid.org/v3.0/search/?q=", + ); }); afterEach(function () { @@ -27,5 +30,82 @@ define(["../../../../../../../../src/js/models/LookupModel"], function ( expect(awards[0]).to.have.property("title"); }); }); + + describe("ORCID Search", function () { + let fetchStub; + + beforeEach(function () { + fetchStub = sinon.stub(window, "fetch"); + }); + + afterEach(function () { + if (fetchStub && fetchStub.restore) { + fetchStub.restore(); + } + }); + + it("formats ORCID v3 search results and respects the ignore list", async function () { + const lookup = new LookupModel(); + const request = { term: "Example" }; + const moreResults = [{ value: "existing-user" }]; + const ignore = [ + "https://orcid.org/0000-0000-0000-0001", + "0000-0000-0000-0002", + ]; + const numResults = 5; + + const apiResponse = { + "num-found": 1, + "expanded-result": [ + { + "orcid-id": "0000-0001-7648-6754", + "given-names": "Test", + "family-names": "User", + "institution-name": ["Org A", "Org B"], + }, + ], + }; + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: "OK", + json: () => Promise.resolve(apiResponse), + }); + + const responseSpy = sinon.spy(); + + await lookup.orcidSearch( + request, + responseSpy, + moreResults, + ignore, + numResults, + ); + + fetchStub.calledOnce.should.equal(true); + const callArgs = fetchStub.firstCall.args; + expect(callArgs[0]).to.equal( + "https://pub.orcid.org/v3.0/search/?q=Example+-orcid:(0000-0000-0000-0001+0000-0000-0000-0002)&rows=5&start=0", + ); + expect(callArgs[1]).to.have.property("headers"); + expect(callArgs[1].headers).to.deep.equal({ + "Content-Type": "application/json", + }); + + responseSpy.calledOnce.should.equal(true); + const results = responseSpy.firstCall.args[0]; + expect(results).to.be.an("array").with.lengthOf(2); + expect(results[0]).to.deep.equal(moreResults[0]); + + const newPerson = results[1]; + expect(newPerson.value).to.equal( + "https://orcid.org/0000-0001-7648-6754", + ); + expect(newPerson.label).to.equal("Test User"); + expect(newPerson.fullName).to.equal("Test User"); + expect(newPerson.desc).to.equal("Org A, Org B"); + }); + }); }); });