diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js index cffba7959a..aa7ebcf7c9 100644 --- a/chrome/content/zotero/xpcom/data/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -1039,7 +1039,7 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { } if (retracted) { - sql += " AND (itemID IN (SELECT itemID FROM retractedItems))"; + sql += " AND (itemID IN (SELECT itemID FROM retractedItems WHERE flag=0))"; } if (publications) { diff --git a/chrome/content/zotero/xpcom/retractions.js b/chrome/content/zotero/xpcom/retractions.js index f7f3a2a477..44d51d0ac1 100644 --- a/chrome/content/zotero/xpcom/retractions.js +++ b/chrome/content/zotero/xpcom/retractions.js @@ -28,6 +28,10 @@ Zotero.Retractions = { TYPE_PMID: 'p', TYPE_NAMES: ['DOI', 'PMID'], + FLAG_NORMAL: 0, + FLAG_HIDDEN: 1, + FLAG_NO_CITATION_WARNING: 2, + _prefObserverRegistered: false, _initialized: false, _version: 1, @@ -60,13 +64,13 @@ Zotero.Retractions = { // Load existing retracted items var rows = await Zotero.DB.queryAsync( - "SELECT libraryID, itemID, DI.itemID IS NOT NULL AS deleted FROM items " - + "JOIN retractedItems USING (itemID) " + "SELECT libraryID, itemID, DI.itemID IS NOT NULL AS deleted, RI.flag FROM items " + + "JOIN retractedItems RI USING (itemID) " + "LEFT JOIN deletedItems DI USING (itemID)" ); for (let row of rows) { - this._retractedItems.add(row.itemID); - if (!row.deleted) { + this._retractedItems.set(row.itemID, row.flag); + if (!row.deleted && row.flag != this.FLAG_HIDDEN) { if (!this._retractedItemsByLibrary[row.libraryID]) { this._retractedItemsByLibrary[row.libraryID] = new Set(); } @@ -88,7 +92,7 @@ Zotero.Retractions = { this._itemKeys = {}; this._queuedItems = new Set(); this._queuedPrefixStrings = new Set(); - this._retractedItems = new Set(); + this._retractedItems = new Map(); this._retractedItemsByLibrary = {}; this._librariesWithRetractions = new Set(); this._cacheVersion = null; @@ -99,11 +103,53 @@ Zotero.Retractions = { }, /** - * @param {Zotero.Item} + * If item was retracted and the retraction hasn't been hidden + * + * @param {Zotero.Item} item * @return {Boolean} */ isRetracted: function (item) { - return this._retractedItems.has(item.id); + var flag = this._retractedItems.get(item.id); + return flag !== undefined && flag !== this.FLAG_HIDDEN; + }, + + /** + * If item was retracted and hasn't been marked to not show citation warnings + * + * @param {Zotero.Item} + * @return {Boolean} + */ + shouldShowCitationWarning: function (item) { + return this._retractedItems.get(item.id) === this.FLAG_NORMAL; + }, + + /** + * Don't show any future retraction warnings for this item + * + * @param {Zotero.Item} item + * @return {Promise} + */ + hideRetraction: async function (item) { + return this._updateItemFlag(item, this.FLAG_HIDDEN); + }, + + /** + * Don't show future citation warnings for this item + * + * @param {Zotero.Item} item + * @return {Promise} + */ + disableCitationWarningsForItem: async function (item) { + return this._updateItemFlag(item, this.FLAG_NO_CITATION_WARNING); + }, + + _updateItemFlag: async function (item, flag) { + this._retractedItems.set(item.id, flag); + await Zotero.DB.queryAsync( + "UPDATE retractedItems SET flag=? WHERE itemID=?", + [flag, item.id] + ); + await Zotero.Notifier.trigger('modify', 'item', [item.id]); }, getRetractionsFromJSON: Zotero.serial(async function (jsonItems) { @@ -369,8 +415,9 @@ Zotero.Retractions = { // handled for newly detected items in _addEntry(), which gets called by // _updateItem() above after a delay (such that the item won't yet be retracted // here). - if (this._retractedItems.has(item.id)) { - if (item.deleted) { + let flag = this._retractedItems.get(item.id); + if (flag !== undefined) { + if (item.deleted || flag == this.FLAG_HIDDEN) { await this._removeLibraryRetractedItem(item.libraryID, item.id); } else { @@ -577,7 +624,7 @@ Zotero.Retractions = { // Remove existing retracted items that no longer match var removed = 0; if (removeExisting) { - for (let itemID of this._retractedItems) { + for (let itemID of this._retractedItems.keys()) { if (!allItemIDs.has(itemID)) { let item = await Zotero.Items.getAsync(itemID); await this._removeEntry(itemID, item.libraryID); @@ -800,13 +847,17 @@ Zotero.Retractions = { delete o.retractionDOI; delete o.retractionPMID; - var sql = "REPLACE INTO retractedItems VALUES (?, ?)"; + var sql = "REPLACE INTO retractedItems (itemID, data) VALUES (?, ?)"; await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]); var item = await Zotero.Items.getAsync(itemID); var libraryID = item.libraryID; - this._retractedItems.add(itemID); - if (!item.deleted) { + // Check whether the retraction is already hidden by the user + var flag = this._retractedItems.get(itemID); + if (flag === undefined) { + this._retractedItems.set(itemID, this.FLAG_NORMAL); + } + if (!item.deleted && flag !== this.FLAG_HIDDEN) { if (!this._retractedItemsByLibrary[libraryID]) { this._retractedItemsByLibrary[libraryID] = new Set(); } diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index 763263db9d..0e5bcd631b 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -2518,6 +2518,10 @@ Zotero.Schema = new function(){ yield Zotero.DB.queryAsync("CREATE TABLE IF NOT EXISTS retractedItems (\n itemID INTEGER PRIMARY KEY,\n data TEXT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n);"); } + else if (i == 105) { + yield Zotero.DB.queryAsync("ALTER TABLE retractedItems ADD COLUMN flag INT DEFAULT 0"); + } + // If breaking compatibility or doing anything dangerous, clear minorUpdateFrom } diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index be41031bf8..998af81053 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -1,4 +1,4 @@ --- 104 +-- 105 -- Copyright (c) 2009 Center for History and New Media -- George Mason University, Fairfax, Virginia, USA @@ -289,6 +289,7 @@ CREATE TABLE publicationsItems ( CREATE TABLE retractedItems ( itemID INTEGER PRIMARY KEY, data TEXT, + flag INT DEFAULT 0, FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE ); diff --git a/test/tests/retractionsTest.js b/test/tests/retractionsTest.js index 2ac1994ab1..997a894619 100644 --- a/test/tests/retractionsTest.js +++ b/test/tests/retractionsTest.js @@ -174,6 +174,23 @@ describe("Retractions", function() { }); + describe("#shouldShowCitationWarning()", function () { + it("should return false if citation warning is hidden", async function () { + var item = await createRetractedItem(); + assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item)); + await Zotero.Retractions.disableCitationWarningsForItem(item); + assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item)); + }); + + it("should return false if retraction is hidden", async function () { + var item = await createRetractedItem(); + assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item)); + await Zotero.Retractions.hideRetraction(item); + assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item)); + }); + }); + + describe("#getRetractionsFromJSON()", function () { it("should identify object with retracted DOI", async function () { var spy = sinon.spy(Zotero.HTTP, 'request'); @@ -275,6 +292,44 @@ describe("Retractions", function() { assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID); }); + it("should hide Retracted Items collection when last retracted item is marked as hidden", async function () { + var rowID = "R" + userLibraryID; + + // Create item + var item = await createRetractedItem(); + assert.ok(zp.collectionsView.getRowIndexByID(rowID)); + + // Select Retracted Items collection + await zp.collectionsView.selectByID(rowID); + await waitForItemsLoad(win); + + await Zotero.Retractions.hideRetraction(item); + + await Zotero.Promise.delay(50); + // Retracted Items should be gone + assert.isFalse(zp.collectionsView.getRowIndexByID(rowID)); + // And My Library should be selected + assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID); + }); + + it("shouldn't hide Retracted Items collection when last retracted item is marked to not show a citation warning", async function () { + var rowID = "R" + userLibraryID; + + // Create item + var item = await createRetractedItem(); + assert.ok(zp.collectionsView.getRowIndexByID(rowID)); + + // Select Retracted Items collection + await zp.collectionsView.selectByID(rowID); + await waitForItemsLoad(win); + + await Zotero.Retractions.disableCitationWarningsForItem(item); + + await Zotero.Promise.delay(50); + // Should still be showing + assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID)); + }); + it("should show Retracted Items collection when retracted item is restored from trash", async function () { // Create trashed item var item = await createRetractedItem({ deleted: true });