Support deleted property for collections and searches

This lays the groundwork for moving collections and searches to the
trash instead of deleting them outright. We're not doing that yet, so
the `deleted` property will never be set (except for items), but this
will allow clients from this point forward to sync collections and
searches with that property for when it's used in the future. For now,
such objects will just be hidden from the collections pane as if they
had been deleted.
This commit is contained in:
Dan Stillman 2021-01-13 00:40:13 -05:00
parent 5a4d78578b
commit e45ca4edad
16 changed files with 346 additions and 136 deletions

View file

@ -441,10 +441,13 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
let row; let row;
let id = ids[0]; let id = ids[0];
let rowID = "C" + id; let rowID = "C" + id;
let selectedIndex = this.selection.count ? this.selection.currentIndex : 0;
switch (type) { switch (type) {
case 'collection': case 'collection':
let collection = Zotero.Collections.get(id);
row = this.getRowIndexByID(rowID); row = this.getRowIndexByID(rowID);
// If collection is visible
if (row !== false) { if (row !== false) {
// TODO: Only move if name changed // TODO: Only move if name changed
let reopen = this.isContainerOpen(row); let reopen = this.isContainerOpen(row);
@ -452,24 +455,52 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
this._closeContainer(row); this._closeContainer(row);
} }
this._removeRow(row); this._removeRow(row);
yield this._addSortedRow('collection', id);
yield this.selectByID(currentTreeRow.id); // Collection was moved to trash, so don't add it back
if (reopen) { if (collection.deleted) {
let newRow = this.getRowIndexByID(rowID); this._refreshRowMap();
if (!this.isContainerOpen(newRow)) { this.selectAfterRowRemoval(selectedIndex);
yield this.toggleOpenState(newRow); }
else {
yield this._addSortedRow('collection', id);
yield this.selectByID(currentTreeRow.id);
if (reopen) {
let newRow = this.getRowIndexByID(rowID);
if (!this.isContainerOpen(newRow)) {
yield this.toggleOpenState(newRow);
}
} }
} }
} }
// If collection isn't currently visible and it isn't in the trash (because it was
// undeleted), add it (if possible without opening any containers)
else if (!collection.deleted) {
yield this._addSortedRow('collection', id);
// Invalidate parent in case it's become non-empty
let parentRow = this.getRowIndexByID("C" + collection.parentID);
if (parentRow !== false) {
this._treebox.invalidateRow(parentRow);
}
}
break; break;
case 'search': case 'search':
let search = Zotero.Searches.get(id);
row = this.getRowIndexByID("S" + id); row = this.getRowIndexByID("S" + id);
if (row !== false) { if (row !== false) {
// TODO: Only move if name changed // TODO: Only move if name changed
this._removeRow(row); this._removeRow(row);
yield this._addSortedRow('search', id);
yield this.selectByID(currentTreeRow.id); // Search moved to trash
if (search.deleted) {
this._refreshRowMap();
this.selectAfterRowRemoval(selectedIndex);
}
// If search isn't in trash, add it back
else {
yield this._addSortedRow('search', id);
yield this.selectByID(currentTreeRow.id);
}
} }
break; break;
@ -1381,6 +1412,9 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
// Add collections // Add collections
for (var i = 0, len = collections.length; i < len; i++) { for (var i = 0, len = collections.length; i < len; i++) {
// Skip collections in trash
if (collections[i].deleted) continue;
let beforeRow = row + 1 + newRows; let beforeRow = row + 1 + newRows;
this._addRowToArray( this._addRowToArray(
rows, rows,

View file

@ -153,6 +153,7 @@ Zotero.Collection.prototype.loadFromRow = function(row) {
// Boolean // Boolean
case 'synced': case 'synced':
case 'deleted':
case 'hasChildCollections': case 'hasChildCollections':
case 'hasChildItems': case 'hasChildItems':
val = !!val; val = !!val;
@ -174,9 +175,15 @@ Zotero.Collection.prototype.loadFromRow = function(row) {
} }
Zotero.Collection.prototype.hasChildCollections = function() { Zotero.Collection.prototype.hasChildCollections = function (includeTrashed) {
this._requireData('childCollections'); this._requireData('childCollections');
return this._childCollections.size > 0; if (!this._childCollections.size) {
return false;
}
if (includeTrashed) {
return this._childCollections.size > 0;
}
return !this.getChildCollections().every(c => c.deleted);
} }
Zotero.Collection.prototype.hasChildItems = function() { Zotero.Collection.prototype.hasChildItems = function() {
@ -328,6 +335,19 @@ Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env)
}.bind(this)); }.bind(this));
} }
} }
if (this._changedData.deleted !== undefined) {
if (this._changedData.deleted) {
sql = "REPLACE INTO deletedCollections (collectionID) VALUES (?)";
}
else {
sql = "DELETE FROM deletedCollections WHERE collectionID=?";
}
yield Zotero.DB.queryAsync(sql, collectionID);
this._clearChanged('deleted');
this._markForReload('primaryData');
}
}); });
Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
@ -709,6 +729,7 @@ Zotero.Collection.prototype.fromJSON = function (json, options = {}) {
case 'name': case 'name':
case 'parentCollection': case 'parentCollection':
case 'relations': case 'relations':
case 'deleted':
break; break;
default: default:
@ -726,6 +747,10 @@ Zotero.Collection.prototype.fromJSON = function (json, options = {}) {
this.parentKey = json.parentCollection ? json.parentCollection : false; this.parentKey = json.parentCollection ? json.parentCollection : false;
this.setRelations(json.relations || {}); this.setRelations(json.relations || {});
if (json.deleted || this.deleted) {
this.deleted = !!json.deleted;
}
} }

