diff --git a/src/js/mixins/papers_utils.js b/src/js/mixins/papers_utils.js index bad6b6965..4d67a0f67 100644 --- a/src/js/mixins/papers_utils.js +++ b/src/js/mixins/papers_utils.js @@ -137,7 +137,10 @@ define([ data.details = data.details || {shortAbstract: data.shortAbstract, pub: data.pub, abstract : data.abstract}; data.num_citations = data["[citations]"] ? data["[citations]"]["num_citations"] : undefined; data.identifier = data.bibcode; - data.encodedIdentifier = encodeURIComponent(data.identifier); + + // make sure undefined doesn't become "undefined" + data.encodedIdentifier = _.isUndefined(data.identifier) ? + data.identifier : encodeURIComponent(data.identifier); if (data.pubdate || data.shortAbstract){ data.popover = true; diff --git a/src/js/modules/orcid/extension.js b/src/js/modules/orcid/extension.js index d81169eed..9cd6fc35a 100644 --- a/src/js/modules/orcid/extension.js +++ b/src/js/modules/orcid/extension.js @@ -12,7 +12,8 @@ define([ 'js/components/api_query_updater', 'js/components/api_targets', 'js/modules/orcid/work', - 'js/mixins/dependon' + 'js/components/api_feedback', + 'js/mixins/dependon', ], function ( @@ -21,7 +22,8 @@ define([ ApiRequest, ApiQueryUpdater, ApiTargets, - Work + Work, + ApiFeedback ) { return function(WidgetClass) { @@ -140,6 +142,8 @@ define([ */ WidgetClass.prototype.addOrcidInfo = function (docs) { var self = this; + var isRetry = false; + // add orcid info to the documents var orcidApi = this.getBeeHive().getService('OrcidApi'); @@ -147,77 +151,130 @@ define([ return docs; } - var recInfo; - var counter = 0; + // find all pending models, and update them with an error state + var setPendingToError = _.debounce(function () { + + // go through the current models, and if something is pending + // make it show an error + _.forEach(self.hiddenCollection.models, function (m) { + var orcid = m.get('orcid'); + if (orcid.pending) { + orcid = _.extend({}, orcid, { + pending: false, + error: 'Error while applying Orcid Data' + }); + m.set('orcid', orcid); + } + }); + }, 100); - _.each(docs, function(d) { - recInfo = orcidApi.getRecordInfo(d); - if (recInfo.state() === 'pending') { - counter += 1; - recInfo.done(function(rInfo) { - counter -= 1; + // find all pending models, and update them to a default state + var setPendingToDefaultActions = _.debounce(function () { - var actions = self._getOrcidInfo(rInfo); + var defaultActions = self._getOrcidInfo({}); - // get the model for this document - if (self.hiddenCollection && self.hiddenCollection.findWhere) { - var model = self.hiddenCollection.findWhere({ bibcode: d.bibcode }); - if (model) { - model.set('orcid', actions); - } + // go through the current models, and if something is pending + // make it show an error + _.forEach(self.hiddenCollection.models, function (m) { + if (m.get('orcid') && m.get('orcid').pending) { + m.set('orcid', defaultActions); + } + }); + }, 100); - if (rInfo.children) { - _.forEach(rInfo.children, function (putcode) { - var childModel = _.find(self.hiddenCollection.models, function (m) { - return m.get('_work').getPutCode() === putcode; - }); + // attempt to find the model to update and update it's orcid actions + var onSuccess = function () { + _.forEach(_.toArray(arguments), function (info, i) { - if (childModel) { - self.removeModel(childModel); - } - }); - } - } + // since the order is maintain from the promises we can grab by index + var work = docs[i]; - if (counter === 0) { - self.trigger('orcid-update-finished'); - } + // make sure the doc has any information we gained + if (_.isString(info.bibcode) && !_.isUndefined(work.identifier)) { + work.identifier = info.bibcode; + } else if (_.isArray(info.doi) && !_.isUndefined(work.identifier)) { + work.identifier = info.doi; + } + + var model = _.find(self.hiddenCollection.models, function (m) { + + // do our best to find the match + return (_.isPlainObject(work._work) && work._work === m.get('_work')) + || (_.isString(work.bibcode) && work.bibcode === m.get('bibcode')) + || (_.isArray(work.doi) && work.doi === m.get('doi')) + || (!_.isUndefined(work.identifier) && work.identifier === m.get('identifier')); }); - recInfo.fail(function (data) { - counter -= 1; + // found the model, update it + if (model) { + var sources, orcidPath; + + // grab the array of sources, if it exists + if (_.isPlainObject(work._work)) { + sources = work._work.getSources(); + orcidPath = '//' + work._work.getSourceOrcidIdHost() + '/' + work._work.getPath(); + } + + // get the new set of actions, also set the source name + var actions = self._getOrcidInfo(info); + + model.set({ + orcid: actions, + source_name: _.isArray(sources) ? sources.join('; ') : model.get('source_name'), + orcidWorkPath: orcidPath + }); + } else { + _.defer(setPendingToDefaultActions); + } + + // if entry has children, remove them + if (_.isArray(info.children)) { + _.forEach(info.children, function (putcode) { + var model = _.find(self.hiddenCollection.models, function (m) { + return _.isString(putcode) && putcode === m.get('_work').getPutCode(); + }); - // very likely, the request timed out - // keep the actions and let user redo the operation - if (self.collection && self.collection.findWhere) { - var model = self.collection.findWhere({bibcode: d.bibcode}); if (model) { - var o = _.extend({}, model.get('orcid') || {}); - delete o.pending; - o.error = 'Orcid API reported error'; - model.set('orcid', o); //TODO: distinguish different types of errors + self.removeModel(model); + } else { + _.defer(setPendingToDefaultActions); } - } + }); + } + }); + }; - if (counter === 0) { - self.trigger('orcid-update-finished'); - } - }); - d.orcid = {pending: true}; - } - else { - recInfo.done(function(rInfo) { - d.orcid = self._getOrcidInfo(rInfo); - // enhance the ORCID record with an identifier - // the bibcode, if there, was discovered from our api - if (!d.identifier && rInfo.bibcode) { - d.identifier = rInfo.bibcode; - } else if (!d.identifier && rInfo.doi) { - d.identifier = rInfo.doi; - } - }); + // retry once, then just set everything still pending to errored + var onFail = function () { + + if (!isRetry) { + isRetry = true; + return getDocInfo(); } - }); + + // set everything pending to be an error + _.defer(setPendingToError); + }; + + // set all docs to pending and start the async doc info requests + var getDocInfo = function () { + var promises = []; + + _.each(docs, function (d) { + + // set the orcid prop to pending + d.orcid = _.extend({}, d.orcid, { pending: true }); + promises.push(orcidApi.getRecordInfo(d)); + }); + + // wait for all the promises to resolve + $.when.apply($, promises).then(onSuccess, onFail).always(function () { + self.trigger('orcid-update-finished'); + }); + }; + + // start the process + getDocInfo(); return docs; }; @@ -226,7 +283,7 @@ define([ var self = this; _.forEach(this.hiddenCollection.models, function (hiddenModel) { var match = _.find(self.collection.models, function (model) { - return model.get('bibcode') === hiddenModel.get('bibcode'); + return model.get('_work') === hiddenModel.get('_work'); }); if (match && match.get('orcid').pending) { @@ -249,14 +306,14 @@ define([ oApi.getUserProfile().done(function (profile) { var works = profile.getWorks(); _.forEach(modelsToUpdate, function (m) { - var exIds = _.pick(m.attributes, ['bibcode', 'doi']); + var exIds = _.flatten(_.values(_.pick(m.attributes, ['bibcode', 'doi', 'identifier']))); _.forEach(works, function (w) { - var wIds = w.getExternalIds(); - var doi = _.any(exIds.doi, wIds.doi); - - if (exIds.bibcode === wIds.bibcode || doi) { + var wIds = _.flatten(_.values(w.getExternalIds())); + var idMatch = _.intersection(exIds, wIds).length > 0; + + if ((_.isPlainObject(m._work) && m._work === w) || idMatch) { m.set({ - 'source_name': w.sources.join('; '), + 'source_name': w.getSources().join('; '), '_work': w }); } @@ -269,7 +326,7 @@ define([ docs = processDocs.apply(this, arguments); var user = this.getBeeHive().getObject('User'); //for results list only show if orcidModeOn, for orcid big widget show always - if (user && user.isOrcidModeOn() || this.orcidWidget ){ + if (user && user.isOrcidModeOn() || this.orcidWidget ) { var result = this.addOrcidInfo(docs); if (pagination.numFound !== result.length) { _.extend(pagination, this.getPaginationInfo(apiResponse, docs)); @@ -303,8 +360,13 @@ define([ var adsWork = adsResponse.response && adsResponse.response.docs && adsResponse.response.docs[0]; - model.attributes = _.extend(model.attributes, - fullOrcidWork.toADSFormat(), adsWork); + var parsedOrcidWork = fullOrcidWork.toADSFormat(); + + parsedOrcidWork = _.isPlainObject(parsedOrcidWork) ? parsedOrcidWork : {}; + adsWork = _.isPlainObject(adsWork) ? adsWork : {}; + + // extend the current model with our new information + model.set(_.extend({}, model.attributes, parsedOrcidWork, adsWork)); final.resolve(model); }; @@ -406,7 +468,7 @@ define([ var msg = 'Could not find a matching ORCiD Record'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); } @@ -417,7 +479,7 @@ define([ var msg = 'Error retrieving ORCiD profile'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); }; @@ -487,7 +549,7 @@ define([ model.set({ orcid: self._getOrcidInfo(recInfo), - 'source_name': work.sources.join('; ') + 'source_name': work.getSources().join('; ') }); self.trigger('orcidAction:' + action, model); @@ -507,7 +569,7 @@ define([ var msg = 'Failed to add entry, please try again'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); }) @@ -520,7 +582,7 @@ define([ var msg = 'There was a problem adding the record, try again'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); } @@ -558,7 +620,7 @@ define([ var msg = 'Error deleting record, please try again'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); }); @@ -575,13 +637,29 @@ define([ model.set('orcid', {pending: true}); + var failedUpdating = function () { + var msg = 'Error updating record, please try again'; + console.error.apply(console, [msg].concat(arguments)); + model.set('orcid', _.extend(oldOrcid, { + pending: false, + error: msg + })); + }; + self.mergeADSAndOrcidData(model) // done merging, begin update .done(function (model) { var putCode = model.get('_work').getPutCode(); - orcidApi.updateWork(Work.adsToOrcid(model.attributes, putCode)) + var work = Work.adsToOrcid(model.attributes, putCode); + if (_.isNull(work)) { + + // if something went wrong parsing the work, fail it here + return failedUpdating(work); + } + + orcidApi.updateWork(work) // update successful .done(function doneUpdating(orcidWork) { @@ -596,17 +674,10 @@ define([ }) // update failed, update model accordingly - .fail(function failedUpdating() { - var msg = 'Error updating record, please try again'; - console.error.apply(console, [msg].concat(arguments)); - model.set('orcid', _.extend(oldOrcid, { - pending: null, - error: msg - })); - }); + .fail(failedUpdating); // update the model with the updated data - model.set(model.attributes, {silent: true}); + model.set(model.attributes, { silent: true }); }) // merging failed, update the model @@ -614,7 +685,7 @@ define([ var msg = 'Failed to merge ORCiD and ADS data'; console.error.apply(console, [msg].concat(arguments)); model.set('orcid', _.extend(oldOrcid, { - pending: null, + pending: false, error: msg })); }); @@ -626,7 +697,13 @@ define([ orcidApi.signOut(); }, 'orcid-view': function (model) { - // do nothing for now + + // send them to the work on their orcid profile + var url = model.get('orcidWorkPath'); + if (_.isString(url)) { + var win = window.open(url, '_blank'); + win.focus(); + } } }; handlers[action] && handlers[action](data.model); diff --git a/src/js/modules/orcid/orcid_api.js b/src/js/modules/orcid/orcid_api.js index df09b64c0..f28aa5922 100644 --- a/src/js/modules/orcid/orcid_api.js +++ b/src/js/modules/orcid/orcid_api.js @@ -94,7 +94,7 @@ define([ this.db = {}; this.clearDBWait = 30000; this.dbUpdatePromise = null; - this.maxQuerySize = 50; + this.maxQuerySize = 100; this.queryUpdater = new ApiQueryUpdater('orcid_api'); this.orcidApiTimeout = 30000; // 30 seconds this.adsQueryTimeout = 10; // 10 seconds @@ -449,9 +449,9 @@ define([ } // set the work's list of sources based on the full list from orcid - w.sources = _.map(work, function (_w) { + w.setSources(_.map(work, function (_w) { return _w.getSourceName(); - }); + })); } // take the first work if we haven't found an array to process @@ -992,20 +992,18 @@ define([ } // reformat array as 'identifier:xxx OR identifier:xxx' - var q = _(query.identifier) - .map(function (v) { - return 'identifier:' + v; - }) - .filter('length') - .value() - .join(' OR '); + var q = _.filter(query.identifier, function (i) { + + // grab only non-empty entries + return !_.isEmpty(i.trim()) || i === 'NONE'; + }).join(' OR '); // don't let an empty query string through if (_.isEmpty(q)) { return null; } - return new ApiQuery({ q: q }); + return new ApiQuery({ q: 'identifier:(' + q + ')' }); }, /** @@ -1046,10 +1044,17 @@ define([ _.forEach(works, function addIdsToDatabase(w, i) { var key = 'identifier:'; var ids = w.getExternalIds(); - if (ids.bibcode) { + + if (_.has(ids, 'bibcode')) { key += ids.bibcode; - } else if (ids.doi) { + } else if (_.has(ids, 'doi')) { key += ids.doi; + } else if (_.has(ids, 'null')) { + key += 'NONE'; + } else if (!_.isEmpty(ids)) { + + // grab the first value + key += _.values(ids)[0]; } if (key) { @@ -1213,7 +1218,7 @@ define([ Then we can add some metadata like whether it was an ADS sourced record or not */ - var updateRecord = function (v, k, out) { + var updateRecord = function (v, k) { // db is always 'identifier:xxx' var key = ('identifier:' + v).toLowerCase(); @@ -1229,12 +1234,7 @@ define([ if (rec.idx > -1) { out.isKnownToADS = true; } - out.putcode = rec.putcode; - out.bibcode = rec.bibcode; - - if (rec.children) { - out.children = rec.children; - } + out = _.extend({}, out, rec) } }; diff --git a/src/js/modules/orcid/widget/widget.js b/src/js/modules/orcid/widget/widget.js index b82e07076..e77c3cc50 100644 --- a/src/js/modules/orcid/widget/widget.js +++ b/src/js/modules/orcid/widget/widget.js @@ -77,8 +77,6 @@ define([ var pubsub = this.getPubSub(), query = new ApiQuery({q : searchTerm, sort: 'date desc'}); pubsub.publish(pubsub.START_SEARCH, query); }); - - this.on('orcid-update-finished', this.mergeDuplicateRecords); }, orcidWidget : true, diff --git a/src/js/modules/orcid/work.js b/src/js/modules/orcid/work.js index 6fd44141e..f114b8fe2 100644 --- a/src/js/modules/orcid/work.js +++ b/src/js/modules/orcid/work.js @@ -58,18 +58,34 @@ define([ // find the inner summary as the root this._root = (work['work-summary']) ? work['work-summary'][0] : work; - var sources = ''; - Object.defineProperty(this, 'sources', { - set: function (data) { - sources = data; - }, - get: function () { - if (sources.length > 0) { - return sources; - } + this.sources = []; + + /** + * get the sources array + * if the array is empty, it returns an array containing the single source name + * + * @returns {Array} - the sources + */ + this.getSources = function () { + if (_.isEmpty(this.sources)) { return [this.getSourceName()]; } - }); + + return this.sources; + }; + + /** + * Set the sources array + * + * @param {Array} sources + * @returns {Array} - the sources + */ + this.setSources = function (sources) { + if (_.isArray(sources)) { + this.sources = sources; + } + return this.sources; + }; /** * Get the value at path @@ -124,7 +140,7 @@ define([ title: [this.getTitle()], formattedDate: this.getFormattedPubDate(), abstract: this.getShortDescription(), - source_name: this.sources.join('; '), + source_name: this.getSources().join('; '), pub: this.getJournalTitle(), _work: this }); @@ -182,8 +198,9 @@ define([ var ids = this.getExternalIds(); var out = {}; _.eachRight(props, function (p) { - if (ids[p]) { - out = p; + if (_.isString(ids[p])) { + out = ids[p]; + return false; } }); return out; diff --git a/src/js/widgets/list_of_things/templates/item-template.html b/src/js/widgets/list_of_things/templates/item-template.html index b950b6420..f0fadc910 100644 --- a/src/js/widgets/list_of_things/templates/item-template.html +++ b/src/js/widgets/list_of_things/templates/item-template.html @@ -112,9 +112,19 @@
- -

{{{title}}}

-
+ {{#if isOrcidWidget}} + {{#if encodedIdentifier}} + +

{{{title}}}

+
+ {{else}} +

{{{title}}}

+ {{/if}} + {{else}} + +

{{{title}}}

+
+ {{/if}}
diff --git a/src/js/widgets/results/widget.js b/src/js/widgets/results/widget.js index 904cab79a..29ed22af1 100644 --- a/src/js/widgets/results/widget.js +++ b/src/js/widgets/results/widget.js @@ -191,7 +191,10 @@ define([ //used by link generator mixin d.link_server = link_server; d.identifier = d.bibcode; - d.encodedIdentifier = encodeURIComponent(d.identifier); + + // make sure undefined doesn't become "undefined" + d.encodedIdentifier = _.isUndefined(d.identifier) ? + d.identifier : encodeURIComponent(d.identifier); var h = {}; if (_.keys(highlights).length) { diff --git a/test/mocha/js/modules/orcid/orcid_extension.spec.js b/test/mocha/js/modules/orcid/orcid_extension.spec.js index 0b4893cc5..f8429eced 100644 --- a/test/mocha/js/modules/orcid/orcid_extension.spec.js +++ b/test/mocha/js/modules/orcid/orcid_extension.spec.js @@ -191,7 +191,7 @@ define([ done(); }); - it("when displaying a record, it can handle the 'pending' state", function(done) { + it('handles a successful getRecordInfo request', function (done) { var w = _getWidget(); var spy = sinon.spy(); w.widget.on('orcid-update-finished', spy); @@ -199,11 +199,8 @@ define([ var oApi = minsub.beehive.getService('OrcidApi'); var d = $.Deferred(); - oApi.getRecordInfo = function() { - return d.promise(); - }; + oApi.getRecordInfo = _.constant(d.promise()); - // force new batch (render) minsub.publish(minsub.DISPLAY_DOCUMENTS, minsub.createQuery({ q: "bibcode:star" })); @@ -222,10 +219,18 @@ define([ expect(w.widget.view.children.findByIndex(1).$el.find('.s-orcid-loading').length).to.eql(0); expect(w.widget.view.children.findByIndex(1).$el.find('.orcid-update').length).to.eql(1); expect(spy.called).to.eql(true); + done(); + }); - // now test errors - d = $.Deferred(); - spy.reset(); + it('handles a rejected getRecordInfo request', function (done) { + var w = _getWidget(); + var spy = sinon.spy(); + w.widget.on('orcid-update-finished', spy); + + var oApi = minsub.beehive.getService('OrcidApi'); + var d = $.Deferred(); + + oApi.getRecordInfo = _.constant(d.promise()); minsub.publish(minsub.DISPLAY_DOCUMENTS, minsub.createQuery({ q: "bibcode:star" @@ -238,13 +243,15 @@ define([ // simulate error d.reject(); - // the widget displays orcid actions - expect(w.widget.view.children.findByIndex(1).$el.find('.s-orcid-loading').length).to.eql(0); - expect(spy.called).to.eql(true); - - // but the style is 'danger' - expect(w.widget.view.children.findByIndex(1).$el.find('button.btn-danger').length).to.eql(1); - done(); + _.delay(function () { + // the widget displays orcid actions + expect(w.widget.view.children.findByIndex(1).$el.find('.s-orcid-loading').length).to.eql(0); + expect(spy.called).to.eql(true); + + // but the style is 'default' should be default actions + expect(w.widget.view.children.findByIndex(1).$el.find('button.btn-default').length).to.eql(1); + done(); + }, 500); }); it("merges ADS data before sending them to orcid", function(done) { @@ -268,50 +275,14 @@ define([ getPutCode: _.constant(99999) }); - widget.mergeADSAndOrcidData(model); - - expect(widget.getPubSub().publish.called).to.eql(true); - expect(widget.getPubSub().publish.lastCall.args[1].get('query').get('q')).to.eql(['identifier:foo']); - - - // test of the internal logic - - widget.mergeADSAndOrcidData = function() { - var d = $.Deferred(); - d.resolve(widget.model); - return d.promise(); - }; - widget._findWorkByModel = function (model) { return $.Deferred().resolve(model).promise(); }; - widget.getBeeHive().getService = function() {return { - updateOrcid: function() { - expect(model.get('orcid').pending).to.eql(true); - var d = $.Deferred(); - d.resolve({}); - return d.promise(); - } - }}; - var uSpy = sinon.spy(); - widget.once('orcidAction:add', uSpy); - - // widget.onAllInternalEvents('childview:OrcidAction', null, {model: model, action: 'add'}); - // expect(model.get('orcid').actions.add).to.be.defined; - // expect(uSpy.called).to.eql(true); - // - // uSpy.reset(); - // widget.once('orcidAction:delete', uSpy); - // model.attributes.source_name = 'external; NASA ADS'; - // widget.onAllInternalEvents('childview:OrcidAction', null, {model: model, action: 'delete'}); - // expect(model.attributes.source_name).to.eql('external'); - // expect(uSpy.called).to.eql(false); // when there is still something, keep the rec - // - // model.attributes.source_name = 'NASA ADS'; - // widget.onAllInternalEvents('childview:OrcidAction', null, {model: model, action: 'delete'}); - // expect(uSpy.called).to.eql(true); - + widget.mergeADSAndOrcidData(model); + expect(widget.getPubSub().publish.called).to.eql(true); + expect(widget.getPubSub().publish.lastCall.args[1].get('query').get('q')).to.eql(['identifier:foo']); + done(); }); })