diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html index f3bc2a142..4c24f8c51 100644 --- a/docs/_includes/nav.html +++ b/docs/_includes/nav.html @@ -8,6 +8,7 @@

Themes

Create a custom theme Images Citations + Funding Autocomplete

API

Configuration diff --git a/docs/guides/editor/funding-autocomplete.md b/docs/guides/editor/funding-autocomplete.md new file mode 100644 index 000000000..df3bad123 --- /dev/null +++ b/docs/guides/editor/funding-autocomplete.md @@ -0,0 +1,87 @@ +# Setting Up a Proxy for NSF Award API in MetacatUI + +MetacatUI integrates with the NSF (National Science Foundation) Award API to fetch award information. Since the NSF Award API does not support CORS (Cross-Origin Resource Sharing) or JSONP (JSON with Padding), it's necessary to set up a server-side proxy. This documentation guides you through setting up an Apache proxy to enable this functionality in MetacatUI. + +![NSF Award API Proxy](guides/images/funding.png) + +## Prerequisites + +- Apache Web Server +- Access to Apache configuration files +- MetacatUI already installed and served via Apache + +## Steps to Configure Apache as a Proxy + +### 1. Enable Required Apache Modules + +Ensure that the following Apache modules are enabled: + +- `mod_proxy` +- `mod_proxy_http` + +You can enable them by running the following commands: + +```bash +sudo a2enmod proxy +sudo a2enmod proxy_http +``` + +Then, restart Apache to apply the changes: + +```bash +sudo systemctl restart apache2 +``` + +### 2. Configure Apache Virtual Host + +Edit your Apache virtual host configuration file where MetacatUI is served. This file is typically located in `/etc/apache2/sites-available/`. + +Add the following configuration inside the `` block: + +```apache +# NSF Award API Proxy Configuration +ProxyPass "/research.gov/awardapi-service/v1/awards.json" "https://www.research.gov/awardapi-service/v1/awards.json" +ProxyPassReverse "/research.gov/awardapi-service/v1/awards.json" "https://www.research.gov/awardapi-service/v1/awards.json" +``` + +Replace `"https://www.research.gov/awardapi-service/v1/awards.json"` with the actual URL of the NSF Award API if it's different. You may also use a different proxy path if you prefer (other than `/research.gov/awardapi-service/v1/awards.json`), but make sure to update the proxy path in the `grantsUrl` property of the MetacatUI configuration as well. + +### 3. Restart Apache + +After editing the configuration file, restart Apache to apply the new settings: + +```bash +sudo systemctl restart apache2 +``` + +## Update MetacatUI Configuration + +The last step is to update the MetacatUI configuration to use the proxy path, if you used a different proxy path than the default one. The default path is `/research.gov/awardapi-service/v1/awards.json`, relative to the domain on which your MetacatUI is served. If you used a different proxy path, you will need to update the `grantsUrl` property in the MetacatUI configuration: + +```javascript +grantsUrl: "/research.gov/awardapi-service/v1/awards.json"; +``` + +Ensure that you also have the funding lookup feature enabled in the MetacatUI configuration by setting the `fundingLookup` property to `true`. It defaults to `false`. + +```javascript +useNSFAwardAPI: true; +``` + +## Testing + +Ensure that the proxy is correctly set up by accessing the following URL in your browser: + +``` +[your MetacatUI domain]/research.gov/awardapi-service/v1/awards.json +``` + +You should see the JSON response from the NSF Award API. + +## Conclusion + +By following these steps, you set up an Apache proxy to enable the NSF award lookup feature in MetacatUI. Ensure you test the configuration to confirm everything is working as expected. + +## Additional Notes + +- Each MetacatUI installation that wants to use the NSF award lookup feature will need to set up its own proxy. \ No newline at end of file diff --git a/docs/guides/images/funding.png b/docs/guides/images/funding.png new file mode 100644 index 000000000..531eaa9c8 Binary files /dev/null and b/docs/guides/images/funding.png differ diff --git a/docs/guides/index.md b/docs/guides/index.md index 868e856a5..44e1f8780 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -8,5 +8,6 @@ of your MetacatUI application. - 📑 Catalog Search View - 🌎 Cesium Map - 📍 Cesium Map for Portals +- 🔍 Funding Autocomplete ℹī¸ Is something missing? [Email us](mailto:metacat-dev@ecoinformatics.org) or join us on [Slack](https://slack.dataone.org/) and we'll add it! \ No newline at end of file diff --git a/src/css/metacatui-common.css b/src/css/metacatui-common.css index f786db5ce..6b167e72d 100644 --- a/src/css/metacatui-common.css +++ b/src/css/metacatui-common.css @@ -8322,11 +8322,11 @@ textarea.medium{ position: relative; } .Editor .funding-container .icon-spinner{ + position: absolute; + left: 11px; top: 5px; - left: 45px; - position: absolute; - font-size: 1.5em; - display: none; + font-size: 1.5em; + display: none; } .metadata-container #funding-visible{ padding-left: 30px; diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index 498b5e496..8701b1f18 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -720,11 +720,13 @@ define(['jquery', 'underscore', 'backbone'], useNSFAwardAPI: false, /** * The URL for the NSF Award API, which can be used by the {@link LookupModel} - * to look up award information for the dataset editor or other views + * to look up award information for the dataset editor or other views. The + * URL must point to a proxy that can make requests to the NSF Award API, + * since it does not support CORS. * @type {string} - * @default "https://api.nsf.gov/services/v1/awards.json" + * @default "/research.gov/awardapi-service/v1/awards.json" */ - grantsUrl: "https://api.nsf.gov/services/v1/awards.json", + grantsUrl: "/research.gov/awardapi-service/v1/awards.json", /** * The base URL for the ORCID REST services diff --git a/src/js/models/LookupModel.js b/src/js/models/LookupModel.js index 8ef1e1745..d65573b4e 100644 --- a/src/js/models/LookupModel.js +++ b/src/js/models/LookupModel.js @@ -1,575 +1,637 @@ /*global define */ -define(['jquery', 'jqueryui', 'underscore', 'backbone'], - function($, $ui, _, Backbone) { - 'use strict'; - - /** - * @class LookupModel - * @classdesc A uttility model that contains functions for looking up values from various services - * @classcategory Models - */ - var LookupModel = Backbone.Model.extend( - /** @lends LookupModel.prototype */{ - defaults: { - concepts: {} - }, - - initialize: function() { - - - - }, - - bioportalSearch: function(request, response, localValues, allValues) { - - // make sure we have something to lookup - if (!MetacatUI.appModel.get('bioportalAPIKey')) { - response(localValues); - return; - } - - var query = MetacatUI.appModel.get('bioportalSearchUrl') + - "?q=" + request.term + - "&apikey=" + MetacatUI.appModel.get("bioportalAPIKey") + - "&ontologies=ECSO&pagesize=1000&suggest=true"; - var availableTags = []; - $.get(query, function(data, textStatus, xhr) { - - _.each(data.collection, function(obj) { - var choice = {}; - choice.label = obj['prefLabel']; - var synonyms = obj['synonym']; - if (synonyms) { - choice.synonyms = []; - _.each(synonyms, function(synonym) { - choice.synonyms.push(synonym); - }); - } - choice.filterLabel = obj['prefLabel']; - choice.value = obj['@id']; - if (obj['definition']) { - choice.desc = obj['definition'][0]; - } - - // mark items that we know we have matches for - if (allValues) { - var matchingChoice = _.findWhere(allValues, {value: choice.value}); - if (matchingChoice) { - //choice.label = "*" + choice.label; - choice.match = true; - - // remove it from the local value - why have two? - if (localValues) { - localValues = _.reject(localValues, function(obj) { - return obj.value == matchingChoice.value; - }); - } - //availableTags.push(choice); - } - } - - availableTags.push(choice); - - }); - - // combine the lists if called that way - if (localValues) { - availableTags = localValues.concat(availableTags); - } - - response(availableTags); - - }); - }, - - bioportalExpand: function(term) { - - // make sure we have something to lookup - if (!MetacatUI.appModel.get('bioportalAPIKey')) { - response(null); - return; - } - - var terms = []; - var countdown = 0; - - var query = MetacatUI.appModel.get('bioportalSearchUrl') + - "?q=" + term + - "&apikey=" + MetacatUI.appModel.get("bioportalAPIKey") + - "&ontologies=ECSO&pagesize=1000&suggest=true";; - $.ajax( - { - url: query, - method: "GET", - async: false, // we want to wait for the response! - success: function(data, textStatus, xhr) { - - _.each(data.collection, function(obj) { - // use the preferred label - var prefLabel = obj['prefLabel']; - terms.push(prefLabel); - - // add the synonyms - var synonyms = obj['synonym']; - if (synonyms) { - _.each(synonyms, function(synonym) { - terms.push(synonym); - }); - } - // process the descendants - var descendantsUrl = obj['links']['descendants']; - //if (false) { - if (descendantsUrl && countdown > 0) { - - countdown--; - - $.ajax( - { - url: descendantsUrl + "?apikey=" + MetacatUI.appModel.get("bioportalAPIKey"), - method: "GET", - async: false, - success: function(data, textStatus, xhr) { - _.each(data.collection, function(obj) { - var prefLabel = obj['prefLabel']; - var synonyms = obj['synonym']; - if (synonyms) { - _.each(synonyms, function(synonym) { - terms.push(synonym); - }); - } - }); - } - }); - } - }); - } - }); - return terms; - }, - - bioportalGetConcepts: function(uri, callback) { - - var concepts = this.get('concepts')[uri]; - - if (concepts) { - callback(concepts); - return; - } else { - concepts = []; - } - - // make sure we have something to lookup - if (!MetacatUI.appModel.get('bioportalAPIKey')) { - return; - } - - var query = MetacatUI.appModel.get('bioportalSearchUrl') + - "?q=" + encodeURIComponent(uri) + - "&apikey=" + MetacatUI.appModel.get("bioportalAPIKey") + - "&ontologies=ECSO&pagesize=1000&suggest=true"; - var availableTags = []; - var model = this; - $.get(query, function(data, textStatus, xhr) { - - _.each(data.collection, function(obj) { - var concept = {}; - concept.label = obj['prefLabel']; - concept.value = obj['@id']; - if (obj['definition']) { - concept.desc = obj['definition'][0]; - } - // add the synonyms - var synonyms = obj['synonym']; - if (synonyms) { - concept.synonyms = []; - _.each(synonyms, function(synonym) { - concept.synonyms.push(synonym); - }); - } - - concepts.push(concept); - - }); - model.get('concepts')[uri] = concepts; - - callback(concepts); - }); - }, - - bioportalGetConceptsBatch: function(uris, callback) { - - // make sure we have something to lookup - if (!MetacatUI.appModel.get('bioportalBatchUrl')) { - return; - } - // prepare the request JSON - var batchData = {}; - batchData["http://www.w3.org/2002/07/owl#Class"] = {}; - batchData["http://www.w3.org/2002/07/owl#Class"]["display"] = "prefLabel,synonym,definition"; - batchData["http://www.w3.org/2002/07/owl#Class"]["collection"] = []; - _.each(uris, function(uri) { - var item = {}; - item["class"] = uri; - item["ontology"] = "http://data.bioontology.org/ontologies/ECSO"; - batchData["http://www.w3.org/2002/07/owl#Class"]["collection"].push(item); - }); - - var url = MetacatUI.appModel.get('bioportalBatchUrl'); - var model = this; - $.ajax(url, - { - method: "POST", - //url: url, - data: JSON.stringify(batchData), - contentType: "application/json", - headers: { - "Authorization": "apikey token="+ MetacatUI.appModel.get("bioportalAPIKey") - }, - error: function(e) { - console.log(e); - }, - success: function(data, textStatus, xhr) { - - _.each(data["http://www.w3.org/2002/07/owl#Class"], function(obj) { - var concept = {}; - concept.label = obj['prefLabel']; - concept.value = obj['@id']; - if (obj['definition']) { - concept.desc = obj['definition'][0]; - } - // add the synonyms - var synonyms = obj['synonym']; - if (synonyms) { - concept.synonyms = []; - _.each(synonyms, function(synonym) { - concept.synonyms.push(synonym); - }); - } - - var conceptList = []; - conceptList.push(concept); - model.get('concepts')[concept.value] = conceptList; - - }); - - callback.apply(); - } - }); - - }, - - orcidGetConcepts: function(uri, callback) { - - var people = this.get('concepts')[uri]; - - 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 - */ - orcidSearch: function(request, response, more, ignore) { - - var people = []; - - if(!ignore) var ignore = []; - - var query = MetacatUI.appModel.get('orcidSearchUrl') + request.term; - $.get(query, function(data, status, xhr) { - // get the orcid info - var profile = $(data).find("orcid-profile"); - - _.each(profile, function(obj) { - var choice = {}; - choice.value = $(obj).find("orcid-identifier > uri").text(); - - if(_.contains(ignore, choice.value.toLowerCase())) return; - - 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); - }); - - // add more if called that way - if (more) { - people = more.concat(people); - } - - // callback with answers - response(people); - }); - }, - - /* - * Gets the bio of a person given an ORCID - * Updates the given user model with the bio info from ORCID - */ - orcidGetBio: function(options){ - if(!options || !options.userModel) return; - - var orcid = options.userModel.get("username"), - onSuccess = options.success || function(){}, - onError = options.error || function(){}; - - $.ajax({ - url: MetacatUI.appModel.get("orcidSearchUrl") + orcid, - type: "GET", - //accepts: "application/orcid+json", - success: function(data, textStatus, xhr){ - // get the orcid info - var orcidNode = $(data).find("path:contains(" + orcid + ")"), - profile = orcidNode.length? $(orcidNode).parents("orcid-profile") : []; - - if(!profile.length) return; - - var fullName = $(profile).find("orcid-bio > personal-details > given-names").text() + " " + $(profile).find("orcid-bio > personal-details > family-name").text(); - options.userModel.set("fullName", fullName); - - onSuccess(data, textStatus, xhr); - }, - error: function(xhr, textStatus, error){ - onError(xhr, textStatus, error); - } - }); - }, - - getGrantAutocomplete: function(request, response){ - var term = $.ui.autocomplete.escapeRegex(request.term), - filterBy = ""; - - //Only search after 3 characters or more - if(term.length < 3) return; - else if(term.match(/\d/)) return; //Don't search for digit only since it's most likely a user just entering the grant number directy - else filterBy = "keyword"; - - var url = MetacatUI.appModel.get("grantsUrl") + "?" + filterBy + "=" + term + "&printFields=title,id"; - - // Send the AJAX request as a JSONP data type since it will be cross-origin - var requestSettings = { - url: url, - dataType: "jsonp", - success: function(data, textStatus, xhr) { - - // Handle the response from the NSF Award Search API and - //transform each award query result into a jQueryUI autocomplete item - - if(!data || !data.response || !data.response.award) return []; - - var list = []; - - _.each(data.response.award, function(award, i){ - list.push({ - value: award.id, - label: award.title - }); - }); - - var term = $.ui.autocomplete.escapeRegex(request.term) - , startsWithMatcher = new RegExp("^" + term, "i") - , startsWith = $.grep(list, function(value) { - return startsWithMatcher.test(value.label || value.value || value); - }) - , containsMatcher = new RegExp(term, "i") - , contains = $.grep(list, function (value) { - return $.inArray(value, startsWith) < 0 && - containsMatcher.test(value.label || value.value || value); - }); - - response(startsWith.concat(contains)); - } - } - - //Send the query - $.ajax(requestSettings); - }, - - getGrant: function(id, onSuccess, onError){ - if(!id || !onSuccess || !MetacatUI.appModel.get("useNSFAwardAPI") || !MetacatUI.appModel.get("grantsUrl")) return; - - var requestSettings = { - url: MetacatUI.appModel.get("grantsUrl") + "?id=" + id, - success: function(data, textStatus, xhr){ - if(!data || !data.response || !data.response.award || !data.response.award.length){ - if(onError) onError(); - return; - } - - onSuccess(data.response.award[0]); - }, - error: function(){ - if(onError) onError(); - } - } - - //Send the query - $.ajax(requestSettings); - }, - - getAccountsAutocomplete: function(request, response){ - var searchTerm = $.ui.autocomplete.escapeRegex(request.term); - - //Only search after 2 characters or more - if(searchTerm.length < 2) - return; - - var url = MetacatUI.appModel.get("accountsUrl") + "?query=" + searchTerm; - - // Send the AJAX request as a JSONP data type since it will be cross-origin - var requestSettings = { - url: url, - success: function(data, textStatus, xhr) { - - if(!data) - return []; - - //If an XML doc was not returned from the server, then try to parse the response as XML - if( !XMLDocument.prototype.isPrototypeOf(data) ){ - try{ - data = $.parseXML(data); +define(["jquery", "jqueryui", "underscore", "backbone"], function ( + $, + $ui, + _, + Backbone +) { + "use strict"; + + /** + * @class LookupModel + * @classdesc A utility model that contains functions for looking up values + * from various services + * @classcategory Models + */ + var LookupModel = Backbone.Model.extend( + /** @lends LookupModel.prototype */ { + defaults: { + concepts: {}, + }, + + initialize: function () {}, + + bioportalSearch: function (request, response, localValues, allValues) { + // make sure we have something to lookup + if (!MetacatUI.appModel.get("bioportalAPIKey")) { + response(localValues); + return; + } + + var query = + MetacatUI.appModel.get("bioportalSearchUrl") + + "?q=" + + request.term + + "&apikey=" + + MetacatUI.appModel.get("bioportalAPIKey") + + "&ontologies=ECSO&pagesize=1000&suggest=true"; + var availableTags = []; + $.get(query, function (data, textStatus, xhr) { + _.each(data.collection, function (obj) { + var choice = {}; + choice.label = obj["prefLabel"]; + var synonyms = obj["synonym"]; + if (synonyms) { + choice.synonyms = []; + _.each(synonyms, function (synonym) { + choice.synonyms.push(synonym); + }); } - catch(e){ - //If the parsing XML failed, exit now - console.error("The accounts service did not return valid XML.", e); - return; + choice.filterLabel = obj["prefLabel"]; + choice.value = obj["@id"]; + if (obj["definition"]) { + choice.desc = obj["definition"][0]; } + + // mark items that we know we have matches for + if (allValues) { + var matchingChoice = _.findWhere(allValues, { + value: choice.value, + }); + if (matchingChoice) { + //choice.label = "*" + choice.label; + choice.match = true; + + // remove it from the local value - why have two? + if (localValues) { + localValues = _.reject(localValues, function (obj) { + return obj.value == matchingChoice.value; + }); + } + //availableTags.push(choice); + } + } + + availableTags.push(choice); + }); + + // combine the lists if called that way + if (localValues) { + availableTags = localValues.concat(availableTags); } - var list = []; + response(availableTags); + }); + }, - _.each($(data).children(/.+subjectInfo/).children(), function(accountNode, i){ + bioportalExpand: function (term) { + // make sure we have something to lookup + if (!MetacatUI.appModel.get("bioportalAPIKey")) { + response(null); + return; + } - var name = ""; - var type = ""; + var terms = []; + var countdown = 0; + + var query = + MetacatUI.appModel.get("bioportalSearchUrl") + + "?q=" + + term + + "&apikey=" + + MetacatUI.appModel.get("bioportalAPIKey") + + "&ontologies=ECSO&pagesize=1000&suggest=true"; + $.ajax({ + url: query, + method: "GET", + async: false, // we want to wait for the response! + success: function (data, textStatus, xhr) { + _.each(data.collection, function (obj) { + // use the preferred label + var prefLabel = obj["prefLabel"]; + terms.push(prefLabel); + + // add the synonyms + var synonyms = obj["synonym"]; + if (synonyms) { + _.each(synonyms, function (synonym) { + terms.push(synonym); + }); + } + // process the descendants + var descendantsUrl = obj["links"]["descendants"]; + //if (false) { + if (descendantsUrl && countdown > 0) { + countdown--; + + $.ajax({ + url: + descendantsUrl + + "?apikey=" + + MetacatUI.appModel.get("bioportalAPIKey"), + method: "GET", + async: false, + success: function (data, textStatus, xhr) { + _.each(data.collection, function (obj) { + var prefLabel = obj["prefLabel"]; + var synonyms = obj["synonym"]; + if (synonyms) { + _.each(synonyms, function (synonym) { + terms.push(synonym); + }); + } + }); + }, + }); + } + }); + }, + }); + return terms; + }, + + bioportalGetConcepts: function (uri, callback) { + var concepts = this.get("concepts")[uri]; + + if (concepts) { + callback(concepts); + return; + } else { + concepts = []; + } - if( $(accountNode).children("givenName").length ){ - name = $(accountNode).children("givenName").text() + " " + $(accountNode).children("familyName").text() - type = "person" - } - else{ - name = $(accountNode).children("groupName").text(); - type = "group" - } + // make sure we have something to lookup + if (!MetacatUI.appModel.get("bioportalAPIKey")) { + return; + } - if( !name ){ - name = $(accountNode).children("subject").text(); - type = "unknown" + var query = + MetacatUI.appModel.get("bioportalSearchUrl") + + "?q=" + + encodeURIComponent(uri) + + "&apikey=" + + MetacatUI.appModel.get("bioportalAPIKey") + + "&ontologies=ECSO&pagesize=1000&suggest=true"; + var availableTags = []; + var model = this; + $.get(query, function (data, textStatus, xhr) { + _.each(data.collection, function (obj) { + var concept = {}; + concept.label = obj["prefLabel"]; + concept.value = obj["@id"]; + if (obj["definition"]) { + concept.desc = obj["definition"][0]; + } + // add the synonyms + var synonyms = obj["synonym"]; + if (synonyms) { + concept.synonyms = []; + _.each(synonyms, function (synonym) { + concept.synonyms.push(synonym); + }); } - list.push({ - value: $(accountNode).children("subject").text(), - label: name + " (" + $(accountNode).children("subject").text() + ")", - type: type - }); + concepts.push(concept); }); + model.get("concepts")[uri] = concepts; - var term = $.ui.autocomplete.escapeRegex(request.term) - , startsWithMatcher = new RegExp("^" + term, "i") - , startsWith = $.grep(list, function(value) { - return startsWithMatcher.test(value.label || value.value || value); - }) - , containsMatcher = new RegExp(term, "i") - , contains = $.grep(list, function (value) { - return $.inArray(value, startsWith) < 0 && - containsMatcher.test(value.label || value.value || value); + callback(concepts); + }); + }, + + bioportalGetConceptsBatch: function (uris, callback) { + // make sure we have something to lookup + if (!MetacatUI.appModel.get("bioportalBatchUrl")) { + return; + } + // prepare the request JSON + var batchData = {}; + batchData["http://www.w3.org/2002/07/owl#Class"] = {}; + batchData["http://www.w3.org/2002/07/owl#Class"]["display"] = + "prefLabel,synonym,definition"; + batchData["http://www.w3.org/2002/07/owl#Class"]["collection"] = []; + _.each(uris, function (uri) { + var item = {}; + item["class"] = uri; + item["ontology"] = "http://data.bioontology.org/ontologies/ECSO"; + batchData["http://www.w3.org/2002/07/owl#Class"]["collection"].push( + item + ); + }); + + var url = MetacatUI.appModel.get("bioportalBatchUrl"); + var model = this; + $.ajax(url, { + method: "POST", + //url: url, + data: JSON.stringify(batchData), + contentType: "application/json", + headers: { + Authorization: + "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"), + }, + error: function (e) { + console.log(e); + }, + success: function (data, textStatus, xhr) { + _.each(data["http://www.w3.org/2002/07/owl#Class"], function (obj) { + var concept = {}; + concept.label = obj["prefLabel"]; + concept.value = obj["@id"]; + if (obj["definition"]) { + concept.desc = obj["definition"][0]; + } + // add the synonyms + var synonyms = obj["synonym"]; + if (synonyms) { + concept.synonyms = []; + _.each(synonyms, function (synonym) { + concept.synonyms.push(synonym); + }); + } + + var conceptList = []; + conceptList.push(concept); + model.get("concepts")[concept.value] = conceptList; }); - response(startsWith.concat(contains)); + callback.apply(); + }, + }); + }, + + orcidGetConcepts: function (uri, callback) { + var people = this.get("concepts")[uri]; + + if (people) { + callback(people); + return; + } else { + people = []; } - } - //Send the query - $.ajax(requestSettings); - }, + 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); + }); - /** - * Calls the monitor/status DataONE MN API and gets the size of the index queue. - * @param {function} [onSuccess] - * @param {function} [onError] - */ - getSizeOfIndexQueue: function(onSuccess, onError){ + model.get("concepts")[uri] = people; - try{ + // callback with answers + callback(people); + }); + }, - if( !MetacatUI.appModel.get("monitorStatusUrl") ){ - if( typeof onSuccess == "function" ){ - onSuccess(); - } - else{ - //Trigger a custom event for the size of the index queue. - this.trigger("sizeOfQueue", -1); + /* + * Supplies search results for ORCiDs to autocomplete UI elements + */ + orcidSearch: function (request, response, more, ignore) { + var people = []; + + if (!ignore) var ignore = []; + + var query = MetacatUI.appModel.get("orcidSearchUrl") + request.term; + $.get(query, function (data, status, xhr) { + // get the orcid info + var profile = $(data).find("orcid-profile"); + + _.each(profile, function (obj) { + var choice = {}; + choice.value = $(obj).find("orcid-identifier > uri").text(); + + if (_.contains(ignore, choice.value.toLowerCase())) return; + + 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); + }); + + // add more if called that way + if (more) { + people = more.concat(people); } - return false; - } + // callback with answers + response(people); + }); + }, - var model = this; + /* + * Gets the bio of a person given an ORCID Updates the given user model + * with the bio info from ORCID + */ + orcidGetBio: function (options) { + if (!options || !options.userModel) return; - //Check if there is an indexing queue, because this model may still be indexing - var requestSettings = { - url: MetacatUI.appModel.get("monitorStatusUrl"), + var orcid = options.userModel.get("username"), + onSuccess = options.success || function () {}, + onError = options.error || function () {}; + + $.ajax({ + url: MetacatUI.appModel.get("orcidSearchUrl") + orcid, type: "GET", - error: function(){ - if( typeof onError == "function" ){ - onError(); - } + //accepts: "application/orcid+json", + success: function (data, textStatus, xhr) { + // get the orcid info + var orcidNode = $(data).find("path:contains(" + orcid + ")"), + profile = orcidNode.length + ? $(orcidNode).parents("orcid-profile") + : []; + + if (!profile.length) return; + + var fullName = + $(profile) + .find("orcid-bio > personal-details > given-names") + .text() + + " " + + $(profile) + .find("orcid-bio > personal-details > family-name") + .text(); + options.userModel.set("fullName", fullName); + + onSuccess(data, textStatus, xhr); }, - success: function(data){ + error: function (xhr, textStatus, error) { + onError(xhr, textStatus, error); + }, + }); + }, + + /** + * Using the NSF Award Search API, get a list of grants that match the + * given term, as long as the term is at least 3 characters long and + * doesn't consist of only digits. Used to populate the autocomplete list + * for the Funding fields in the metadata editor. For this method to work, + * there must be a grantsUrl set in the MetacatUI.appModel. + * @param {jQuery} request - The jQuery UI autocomplete request object + * @param {function} response - The jQuery UI autocomplete response function + * @see {@link https://www.research.gov/common/webapi/awardapisearch-v1.htm} + */ + getGrantAutocomplete: function ( + request, + response, + beforeRequest, + afterRequest + ) { + // Handle errors in this function or in the findGrants function + function handleError(error) { + if (typeof afterRequest == "function") afterRequest(); + console.log("Error fetching awards from NSF: ", error); + response([]); + } - var sizeOfQueue = parseInt($(data).find("status > index > sizeOfQueue").text()); + try { + let term = request.term; - if(sizeOfQueue > 0 || sizeOfQueue == 0){ - //Trigger a custom event for the size of the index queue. - model.trigger("sizeOfQueue", sizeOfQueue); + // Only search after 3 characters or more, and not just digits + if (!term || term.length < 3 || /^\d+$/.test(term)) return; + + // If the beforeRequest function was passed, call it now + if (typeof beforeRequest == "function") beforeRequest(); - if( typeof onSuccess == "function" ){ - onSuccess(sizeOfQueue); + // Search for grants + this.findGrants(term) + .then((awards) => { + response(this.formatFundingForAutocomplete(awards)); + }) + .catch(handleError) + .finally(() => { + if (typeof afterRequest == "function") afterRequest(); + }); + } catch (error) { + handleError(error); + } + }, + + /** + * Search the NSF Award Search API for grants that match the given term. + * @param {string} term - The term to search for + * @param {number} [offset=1] - The offset to use in the search. Defaults + * to 1. + * @returns {Promise} A promise that resolves to an array of awards in the + * format {id: string, title: string} + * @since x.x.x + */ + findGrants: function (term, offset = 1) { + let awards = []; + if (!term || term.length < 3) return awards; + const grantsUrl = MetacatUI.appModel.get("grantsUrl"); + if (!grantsUrl) return awards; + + term = $.ui.autocomplete.escapeRegex(term); + term = encodeURIComponent(term); + const filterBy = "keyword"; + const url = + `${grantsUrl}?${filterBy}=${term}` + + `&printFields=title,id&offset=${offset}`; + + return fetch(url) + .then((response) => { + return response.json(); + }) + .then((data) => { + if (!data || !data.response || !data.response.award) return awards; + return data.response.award; + }) + .catch((error) => { + console.error("Error fetching data: ", error); + return awards; + }); + }, + + /** + * Format awards from the NSF Award Search API for use in the jQuery UI + * autocomplete widget. + * @param {Array} awards - An array of awards in the format + * {id: string, title: string} + * @returns {Array} An array of awards in the format + * {value: string, label: string} + * @since x.x.x + */ + formatFundingForAutocomplete: function (awards) { + if (!awards || !awards.length) return []; + return awards.map((award) => ({ + value: award.id, + label: award.title, + })); + }, + + getAccountsAutocomplete: function (request, response) { + var searchTerm = $.ui.autocomplete.escapeRegex(request.term); + + //Only search after 2 characters or more + if (searchTerm.length < 2) return; + + var url = + MetacatUI.appModel.get("accountsUrl") + "?query=" + searchTerm; + + // Send the AJAX request as a JSONP data type since it will be + // cross-origin + var requestSettings = { + url: url, + success: function (data, textStatus, xhr) { + if (!data) return []; + + //If an XML doc was not returned from the server, then try to parse + //the response as XML + if (!XMLDocument.prototype.isPrototypeOf(data)) { + try { + data = $.parseXML(data); + } catch (e) { + //If the parsing XML failed, exit now + console.error( + "The accounts service did not return valid XML.", + e + ); + return; } } - else{ - if( typeof onError == "function" ){ - onError(); + + var list = []; + + _.each( + $(data) + .children(/.+subjectInfo/) + .children(), + function (accountNode, i) { + var name = ""; + var type = ""; + + if ($(accountNode).children("givenName").length) { + name = + $(accountNode).children("givenName").text() + + " " + + $(accountNode).children("familyName").text(); + type = "person"; + } else { + name = $(accountNode).children("groupName").text(); + type = "group"; + } + + if (!name) { + name = $(accountNode).children("subject").text(); + type = "unknown"; + } + + list.push({ + value: $(accountNode).children("subject").text(), + label: + name + + " (" + + $(accountNode).children("subject").text() + + ")", + type: type, + }); } + ); + + var term = $.ui.autocomplete.escapeRegex(request.term), + startsWithMatcher = new RegExp("^" + term, "i"), + startsWith = $.grep(list, function (value) { + return startsWithMatcher.test( + value.label || value.value || value + ); + }), + containsMatcher = new RegExp(term, "i"), + contains = $.grep(list, function (value) { + return ( + $.inArray(value, startsWith) < 0 && + containsMatcher.test(value.label || value.value || value) + ); + }); + + response(startsWith.concat(contains)); + }, + }; + + //Send the query + $.ajax(requestSettings); + }, + + /** + * Calls the monitor/status DataONE MN API and gets the size of the index + * queue. + * @param {function} [onSuccess] + * @param {function} [onError] + */ + getSizeOfIndexQueue: function (onSuccess, onError) { + try { + if (!MetacatUI.appModel.get("monitorStatusUrl")) { + if (typeof onSuccess == "function") { + onSuccess(); + } else { + //Trigger a custom event for the size of the index queue. + this.trigger("sizeOfQueue", -1); } + + return false; } - } - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - } - catch(e){ - console.error(e); + var model = this; - if( typeof onError == "function" ){ - onError(); + //Check if there is an indexing queue, because this model may still be + //indexing + var requestSettings = { + url: MetacatUI.appModel.get("monitorStatusUrl"), + type: "GET", + error: function () { + if (typeof onError == "function") { + onError(); + } + }, + success: function (data) { + var sizeOfQueue = parseInt( + $(data).find("status > index > sizeOfQueue").text() + ); + + if (sizeOfQueue > 0 || sizeOfQueue == 0) { + //Trigger a custom event for the size of the index queue. + model.trigger("sizeOfQueue", sizeOfQueue); + + if (typeof onSuccess == "function") { + onSuccess(sizeOfQueue); + } + } else { + if (typeof onError == "function") { + onError(); + } + } + }, + }; + + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings() + ) + ); + } catch (e) { + console.error(e); + + if (typeof onError == "function") { + onError(); + } } - - } + }, } - - }); - return LookupModel; + ); + return LookupModel; }); diff --git a/src/js/views/metadata/EML211View.js b/src/js/views/metadata/EML211View.js index 700595a1b..dead17758 100644 --- a/src/js/views/metadata/EML211View.js +++ b/src/js/views/metadata/EML211View.js @@ -1196,7 +1196,6 @@ define(['underscore', 'jquery', 'backbone', fundingInput.val(value); $(".funding .ui-helper-hidden-accessible").hide(); - loadingSpinner.css("top", "5px"); view.updateFunding(e); diff --git a/test/config/tests.json b/test/config/tests.json index 1fb5afcfa..7156a2e2e 100644 --- a/test/config/tests.json +++ b/test/config/tests.json @@ -42,5 +42,8 @@ "./js/specs/unit/models/maps/assets/CesiumGeohash.spec.js", "./js/specs/unit/common/Utilities.spec.js" ], - "integration": ["./js/specs/integration/collections/SolrResults.spec.js"] + "integration": [ + "./js/specs/integration/collections/SolrResults.spec.js", + "./js/specs/integration/models/LookupModel.js" + ] } diff --git a/test/js/specs/integration/models/LookupModel.js b/test/js/specs/integration/models/LookupModel.js new file mode 100644 index 000000000..29e7ba74f --- /dev/null +++ b/test/js/specs/integration/models/LookupModel.js @@ -0,0 +1,33 @@ +define(["../../../../../../../../src/js/models/LookupModel"], function ( + LookupModel +) { + // Configure the Chai assertion library + var should = chai.should(); + var expect = chai.expect; + + describe("Lookup Model", function () { + beforeEach(function () { + MetacatUI.appModel.set( + "grantsUrl", + "https://arcticdata.io/research.gov/awardapi-service/v1/awards.json" + ); + + }); + + afterEach(function () { + var lookup = null; + }); + + describe("NSF Awards API Lookup", function () { + + it("should return results for a valid term", async function () { + let lookup = new LookupModel(); + const awards = await lookup.findGrants("alaska"); + expect(awards).to.be.an("array"); + expect(awards.length).to.be.greaterThan(0); + expect(awards[0]).to.have.property("id"); + expect(awards[0]).to.have.property("title"); + }); + }); + }); +});