View file

@ -40,6 +40,8 @@ Zotero.Collections = function() {
version: "O.version", version: "O.version",
synced: "O.synced", synced: "O.synced",
deleted: "DC.collectionID IS NOT NULL AS deleted",
parentID: "O.parentCollectionID AS parentID", parentID: "O.parentCollectionID AS parentID",
parentKey: "CP.key AS parentKey", parentKey: "CP.key AS parentKey",
@ -51,7 +53,8 @@ Zotero.Collections = function() {
this._primaryDataSQLFrom = "FROM collections O " this._primaryDataSQLFrom = "FROM collections O "
+ "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)"; + "LEFT JOIN deletedCollections DC ON (O.collectionID=DC.collectionID)"
+ "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID) ";
this._relationsTable = "collectionRelations"; this._relationsTable = "collectionRelations";

View file

@ -49,6 +49,7 @@ Zotero.DataObject = function () {
this._identified = false; this._identified = false;
this._parentID = null; this._parentID = null;
this._parentKey = null; this._parentKey = null;
this._deleted = null;
this._relations = []; this._relations = [];
@ -98,6 +99,33 @@ Zotero.defineProperty(Zotero.DataObject.prototype, '_canHaveParent', {
value: true value: true
}); });
// Define boolean properties
for (let name of ['deleted']) {
let prop = '_' + name;
Zotero.defineProperty(Zotero.DataObject.prototype, name, {
get: function() {
if (!this.id) {
return false;
}
var val = this._getLatestField(name);
if (this[prop] !== null) {
return this[prop];
}
this._requireData('primaryData');
},
set: function(val) {
val = !!val;
var oldVal = this._getLatestField(name);
if (oldVal == val) {
Zotero.debug(Zotero.Utilities.capitalize(name)
+ ` state hasn't changed for ${this._objectType} ${this.id}`);
return;
}
this._markFieldChange(name, val);
}
});
}
Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', { Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', {
get: function() { return this._ObjectsClass; } get: function() { return this._ObjectsClass; }
}); });
@ -721,7 +749,7 @@ Zotero.DataObject.prototype._getLatestField = function (field) {
*/ */
Zotero.DataObject.prototype._markFieldChange = function (field, value) { Zotero.DataObject.prototype._markFieldChange = function (field, value) {
// New method (changedData) // New method (changedData)
if (field == 'tags') { if (['deleted', 'tags'].includes(field)) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
this._changedData[field] = [...value]; this._changedData[field] = [...value];
} }
@ -1008,6 +1036,13 @@ Zotero.DataObject.prototype._saveData = function (env) {
env.sqlColumns.push('clientDateModified'); env.sqlColumns.push('clientDateModified');
env.sqlValues.push(Zotero.DB.transactionDateTime); env.sqlValues.push(Zotero.DB.transactionDateTime);
} }
if (!env.options.skipNotifier && this._changedData.deleted !== undefined) {
Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue);
if (!env.isNew && this._changedData.deleted) {
Zotero.Notifier.queue('trash', this._objectType, this.id, {}, env.options.notifierQueue);
}
}
}; };
Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
@ -1291,6 +1326,11 @@ Zotero.DataObject.prototype._preToJSON = function (options) {
} }
Zotero.DataObject.prototype._postToJSON = function (env) { Zotero.DataObject.prototype._postToJSON = function (env) {
var deleted = this._getLatestField('deleted');
if (deleted || env.options.mode == 'full') {
env.obj.deleted = !!deleted;
}
if (env.mode == 'patch') { if (env.mode == 'patch') {
env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj); env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj);
} }

