diff --git a/chrome/content/zotero/xpcom/retractions.js b/chrome/content/zotero/xpcom/retractions.js index 550df0733b..23785bfe12 100644 --- a/chrome/content/zotero/xpcom/retractions.js +++ b/chrome/content/zotero/xpcom/retractions.js @@ -498,10 +498,11 @@ Zotero.Retractions = { // TODO: Diff list and remove existing retractions that are missing let possibleMatches = await this._downloadPossibleMatches([...prefixesToSend]); - await this._addPossibleMatches(possibleMatches); + await this._addPossibleMatches(possibleMatches, true); } else { Zotero.debug("No possible retractions"); + await this._addPossibleMatches([], true); } await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength); @@ -530,12 +531,16 @@ Zotero.Retractions = { }, /** - * @param {Object[]} - Results from API search + * @param {Object[]} possibleMatches - Results from API search + * @param {Boolean} [removeExisting = false] - Remove retracted flag from all items that don't + * match the results. This should only be true if possibleMatches includes all possible + * matches in the database. * @return {Number[]} - Array of added item ids */ - _addPossibleMatches: async function (possibleMatches) { + _addPossibleMatches: async function (possibleMatches, removeExisting) { // Look in the key mappings for local items that match and add them as retractions var addedItemIDs = new Set(); + var allItemIDs = new Set(); for (let row of possibleMatches) { if (row.doi) { let ids = this._keyItems[this.TYPE_DOI].get(row.doi); @@ -544,6 +549,7 @@ Zotero.Retractions = { if (!this._retractedItems.has(id)) { addedItemIDs.add(id); } + allItemIDs.add(id); await this._addEntry(id, row); } } @@ -555,14 +561,31 @@ Zotero.Retractions = { if (!this._retractedItems.has(id)) { addedItemIDs.add(id); } + allItemIDs.add(id); await this._addEntry(id, row); } } } } - Zotero.debug(`Found ${addedItemIDs.size} retracted ` - + Zotero.Utilities.pluralize(addedItemIDs.size, 'item')); + // Remove existing retracted items that no longer match + var removed = 0; + if (removeExisting) { + for (let itemID of this._retractedItems) { + if (!allItemIDs.has(itemID)) { + let item = await Zotero.Items.getAsync(itemID); + await this._removeEntry(itemID, item.libraryID); + removed++; + } + } + } + + var msg = `Found ${addedItemIDs.size} retracted ` + + Zotero.Utilities.pluralize(addedItemIDs.size, 'item'); + if (removed) { + msg += " and removed " + removed; + } + Zotero.debug(msg); addedItemIDs = [...addedItemIDs]; if (addedItemIDs.length) { this._showAlert(addedItemIDs); // async diff --git a/test/tests/retractionsTest.js b/test/tests/retractionsTest.js index 5dd3b0a2ee..2ac1994ab1 100644 --- a/test/tests/retractionsTest.js +++ b/test/tests/retractionsTest.js @@ -74,6 +74,106 @@ describe("Retractions", function() { } + describe("#updateFromServer()", function () { + var server; + var baseURL; + + before(function () { + Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; + baseURL = ZOTERO_CONFIG.API_URL + 'retractions/'; + }); + + beforeEach(function () { + server = sinon.fakeServer.create(); + server.autoRespond = true; + }); + + after(async function () { + Zotero.HTTP.mock = null; + // Restore the real list from the server. We could just mock it as part of the suite. + await Zotero.Retractions.updateFromServer(); + }); + + /*it("shouldn't show banner or virtual collection for already flagged items on list update", async function () { + await Zotero.Retractions.updateFromServer(); + });*/ + + it("should remove retraction flag from items that no longer match prefix list", async function () { + var doi = '10.1234/abcde'; + var hash = Zotero.Utilities.Internal.sha1(doi); + var prefix = hash.substr(0, 5); + var lines = [ + Zotero.Retractions.TYPE_DOI + prefix + ' 12345\n', + Zotero.Retractions.TYPE_DOI + 'aaaaa 23456\n' + ]; + + var listCount = 0; + var searchCount = 0; + server.respond(function (req) { + if (req.method == 'GET' && req.url == baseURL + 'list') { + listCount++; + if (listCount == 1) { + req.respond( + 200, + { + 'Content-Type': 'text/plain', + 'ETag': 'abcdefg' + }, + lines.join('') + ); + } + else if (listCount == 2) { + req.respond( + 200, + { + 'Content-Type': 'text/plain', + 'ETag': 'bcdefgh' + }, + lines[1] + ); + } + } + else if (req.method == 'POST' && req.url == baseURL + 'search') { + searchCount++; + if (searchCount == 1) { + req.respond( + 200, + { + 'Content-Type': 'application/json' + }, + JSON.stringify([ + { + doi: hash, + retractionDOI: '10.1234/bcdef', + date: '2019-01-02' + } + ]) + ); + } + } + }); + + await Zotero.Retractions.updateFromServer(); + + // Create item with DOI from list + var promise = waitForItemEvent('refresh'); + var item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); + item.setField('DOI', doi); + await item.saveTx(); + await promise; + + assert.isTrue(Zotero.Retractions.isRetracted(item)); + + // Make a second request, with the entry removed + promise = waitForItemEvent('refresh'); + await Zotero.Retractions.updateFromServer(); + await promise; + + assert.isFalse(Zotero.Retractions.isRetracted(item)); + }); + }); + + describe("#getRetractionsFromJSON()", function () { it("should identify object with retracted DOI", async function () { var spy = sinon.spy(Zotero.HTTP, 'request');