View file

@ -231,6 +231,40 @@ Zotero.DataObjects.prototype.getLoaded = function () {
} }
/**
* Return objects in the trash
*
* @param {Integer} libraryID - Library to search
* @param {Boolean} [asIDs] - Return object ids instead of objects
* @param {Integer} [days]
* @param {Integer} [limit]
* @return {Promise<Zotero.DataObject[]|Integer[]>}
*/
Zotero.DataObjects.prototype.getDeleted = async function (libraryID, asIDs, days, limit) {
var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} `
+ `JOIN deleted${this._ZDO_Objects} USING (${this._ZDO_id}) `
+ "WHERE libraryID=?";
var params = [libraryID];
if (days) {
sql += " AND dateDeleted <= DATE('NOW', '-" + parseInt(days) + " DAYS')";
}
if (limit) {
sql += " LIMIT ?";
params.push(limit);
}
var ids = await Zotero.DB.columnQueryAsync(sql, params);
if (!ids.length) {
return [];
}
if (asIDs) {
return ids;
}
return this.getAsync(ids);
};
Zotero.DataObjects.prototype.getAllIDs = function (libraryID) { Zotero.DataObjects.prototype.getAllIDs = function (libraryID) {
var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} WHERE libraryID=?`; var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} WHERE libraryID=?`;
return Zotero.DB.columnQueryAsync(sql, [libraryID]); return Zotero.DB.columnQueryAsync(sql, [libraryID]);

View file

@ -68,7 +68,6 @@ Zotero.Item = function(itemTypeOrID) {
this._bestAttachmentState = null; this._bestAttachmentState = null;
this._fileExists = null; this._fileExists = null;
this._deleted = null;
this._hasNote = null; this._hasNote = null;
this._noteAccessTime = null; this._noteAccessTime = null;
@ -1131,7 +1130,7 @@ Zotero.Item.prototype.removeCreator = function(orderIndex, allowMissing) {
// Define boolean properties // Define boolean properties
for (let name of ['deleted', 'inPublications']) { for (let name of ['inPublications']) {
let prop = '_' + name; let prop = '_' + name;
Zotero.defineProperty(Zotero.Item.prototype, name, { Zotero.defineProperty(Zotero.Item.prototype, name, {
get: function() { get: function() {
@ -1517,8 +1516,8 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
} }
// Trashed status // Trashed status
if (this._changed.deleted) { if (this._changedData.deleted !== undefined) {
if (this._deleted) { if (this._changedData.deleted) {
sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
} }
else { else {
@ -1551,7 +1550,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
// Refresh trash // Refresh trash
if (!env.options.skipNotifier) { if (!env.options.skipNotifier) {
Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue); Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue);
if (this._deleted) { if (this._changedData.deleted) {
Zotero.Notifier.queue('trash', 'item', this.id, {}, env.options.notifierQueue); Zotero.Notifier.queue('trash', 'item', this.id, {}, env.options.notifierQueue);
} }
} }
@ -1559,6 +1558,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
if (parentItemID) { if (parentItemID) {
reloadParentChildItems[parentItemID] = true; reloadParentChildItems[parentItemID] = true;
} }
this._clearChanged('deleted');
this._markForReload('primaryData');
} }
if (this._changed.inPublications) { if (this._changed.inPublications) {
@ -3666,8 +3668,8 @@ Zotero.DataObject.prototype.setDeleted = Zotero.Promise.coroutine(function* (del
this._deleted = !!deleted; this._deleted = !!deleted;
if (this._changed.deleted) { if (this._changedData.deleted !== undefined) {
delete this._changed.deleted; delete this._changedData.deleted;
} }
}); });
@ -4636,13 +4638,6 @@ Zotero.Item.prototype.toJSON = function (options = {}) {
obj.inPublications = this._inPublications; obj.inPublications = this._inPublications;
} }
// Deleted
let deleted = this.deleted;
if (deleted || mode == 'full') {
// Match what APIv3 returns, though it would be good to change this
obj.deleted = deleted ? 1 : 0;
}
// Relations // Relations
obj.relations = this.getRelations(); obj.relations = this.getRelations();

View file

@ -94,30 +94,6 @@ Zotero.Items = function() {
}); });
/**
* Return items marked as deleted
*
* @param {Integer} libraryID - Library to search
* @param {Boolean} [asIDs] - Return itemIDs instead of Zotero.Item objects
* @return {Promise<Zotero.Item[]|Integer[]>}
*/
this.getDeleted = Zotero.Promise.coroutine(function* (libraryID, asIDs, days) {
var sql = "SELECT itemID FROM items JOIN deletedItems USING (itemID) "
+ "WHERE libraryID=?";
if (days) {
sql += " AND dateDeleted<=DATE('NOW', '-" + parseInt(days) + " DAYS')";
}
var ids = yield Zotero.DB.columnQueryAsync(sql, [libraryID]);
if (!ids.length) {
return [];
}
if (asIDs) {
return ids;
}
return this.getAsync(ids);
});
/** /**
* Returns all items in a given library * Returns all items in a given library
* *

View file

@ -136,6 +136,7 @@ Zotero.Search.prototype.loadFromRow = function (row) {
// Boolean // Boolean
case 'synced': case 'synced':
case 'deleted':
val = !!val; val = !!val;
break; break;
@ -219,6 +220,20 @@ Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
i++; i++;
} }
} }
// Trashed status
if (this._changedData.deleted !== undefined) {
if (this._changedData.deleted) {
sql = "INSERT OR IGNORE INTO deletedSearches (savedSearchID) VALUES (?)";
}
else {
sql = "DELETE FROM deletedSearches WHERE savedSearchID=?";
}
yield Zotero.DB.queryAsync(sql, searchID);
this._clearChanged('deleted');
this._markForReload('primaryData');
}
}); });
Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
@ -844,6 +859,10 @@ Zotero.Search.prototype.fromJSON = function (json, options = {}) {
condition.value condition.value
); );
} }
if (json.deleted || this.deleted) {
this.deleted = !!json.deleted;
}
} }

View file

@ -36,10 +36,12 @@ Zotero.Searches = function() {
libraryID: "O.libraryID", libraryID: "O.libraryID",
key: "O.key", key: "O.key",
version: "O.version", version: "O.version",
synced: "O.synced" synced: "O.synced",
deleted: "DS.savedSearchID IS NOT NULL AS deleted",
} }
this._primaryDataSQLFrom = "FROM savedSearches O"; this._primaryDataSQLFrom = "FROM savedSearches O "
+ "LEFT JOIN deletedSearches DS ON (O.savedSearchID=DS.savedSearchID)";
this.init = Zotero.Promise.coroutine(function* () { this.init = Zotero.Promise.coroutine(function* () {
yield Zotero.DataObjects.prototype.init.apply(this); yield Zotero.DataObjects.prototype.init.apply(this);

View file

@ -3193,6 +3193,13 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("UPDATE itemNotes SET parentItemID=NULL WHERE itemID=parentItemID"); yield Zotero.DB.queryAsync("UPDATE itemNotes SET parentItemID=NULL WHERE itemID=parentItemID");
} }
else if (i == 111) {
yield Zotero.DB.queryAsync("CREATE TABLE deletedCollections (\n collectionID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE INDEX deletedCollections_dateDeleted ON deletedCollections(dateDeleted)");
yield Zotero.DB.queryAsync("CREATE TABLE deletedSearches (\n savedSearchID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE INDEX deletedSearches_dateDeleted ON deletedSearches(dateDeleted)");
}
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom // If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
} }

View file

@ -1,4 +1,4 @@
-- 110 -- 111
-- Copyright (c) 2009 Center for History and New Media -- Copyright (c) 2009 Center for History and New Media
-- George Mason University, Fairfax, Virginia, USA -- George Mason University, Fairfax, Virginia, USA
@ -241,6 +241,13 @@ CREATE TABLE savedSearchConditions (
FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE
); );
CREATE TABLE deletedCollections (
collectionID INTEGER PRIMARY KEY,
dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE
);
CREATE INDEX deletedCollections_dateDeleted ON deletedCollections(dateDeleted);
CREATE TABLE deletedItems ( CREATE TABLE deletedItems (
itemID INTEGER PRIMARY KEY, itemID INTEGER PRIMARY KEY,
dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL, dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,
@ -248,6 +255,13 @@ CREATE TABLE deletedItems (
); );
CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted); CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted);
CREATE TABLE deletedSearches (
savedSearchID INTEGER PRIMARY KEY,
dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE
);
CREATE INDEX deletedSearches_dateDeleted ON deletedItems(dateDeleted);
CREATE TABLE libraries ( CREATE TABLE libraries (
libraryID INTEGER PRIMARY KEY, libraryID INTEGER PRIMARY KEY,
type TEXT NOT NULL, type TEXT NOT NULL,

View file

@ -431,12 +431,12 @@ function createUnsavedDataObject(objectType, params = {}) {
throw new Error("Object type not provided"); throw new Error("Object type not provided");
} }
var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version']; var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version', 'deleted'];
var itemType; var itemType;
if (objectType == 'item' || objectType == 'feedItem') { if (objectType == 'item' || objectType == 'feedItem') {
itemType = params.itemType || 'book'; itemType = params.itemType || 'book';
allowedParams.push('deleted', 'dateAdded', 'dateModified'); allowedParams.push('dateAdded', 'dateModified');
} }
if (objectType == 'item') { if (objectType == 'item') {
allowedParams.push('inPublications'); allowedParams.push('inPublications');

View file

@ -168,7 +168,31 @@ describe("Zotero.Collection", function() {
collection2.parentKey = collection3.key; collection2.parentKey = collection3.key;
yield collection2.saveTx(); yield collection2.saveTx();
assert.isFalse(collection1.hasChildCollections()); assert.isFalse(collection1.hasChildCollections());
}) });
it("should return false if all child collections are moved to trash", async function () {
var collection1 = await createDataObject('collection');
var collection2 = await createDataObject('collection', { parentID: collection1.id });
var collection3 = await createDataObject('collection', { parentID: collection1.id });
assert.isTrue(collection1.hasChildCollections());
collection2.deleted = true;
await collection2.saveTx();
assert.isTrue(collection1.hasChildCollections());
collection3.deleted = true;
await collection3.saveTx();
assert.isFalse(collection1.hasChildCollections());
});
it("should return true if child collection is in trash and includeTrashed is true", async function () {
var collection1 = await createDataObject('collection');
var collection2 = await createDataObject('collection', { parentID: collection1.id });
assert.isTrue(collection1.hasChildCollections(true));
collection2.deleted = true;
await collection2.saveTx();
assert.isTrue(collection1.hasChildCollections(true));
});
}) })
describe("#getChildCollections()", function () { describe("#getChildCollections()", function () {

View file

@ -160,6 +160,35 @@ describe("Zotero.DataObject", function() {
}); });
}) })
describe("#deleted", function () {
it("should set trash status", async function () {
for (let type of types) {
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
// Set to true
var obj = await createDataObject(type);
assert.isFalse(obj.deleted, type);
obj.deleted = true;
// Sanity check for itemsTest#trash()
if (type == 'item') {
assert.isTrue(obj._changedData.deleted, type);
}
await obj.saveTx();
var id = obj.id;
await pluralClass.reload(id, false, true);
assert.isTrue(obj.deleted, type);
// Set to false
obj.deleted = false;
await obj.saveTx();
await pluralClass.reload(id, false, true);
assert.isFalse(obj.deleted, type);
}
});
});
describe("#loadPrimaryData()", function () { describe("#loadPrimaryData()", function () {
it("should load unloaded primary data if partially set", function* () { it("should load unloaded primary data if partially set", function* () {
var objs = {}; var objs = {};
@ -574,5 +603,79 @@ describe("Zotero.DataObject", function() {
assert.equal(item1.getField('dateModified'), dateModified); assert.equal(item1.getField('dateModified'), dateModified);
}) })
}) })
}) });
describe("#fromJSON()", function () {
it("should remove object from trash if 'deleted' property not provided", async function () {
for (let type of types) {
let obj = await createDataObject(type, { deleted: true });
assert.isTrue(obj.deleted, type);
let json = obj.toJSON();
delete json.deleted;
obj.fromJSON(json);
await obj.saveTx();
assert.isFalse(obj.deleted, type);
}
});
});
describe("#toJSON()", function () {
it("should output 'deleted' as true", function () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
obj.deleted = true;
let json = obj.toJSON();
assert.isTrue(json.deleted, type);
}
});
it("shouldn't include 'deleted' if not set in default mode", function () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
let json = obj.toJSON();
assert.notProperty(json, 'deleted', type);
}
});
describe("'patch' mode", function () {
it("should include changed 'deleted' field", async function () {
for (let type of types) {
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
// True to false
let obj = createUnsavedDataObject(type)
obj.deleted = true;
let id = await obj.saveTx();
obj = await pluralClass.getAsync(id);
let patchBase = obj.toJSON();
obj.deleted = false;
let json = obj.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title, type);
assert.isFalse(json.deleted, type);
// False to true
obj = createUnsavedDataObject(type);
obj.deleted = false;
id = await obj.saveTx();
obj = await pluralClass.getAsync(id);
patchBase = obj.toJSON();
obj.deleted = true;
json = obj.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title, type);
assert.isTrue(json.deleted, type);
}
});
});
});
}) })

View file

@ -321,28 +321,6 @@ describe("Zotero.Item", function () {
}) })
}) })
describe("#deleted", function () {
it("should be set to true after save", function* () {
var item = yield createDataObject('item');
item.deleted = true;
// Sanity check for itemsTest#trash()
assert.isTrue(item._changed.deleted);
yield item.saveTx();
assert.ok(item.deleted);
})
it("should be set to false after save", function* () {
var collection = yield createDataObject('collection');
var item = createUnsavedDataObject('item');
item.deleted = true;
yield item.saveTx();
item.deleted = false;
yield item.saveTx();
assert.isFalse(item.deleted);
})
})
describe("#inPublications", function () { describe("#inPublications", function () {
it("should add item to publications table", function* () { it("should add item to publications table", function* () {
var item = yield createDataObject('item'); var item = yield createDataObject('item');
@ -1500,20 +1478,6 @@ describe("Zotero.Item", function () {
assert.isUndefined(json.numPages); assert.isUndefined(json.numPages);
}) })
it("should output 'deleted' as 1", function* () {
var itemType = "book";
var title = "Test";
var item = new Zotero.Item(itemType);
item.setField("title", title);
item.deleted = true;
var id = yield item.saveTx();
item = Zotero.Items.get(id);
var json = item.toJSON();
assert.strictEqual(json.deleted, 1);
})
it.skip("should output attachment fields from file", function* () { it.skip("should output attachment fields from file", function* () {
var file = getTestDataDirectory(); var file = getTestDataDirectory();
file.append('test.png'); file.append('test.png');
@ -1662,36 +1626,6 @@ describe("Zotero.Item", function () {
assert.isUndefined(json.tags); assert.isUndefined(json.tags);
}) })
it("should include changed 'deleted' field", function* () {
// True to false
var item = new Zotero.Item('book');
item.deleted = true;
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = item.toJSON();
item.deleted = false;
var json = item.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title);
assert.isFalse(json.deleted);
// False to true
var item = new Zotero.Item('book');
item.deleted = false;
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = item.toJSON();
item.deleted = true;
var json = item.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title);
assert.strictEqual(json.deleted, 1);
})
it("should set 'parentItem' to false when cleared", function* () { it("should set 'parentItem' to false when cleared", function* () {
var item = yield createDataObject('item'); var item = yield createDataObject('item');
var note = new Zotero.Item('note'); var note = new Zotero.Item('note');
@ -1810,19 +1744,6 @@ describe("Zotero.Item", function () {
assert.lengthOf(item.getNotes(), 0); assert.lengthOf(item.getNotes(), 0);
}); });
it("should remove item from trash if 'deleted' property not provided", async function () {
var item = await createDataObject('item', { deleted: true });
assert.isTrue(item.deleted);
var json = item.toJSON();
delete json.deleted;
item.fromJSON(json);
await item.saveTx();
assert.isFalse(item.deleted);
});
it("should remove item from My Publications if 'inPublications' property not provided", async function () { it("should remove item from My Publications if 'inPublications' property not provided", async function () {
var item = await createDataObject('item', { inPublications: true }); var item = await createDataObject('item', { inPublications: true });

View file

@ -498,6 +498,19 @@ describe("Zotero.Search", function() {
}); });
}); });
describe("#deleted", function () {
it("should set trash status", async function () {
var search = await createDataObject('search');
assert.isFalse(search.deleted);
search.deleted = true;
await search.saveTx();
assert.isTrue(search.deleted);
search.deleted = false;
await search.saveTx();
assert.isFalse(search.deleted);
});
});
describe("#toJSON()", function () { describe("#toJSON()", function () {
it("should output all data", function* () { it("should output all data", function* () {
let s = new Zotero.Search(); let s = new Zotero.Search();