Add Feed and FeedItem

Also:
* _finalizeErase in Zotero.DataObject is now inheritable
* Call _initErase before starting a DB transaction
* removes Zotero.Libraries.add and Zotero.Libraries.remove (doesn't seem like this is used any more)
This commit is contained in:
Aurimas Vinckevicius 2015-06-01 23:29:40 -05:00
parent 76511eca08
commit 88ab129ffb
35 changed files with 3017 additions and 689 deletions

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
*/ */
Zotero.Collection = function() { Zotero.Collection = function(params = {}) {
Zotero.Collection._super.apply(this); Zotero.Collection._super.apply(this);
this._name = null; this._name = null;
@ -33,6 +33,9 @@ Zotero.Collection = function() {
this._hasChildItems = false; this._hasChildItems = false;
this._childItems = []; this._childItems = [];
Zotero.Utilities.assignProps(this, params, ['name', 'libraryID', 'parentID',
'parentKey', 'lastSync']);
} }
Zotero.extendClass(Zotero.DataObject, Zotero.Collection); Zotero.extendClass(Zotero.DataObject, Zotero.Collection);
@ -244,7 +247,7 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) {
Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) { Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.name) { if (!this.name) {
throw new Error('Collection name is empty'); throw new Error(this._ObjectType + ' name is empty');
} }
var proceed = yield Zotero.Collection._super.prototype._initSave.apply(this, arguments); var proceed = yield Zotero.Collection._super.prototype._initSave.apply(this, arguments);
@ -338,12 +341,6 @@ Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env)
}); });
Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
if (env.isNew && Zotero.Libraries.isGroupLibrary(this.libraryID)) {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
var group = Zotero.Groups.get(groupID);
group.clearCollectionCache();
}
if (!env.options.skipNotifier) { if (!env.options.skipNotifier) {
if (env.isNew) { if (env.isNew) {
Zotero.Notifier.queue('add', 'collection', this.id, env.notifierData); Zotero.Notifier.queue('add', 'collection', this.id, env.notifierData);
@ -362,6 +359,10 @@ Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (
this._clearChanged(); this._clearChanged();
} }
if (env.isNew) {
yield Zotero.Libraries.get(this.libraryID).updateCollections();
}
return env.isNew ? this.id : true; return env.isNew ? this.id : true;
}); });
@ -610,7 +611,19 @@ Zotero.Collection.prototype._eraseData = Zotero.Promise.coroutine(function* (env
} }
} }
if (del.length) { if (del.length) {
yield this.ChildObjects.trash(del); if (Zotero.Libraries.hasTrash(this.libraryID)) {
yield this.ChildObjects.trash(del);
} else {
Zotero.debug(Zotero.Libraries.getName(this.libraryID) + " library does not have trash. "
+ this.ChildObjects._ZDO_Objects + " will be erased");
let options = {};
Object.assign(options, env.options);
options.tx = false;
for (let i=0; i<del.length; i++) {
let obj = yield this.ChildObjects.getAsync(del[i]);
yield obj.erase(options);
}
}
} }
var placeholders = collections.map(function () '?').join(); var placeholders = collections.map(function () '?').join();
@ -631,6 +644,11 @@ Zotero.Collection.prototype._eraseData = Zotero.Promise.coroutine(function* (env
env.deletedObjectIDs = collections; env.deletedObjectIDs = collections;
}); });
Zotero.Collection.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
yield Zotero.Collection._super.prototype._finalizeErase.call(this, env);
yield Zotero.Libraries.get(this.libraryID).updateCollections();
});
Zotero.Collection.prototype.isCollection = function() { Zotero.Collection.prototype.isCollection = function() {
return true; return true;

View file

@ -53,6 +53,7 @@ Zotero.Collections = function() {
this._primaryDataSQLFrom = "FROM collections O " this._primaryDataSQLFrom = "FROM collections O "
+ "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)"; + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)";
this._relationsTable = "collectionRelations";
/** /**
* Get collections within a library * Get collections within a library

View file

@ -611,7 +611,7 @@ Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function*
Zotero.DataObject.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload) { Zotero.DataObject.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload) {
if (this._objectType != 'collection' && this._objectType != 'item') { if (!this.ObjectsClass._relationsTable) {
throw new Error("Relations not supported for " + this._objectTypePlural); throw new Error("Relations not supported for " + this._objectTypePlural);
} }
@ -623,7 +623,7 @@ Zotero.DataObject.prototype.loadRelations = Zotero.Promise.coroutine(function* (
this._requireData('primaryData'); this._requireData('primaryData');
var sql = "SELECT predicate, object FROM " + this._objectType + "Relations " var sql = "SELECT predicate, object FROM " + this.ObjectsClass._relationsTable + " "
+ "JOIN relationPredicates USING (predicateID) " + "JOIN relationPredicates USING (predicateID) "
+ "WHERE " + this.ObjectsClass.idColumn + "=?"; + "WHERE " + this.ObjectsClass.idColumn + "=?";
var rows = yield Zotero.DB.queryAsync(sql, this.id); var rows = yield Zotero.DB.queryAsync(sql, this.id);
@ -936,6 +936,11 @@ Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env)
this.editCheck(); this.editCheck();
} }
let targetLib = Zotero.Libraries.get(this.libraryID);
if (!targetLib.isChildObjectAllowed(this._objectType)) {
throw new Error("Cannot add " + this._objectType + " to a " + targetLib.libraryType + " library");
}
if (!this.hasChanged()) { if (!this.hasChanged()) {
Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4); Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4);
return false; return false;
@ -1145,20 +1150,21 @@ Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options)
env.options.tx = true; env.options.tx = true;
} }
let proceed = yield this._initErase(env);
if (!proceed) return false;
Zotero.debug('Deleting ' + this.objectType + ' ' + this.id); Zotero.debug('Deleting ' + this.objectType + ' ' + this.id);
if (env.options.tx) { if (env.options.tx) {
return Zotero.DB.executeTransaction(function* () { return Zotero.DB.executeTransaction(function* () {
Zotero.DataObject.prototype._initErase.call(this, env);
yield this._eraseData(env); yield this._eraseData(env);
Zotero.DataObject.prototype._finalizeErase.call(this, env); yield this._finalizeErase(env);
}.bind(this)) }.bind(this))
} }
else { else {
Zotero.DB.requireTransaction(); Zotero.DB.requireTransaction();
Zotero.DataObject.prototype._initErase.call(this, env);
yield this._eraseData(env); yield this._eraseData(env);
yield Zotero.DataObject.prototype._finalizeErase.call(this, env); yield this._finalizeErase(env);
} }
}); });
@ -1168,7 +1174,7 @@ Zotero.DataObject.prototype.eraseTx = function (options) {
return this.erase(options); return this.erase(options);
}; };
Zotero.DataObject.prototype._initErase = function (env) { Zotero.DataObject.prototype._initErase = Zotero.Promise.method(function (env) {
env.notifierData = {}; env.notifierData = {};
env.notifierData[this.id] = { env.notifierData[this.id] = {
libraryID: this.libraryID, libraryID: this.libraryID,
@ -1181,8 +1187,8 @@ Zotero.DataObject.prototype._initErase = function (env) {
env.notifierData[this.id].skipDeleteLog = true; env.notifierData[this.id].skipDeleteLog = true;
} }
return Zotero.Promise.resolve(true); return true;
}; });
Zotero.DataObject.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) { Zotero.DataObject.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
// Delete versions from sync cache // Delete versions from sync cache

View file

@ -89,7 +89,16 @@ Zotero.DataObjectUtilities = {
"getObjectTypePlural": function(objectType) { "getObjectTypePlural": function(objectType) {
return objectType == 'search' ? 'searches' : objectType + 's'; switch(objectType) {
case 'search':
return 'searches';
break;
case 'library':
return 'libraries';
break;
default:
return objectType + 's';
}
}, },

View file

@ -51,8 +51,6 @@ Zotero.DataObjects = function () {
this.ObjectClass = Zotero[this._ZDO_Object]; this.ObjectClass = Zotero[this._ZDO_Object];
} }
this.primaryDataSQLFrom = " " + this._primaryDataSQLFrom + " " + this._primaryDataSQLWhere;
this._objectCache = {}; this._objectCache = {};
this._objectKeys = {}; this._objectKeys = {};
this._objectIDs = {}; this._objectIDs = {};
@ -74,6 +72,13 @@ Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryFields', {
get: function () Object.keys(this._primaryDataSQLParts) get: function () Object.keys(this._primaryDataSQLParts)
}, {lazy: true}); }, {lazy: true});
Zotero.defineProperty(Zotero.DataObjects.prototype, "_primaryDataSQLWhere", {
value: "WHERE 1"
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryDataSQLFrom', {
get: function() " " + this._primaryDataSQLFrom + " " + this._primaryDataSQLWhere
}, {lateInit: true});
Zotero.DataObjects.prototype.init = function() { Zotero.DataObjects.prototype.init = function() {
return this._loadIDsAndKeys(); return this._loadIDsAndKeys();
@ -394,6 +399,17 @@ Zotero.DataObjects.prototype.registerObject = function (obj) {
obj._inCache = true; obj._inCache = true;
} }
Zotero.DataObjects.prototype.dropDeadObjectsFromCache = function() {
let ids = [];
for (let libraryID in this._objectIDs) {
if (Zotero.Libraries.exists(libraryID)) continue;
for (let key in this._objectIDs[libraryID]) {
ids.push(this._objectIDs[libraryID][key]);
}
}
this.unload(ids);
}
/** /**
* Clear object from internal array * Clear object from internal array
@ -507,8 +523,6 @@ Zotero.defineProperty(Zotero.DataObjects.prototype, "primaryDataSQL", {
} }
}, {lazy: true}); }, {lazy: true});
Zotero.DataObjects.prototype._primaryDataSQLWhere = "WHERE 1";
Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) { Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
var sql = this._primaryDataSQLParts[part]; var sql = this._primaryDataSQLParts[part];
if (!sql) { if (!sql) {
@ -590,7 +604,7 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
} }
// Object doesn't exist -- create new object and stuff in cache // Object doesn't exist -- create new object and stuff in cache
else { else {
obj = new Zotero[this._ZDO_Object]; obj = this._getObjectForRow(rowObj);
obj.loadFromRow(rowObj, true); obj.loadFromRow(rowObj, true);
if (!options || !options.noCache) { if (!options || !options.noCache) {
this.registerObject(obj); this.registerObject(obj);
@ -624,6 +638,9 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
return loaded; return loaded;
}); });
Zotero.DataObjects.prototype._getObjectForRow = function(row) {
return new Zotero[this._ZDO_Object];
};
Zotero.DataObjects.prototype._loadIDsAndKeys = Zotero.Promise.coroutine(function* () { Zotero.DataObjects.prototype._loadIDsAndKeys = Zotero.Promise.coroutine(function* () {
var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table; var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table;

View file

@ -0,0 +1,273 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Feed = function(params = {}) {
params.libraryType = 'feed';
Zotero.Feed._super.call(this, params);
this._feedCleanupAfter = null;
this._feedRefreshInterval = null;
// Feeds are not editable/filesEditable by the user. Remove the setter
this.editable = false;
Zotero.defineProperty(this, 'editable', {
get: function() this._get('_libraryEditable')
});
this.filesEditable = false;
Zotero.defineProperty(this, 'filesEditable', {
get: function() this._get('_libraryFilesEditable')
});
Zotero.Utilities.assignProps(this, params, ['name', 'url', 'refreshInterval',
'cleanupAfter']);
// Return a proxy so that we can disable the object once it's deleted
return new Proxy(this, {
get: function(obj, prop) {
if (obj._disabled && !(prop == 'libraryID' || prop == 'id')) {
throw new Error("Feed (" + obj.libraryID + ") has been disabled");
}
return obj[prop];
}
});
}
Zotero.defineProperty(Zotero.Feed, '_dbColumns', {
value: Object.freeze(['name', 'url', 'lastUpdate', 'lastCheck',
'lastCheckError', 'cleanupAfter', 'refreshInterval'])
});
Zotero.Feed._colToProp = function(c) {
return "_feed" + Zotero.Utilities.capitalize(c);
}
Zotero.defineProperty(Zotero.Feed, '_rowSQLSelect', {
value: Zotero.Library._rowSQLSelect + ", "
+ Zotero.Feed._dbColumns.map(c => "F." + c + " AS " + Zotero.Feed._colToProp(c)).join(", ")
+ ", (SELECT COUNT(*) FROM items I JOIN feedItems FeI USING (itemID)"
+ " WHERE I.libraryID=F.libraryID AND FeI.readTime IS NULL) AS feedUnreadCount"
});
Zotero.defineProperty(Zotero.Feed, '_rowSQL', {
value: "SELECT " + Zotero.Feed._rowSQLSelect
+ " FROM feeds F JOIN libraries L USING (libraryID)"
});
Zotero.extendClass(Zotero.Library, Zotero.Feed);
Zotero.defineProperty(Zotero.Feed.prototype, '_objectType', {
value: 'feed'
});
Zotero.defineProperty(Zotero.Feed.prototype, 'isFeed', {
value: true
});
Zotero.defineProperty(Zotero.Feed.prototype, 'libraryTypes', {
value: Object.freeze(Zotero.Feed._super.prototype.libraryTypes.concat(['feed']))
});
(function() {
// Create accessors
let accessors = ['name', 'url', 'refreshInterval', 'cleanupAfter'];
for (let i=0; i<accessors.length; i++) {
let name = accessors[i];
let prop = Zotero.Feed._colToProp(name);
Zotero.defineProperty(Zotero.Feed.prototype, name, {
get: function() this._get(prop),
set: function(v) this._set(prop, v)
})
}
let getters = ['lastCheck', 'lastUpdate', 'lastCheckError'];
for (let i=0; i<getters.length; i++) {
let name = getters[i];
let prop = Zotero.Feed._colToProp(name);
Zotero.defineProperty(Zotero.Feed.prototype, name, {
get: function() this._get(prop),
})
}
})()
Zotero.Feed.prototype._isValidFeedProp = function(prop) {
let preffix = '_feed';
if (prop.indexOf(preffix) != 0 || prop.length == preffix.length) {
return false;
}
let col = prop.substr(preffix.length);
col = col.charAt(0).toLowerCase() + col.substr(1);
return Zotero.Feed._dbColumns.indexOf(col) != -1;
}
Zotero.Feed.prototype._isValidProp = function(prop) {
return this._isValidFeedProp(prop)
|| Zotero.Feed._super.prototype._isValidProp.call(this, prop);
}
Zotero.Feed.prototype._set = function (prop, val) {
switch (prop) {
case '_feedName':
if (!val || typeof val != 'string') {
throw new Error(prop + " must be a non-empty string");
}
break;
case '_feedUrl':
let uri,
invalidUrlError = "Invalid feed URL " + val;
try {
uri = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)
.newURI(val, null, null);
val = uri.spec;
} catch(e) {
throw new Error(invalidUrlError);
}
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
throw new Error(invalidUrlError);
}
break;
case '_feedRefreshInterval':
case '_feedCleanupAfter':
if (val === null) break;
let newVal = Number.parseInt(val, 10);
if (newVal != val || !newVal || newVal <= 0) {
throw new Error(prop + " must be null or a positive integer");
}
break;
case '_feedLastCheckError':
if (!val) {
val = null;
break;
}
if (typeof val !== 'string') {
throw new Error(prop + " must be null or a string");
}
break;
}
return Zotero.Feed._super.prototype._set.call(this, prop, val);
}
Zotero.Feed.prototype._loadDataFromRow = function(row) {
Zotero.Feed._super.prototype._loadDataFromRow.call(this, row);
this._feedUrl = row._feedUrl;
this._feedLastCheckError = row._feedLastCheckError || null;
this._feedLastCheck = row._feedLastCheck || null;
this._feedLastUpdate = row._feedLastUpdate || null;
this._feedCleanupAfter = parseInt(row._feedCleanupAfter) || null;
this._feedRefreshInterval = parseInt(row._feedRefreshInterval) || null;
this._feedUnreadCount = parseInt(row.feedUnreadCount);
}
Zotero.Feed.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
let sql = Zotero.Feed._rowSQL + " WHERE F.libraryID=?";
let row = yield Zotero.DB.rowQueryAsync(sql, [this.libraryID]);
this._loadDataFromRow(row);
});
Zotero.defineProperty(Zotero.Feed.prototype, '_childObjectTypes', {
value: Object.freeze(['feedItem'])
});
Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
let proceed = yield Zotero.Feed._super.prototype._initSave.call(this, env);
if (!proceed) return false;
if (!this._feedName) throw new Error("Feed name not set");
if (!this._feedUrl) throw new Error("Feed URL not set");
if (env.isNew) {
// Make sure URL is unique
if (Zotero.Feeds.existsByURL(this._feedUrl)) {
throw new Error('Feed for URL already exists: ' + this._feedUrl);
}
}
return true;
});
Zotero.Feed.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.Feed._super.prototype._saveData.apply(this, arguments);
Zotero.debug("Saving feed data for collection " + this.id);
let changedCols = [], params = [];
for (let i=0; i<Zotero.Feed._dbColumns.length; i++) {
let col = Zotero.Feed._dbColumns[i];
let prop = Zotero.Feed._colToProp(col);
if (!this._changed[prop]) continue;
changedCols.push(col);
params.push(this[prop]);
}
if (env.isNew) {
changedCols.push('libraryID');
params.push(this.libraryID);
let sql = "INSERT INTO feeds (" + changedCols.join(', ') + ") "
+ "VALUES (" + Array(params.length).fill('?').join(', ') + ")";
yield Zotero.DB.queryAsync(sql, params);
Zotero.Notifier.queue('add', 'feed', this.libraryID);
}
else if (changedCols.length) {
let sql = "UPDATE feeds SET " + changedCols.map(v => v + '=?').join(', ')
+ " WHERE libraryID=?";
params.push(this.libraryID);
yield Zotero.DB.queryAsync(sql, params);
Zotero.Notifier.queue('modify', 'feed', this.libraryID);
}
else {
Zotero.debug("Feed data did not change for feed " + this.libraryID, 5);
}
});
Zotero.Feed.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
let changedURL = this._changed._feedUrl;
yield Zotero.Feed._super.prototype._finalizeSave.apply(this, arguments);
if (env.isNew) {
Zotero.Feeds.register(this);
} else if (changedURL) {
// Re-register library if URL changed
Zotero.Feeds.unregister(this.libraryID);
Zotero.Feeds.register(this);
}
});
Zotero.Feed.prototype._finalizeErase = Zotero.Promise.method(function(env) {
Zotero.Feeds.unregister(this.libraryID);
return Zotero.Feed._super.prototype._finalizeErase.apply(this, arguments);
});

View file

@ -0,0 +1,143 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/*
* Constructor for FeedItem object
*/
Zotero.FeedItem = function(itemTypeOrID, params = {}) {
Zotero.FeedItem._super.call(this, itemTypeOrID);
this._feedItemReadTime = null;
Zotero.Utilities.assignProps(this, params, ['guid']);
}
Zotero.extendClass(Zotero.Item, Zotero.FeedItem)
Zotero.FeedItem.prototype._objectType = 'feedItem';
Zotero.FeedItem.prototype._containerObject = 'feed';
Zotero.defineProperty(Zotero.FeedItem.prototype, 'isFeedItem', {
value: true
});
Zotero.defineProperty(Zotero.FeedItem.prototype, 'guid', {
get: function() this._feedItemGUID,
set: function(val) {
if (this.id) throw new Error('Cannot set GUID after item ID is already set');
if (typeof val != 'string') throw new Error('GUID must be a non-empty string');
this._feedItemGUID = val;
}
});
Zotero.defineProperty(Zotero.FeedItem.prototype, 'isRead', {
get: function() {
return !!this._feedItemReadTime;
},
set: function(read) {
if (!read != !this._feedItemReadTime) {
// changed
if (read) {
this._feedItemReadTime = Zotero.Date.dateToSQL(new Date(), true);
} else {
this._feedItemReadTime = null;
}
this._changed.feedItemData = true;
}
}
});
Zotero.FeedItem.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) {
if (this.guid && !this.id) {
// fill in item ID
this.id = yield this.ObjectsClass.getIDFromGUID(this.guid);
}
yield Zotero.FeedItem._super.prototype.loadPrimaryData.apply(this, arguments);
});
Zotero.FeedItem.prototype.setField = function(field, value) {
if (field == 'libraryID') {
// Ensure that it references a feed
if (!Zotero.Libraries.get(value).isFeed) {
throw new Error('libraryID must reference a feed');
}
}
return Zotero.FeedItem._super.prototype.setField.apply(this, arguments);
}
Zotero.FeedItem.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.guid) {
throw new Error('GUID must be set before saving ' + this._ObjectType);
}
let proceed = yield Zotero.FeedItem._super.prototype._initSave.apply(this, arguments);
if (!proceed) return proceed;
if (env.isNew) {
// verify that GUID doesn't already exist for a new item
var item = yield this.ObjectsClass.getIDFromGUID(this.guid);
if (item) {
throw new Error('Cannot create new item with GUID ' + this.guid + '. Item already exists.');
}
// Register GUID => itemID mapping in cache on commit
if (!env.transactionOptions) env.transactionOptions = {};
var superOnCommit = env.transactionOptions.onCommit;
env.transactionOptions.onCommit = () => {
if (superOnCommit) superOnCommit();
this.ObjectsClass._setGUIDMapping(this.guid, env.id);
};
}
return proceed;
});
Zotero.FeedItem.prototype.forceSaveTx = function(options) {
let newOptions = {};
Object.assign(newOptions, options || {});
newOptions.skipEditCheck = true;
return this.saveTx(newOptions);
}
Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
if (this._changed.feedItemData || env.isNew) {
var sql = "REPLACE INTO feedItems VALUES (?,?,?)";
yield Zotero.DB.queryAsync(sql, [env.id, this.guid, this._feedItemReadTime]);
this._clearChanged('feedItemData');
}
});
Zotero.FeedItem.prototype.forceEraseTx = function(options) {
let newOptions = {};
Object.assign(newOptions, options || {});
newOptions.skipEditCheck = true;
return this.eraseTx(newOptions);
}

View file

@ -0,0 +1,110 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/*
* Primary interface for accessing Zotero feed items
*/
Zotero.FeedItems = new Proxy(function() {
let _idCache = {},
_guidCache = {};
// Teach Zotero.Items about Zotero.FeedItem
// This one is a lazy getter, so we don't patch it up until first access
let zi_primaryDataSQLParts = Object.getOwnPropertyDescriptor(Zotero.Items, '_primaryDataSQLParts').get;
Zotero.defineProperty(Zotero.Items, '_primaryDataSQLParts', {
get: function() {
let obj = zi_primaryDataSQLParts.call(this);
obj.feedItemGUID = "FeI.guid AS feedItemGUID";
obj.feedItemReadTime = "FeI.readTime AS feedItemReadTime";
return obj;
}
}, {lazy: true});
Zotero.Items._primaryDataSQLFrom += " LEFT JOIN feedItems FeI ON (FeI.itemID=O.itemID)";
let zi_getObjectForRow = Zotero.Items._getObjectForRow;
Zotero.Items._getObjectForRow = function(row) {
if (row.feedItemGUID) {
return new Zotero.FeedItem();
}
return zi_getObjectForRow.apply(Zotero.Items, arguments);
}
this.getIDFromGUID = Zotero.Promise.coroutine(function* (guid) {
if (_idCache[guid] !== undefined) return _idCache[guid];
id = yield Zotero.DB.valueQueryAsync('SELECT itemID FROM feedItems WHERE guid=?', [guid]);
if (!id) return false;
this._setGUIDMapping(guid, id);
return id;
});
this._setGUIDMapping = function(guid, id) {
_idCache[guid] = id;
_guidCache[id] = guid;
};
this._deleteGUIDMapping = function(guid, id) {
if (!id) id = _idCache[guid];
if (!guid) guid = _guidCache[id];
if (!guid || !id) return;
delete _idCache[guid];
delete _guidCache[id];
};
this.unload = function() {
Zotero.Items.unload.apply(Zotero.Items, arguments);
let ids = Zotero.flattenArguments(arguments);
for (let i=0; i<ids.length; i++) {
this._deleteGUIDMapping(null, ids[i]);
}
};
this.getAsyncByGUID = Zotero.Promise.coroutine(function* (guid) {
let id = yield this.getIDFromGUID(guid);
if (id === false) return false;
return this.getAsync(id);
});
return this;
}.call({}),
// Proxy handler
{
get: function(target, name) {
return name in target
? target[name]
: Zotero.Items[name];
},
has: function(target, name) {
return name in target || name in Zotero.Items;
}
});

View file

@ -0,0 +1,113 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
// Add some feed methods, but otherwise proxy to Zotero.Collections
Zotero.Feeds = new function() {
this._cache = null;
this._makeCache = function() {
return {
libraryIDByURL: {},
urlByLibraryID: {}
};
}
this.register = function (feed) {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
Zotero.debug("Zotero.Feeds: Registering feed " + feed.libraryID, 5);
this._addToCache(this._cache, feed);
}
this._addToCache = function (cache, feed) {
if (!feed.libraryID) throw new Error('Cannot register an unsaved feed');
if (cache.libraryIDByURL[feed.url]) {
Zotero.debug('Feed with url ' + feed.url + ' is already registered', 2, true);
}
if (cache.urlByLibraryID[feed.libraryID]) {
Zotero.debug('Feed with libraryID ' + feed.libraryID + ' is already registered', 2, true);
}
cache.libraryIDByURL[feed.url] = feed.libraryID;
cache.urlByLibraryID[feed.libraryID] = feed.url;
}
this.unregister = function (libraryID) {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
Zotero.debug("Zotero.Feeds: Unregistering feed " + libraryID, 5);
let url = this._cache.urlByLibraryID[libraryID];
if (url === undefined) {
Zotero.debug('Attempting to unregister a feed that is not registered (' + libraryID + ')', 2, true);
return;
}
delete this._cache.urlByLibraryID[libraryID];
delete this._cache.libraryIDByURL[url];
}
this.getByURL = function(urls) {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
let asArray = true;
if (!Array.isArray(urls)) {
urls = [urls];
asArray = false;
}
let libraryIDs = Array(urls.length);
for (let i=0; i<urls.length; i++) {
let libraryID = this._cache.libraryIDByURL[urls[i]];
if (!libraryID) {
throw new Error('Feed with url ' + urls[i] + ' not registered in feed cache');
}
libraryIDs[i] = libraryID;
}
let feeds = Zotero.Libraries.get(libraryIDs);
return asArray ? feeds : feeds[0];
}
this.existsByURL = function(url) {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
return this._cache.libraryIDByURL[url] !== undefined;
}
this.getAll = function() {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
return Object.keys(this._cache.urlByLibraryID)
.map(id => Zotero.Libraries.get(id));
}
this.haveFeeds = function() {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
return !!Object.keys(this._cache.urlByLibraryID).length
}
}

View file

@ -23,292 +23,217 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
*/ */
Zotero.Group = function (params = {}) {
Zotero.Group = function () { params.libraryType = 'group';
if (arguments[0]) { Zotero.Group._super.call(this, params);
throw ("Zotero.Group constructor doesn't take any parameters");
}
this._init(); Zotero.Utilities.assignProps(this, params, ['groupID', 'name', 'description',
} 'version']);
Zotero.Group.prototype._init = function () {
this._id = null;
this._libraryID = null;
this._name = null;
this._description = null;
this._editable = null;
this._filesEditable = null;
this._version = null;
this._loaded = false; // Return a proxy so that we can disable the object once it's deleted
this._changed = false; return new Proxy(this, {
this._hasCollections = null; get: function(obj, prop) {
this._hasSearches = null; if (obj._disabled && !(prop == 'libraryID' || prop == 'id')) {
} throw new Error("Group (" + obj.libraryID + ") has been disabled");
Zotero.Group.prototype.__defineGetter__('objectType', function () { return 'group'; });
Zotero.Group.prototype.__defineGetter__('id', function () { return this._get('id'); });
Zotero.Group.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
Zotero.Group.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
Zotero.Group.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Group.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Group.prototype.__defineGetter__('description', function () { return this._get('description'); });
Zotero.Group.prototype.__defineSetter__('description', function (val) { this._set('description', val); });
Zotero.Group.prototype.__defineGetter__('editable', function () { return this._get('editable'); });
Zotero.Group.prototype.__defineSetter__('editable', function (val) { this._set('editable', val); });
Zotero.Group.prototype.__defineGetter__('filesEditable', function () { if (!this.editable) { return false; } return this._get('filesEditable'); });
Zotero.Group.prototype.__defineSetter__('filesEditable', function (val) { this._set('filesEditable', val); });
Zotero.Group.prototype.__defineGetter__('version', function () { return this._get('version'); });
Zotero.Group.prototype.__defineSetter__('version', function (val) { this._set('version', val); });
Zotero.Group.prototype._get = function (field) {
if (this['_' + field] !== null) {
return this['_' + field];
}
this._requireLoad();
return null;
}
Zotero.Group.prototype._set = function (field, val) {
switch (field) {
case 'id':
case 'libraryID':
if (val == this['_' + field]) {
return;
} }
return obj[prop];
if (this._loaded) {
throw new Error("Cannot set " + field + " after object is already loaded");
}
//this._checkValue(field, val);
this['_' + field] = val;
return;
}
this._requireLoad();
if (this['_' + field] !== val) {
this._prepFieldChange(field);
switch (field) {
default:
this['_' + field] = val;
} }
} });
} }
/* /**
* Build group from database * Non-prototype properties
*/ */
Zotero.Group.prototype.load = Zotero.Promise.coroutine(function* () {
var id = this._id; Zotero.defineProperty(Zotero.Group, '_dbColumns', {
value: Object.freeze(['name', 'description', 'version'])
if (!id) { });
throw new Error("ID not set");
} Zotero.Group._colToProp = function(c) {
return "_group" + Zotero.Utilities.capitalize(c);
var sql = "SELECT G.* FROM groups G WHERE groupID=?"; }
var data = yield Zotero.DB.rowQueryAsync(sql, id);
if (!data) { Zotero.defineProperty(Zotero.Group, '_rowSQLSelect', {
this._loaded = true; value: Zotero.Library._rowSQLSelect + ", G.groupID, "
+ Zotero.Group._dbColumns.map(function(c) "G." + c + " AS " + Zotero.Group._colToProp(c)).join(", ")
});
Zotero.defineProperty(Zotero.Group, '_rowSQL', {
value: "SELECT " + Zotero.Group._rowSQLSelect
+ " FROM groups G JOIN libraries L USING (libraryID)"
});
Zotero.extendClass(Zotero.Library, Zotero.Group);
Zotero.defineProperty(Zotero.Group.prototype, '_objectType', {
value: 'group'
});
Zotero.defineProperty(Zotero.Group.prototype, 'libraryTypes', {
value: Object.freeze(Zotero.Group._super.prototype.libraryTypes.concat(['group']))
});
Zotero.defineProperty(Zotero.Group.prototype, 'groupID', {
get: function() this._groupID,
set: function(v) this._groupID = v
});
Zotero.defineProperty(Zotero.Group.prototype, 'id', {
get: function() this.groupID,
set: function(v) this.groupID = v
});
// Create accessors
(function() {
let accessors = ['name', 'description', 'version'];
for (let i=0; i<accessors.length; i++) {
let name = accessors[i];
let prop = Zotero.Group._colToProp(name);
Zotero.defineProperty(Zotero.Group.prototype, name, {
get: function() this._get(prop),
set: function(v) this._set(prop, v)
})
}
})();
Zotero.Group.prototype._isValidGroupProp = function(prop) {
let preffix = '_group';
if (prop.indexOf(preffix) !== 0 || prop.length == preffix.length) {
return false; return false;
} }
this.loadFromRow(data); let col = prop.substr(preffix.length);
col = col.charAt(0).toLowerCase() + col.substr(1);
return true; return Zotero.Group._dbColumns.indexOf(col) != -1;
}); }
Zotero.Group.prototype._isValidProp = function(prop) {
return this._isValidGroupProp(prop)
|| Zotero.Group._super.prototype._isValidProp.call(this, prop);
}
/* /*
* Populate group data from a database row * Populate group data from a database row
*/ */
Zotero.Group.prototype.loadFromRow = function(row) { Zotero.Group.prototype._loadDataFromRow = function(row) {
this._loaded = true; Zotero.Group._super.prototype._loadDataFromRow.call(this, row);
this._changed = false;
this._hasCollections = null;
this._hasSearches = null;
this._id = row.groupID; this._groupID = row.groupID;
this._libraryID = row.libraryID; this._groupName = row._groupName;
this._name = row.name; this._groupDescription = row._groupDescription;
this._description = row.description; this._groupVersion = row._groupVersion;
this._editable = Zotero.Libraries.isEditable(row.libraryID);
this._filesEditable = Zotero.Libraries.isFilesEditable(row.libraryID);
this._version = row.version;
} }
Zotero.Group.prototype._set = function(prop, val) {
/** switch(prop) {
* Check if group exists in the database case '_groupVersion':
* let newVal = Number.parseInt(val, 10);
* @return bool TRUE if the group exists, FALSE if not if (newVal != val) {
*/ throw new Error(prop + ' must be an integer');
Zotero.Group.prototype.exists = function() { }
return Zotero.Groups.exists(this.id); val = newVal
}
if (val < 0) {
throw new Error(prop + ' must be non-negative');
Zotero.Group.prototype.hasCollections = function () { }
if (this._hasCollections !== null) {
return this._hasCollections; // Ensure that it is never decreasing
} if (val < this._groupVersion) {
this._requireLoad(); throw new Error(prop + ' cannot decrease');
return false; }
}
break;
case '_groupName':
Zotero.Group.prototype.hasSearches = function () { case '_groupDescription':
if (this._hasSearches !== null) { if (typeof val != 'string') {
return this._hasSearches; throw new Error(prop + ' must be a string');
} }
this._requireLoad(); break;
return false;
}
Zotero.Group.prototype.clearCollectionCache = function () {
this._hasCollections = null;
}
Zotero.Group.prototype.clearSearchCache = function () {
this._hasSearches = null;
}
Zotero.Group.prototype.hasItem = function (item) {
if (!(item instanceof Zotero.Item)) {
throw new Error("item must be a Zotero.Item");
}
return item.libraryID == this.libraryID;
}
Zotero.Group.prototype.save = Zotero.Promise.coroutine(function* () {
if (!this.id) throw new Error("Group id not set");
if (!this.name) throw new Error("Group name not set");
if (!this.version) throw new Error("Group version not set");
if (!this._changed) {
Zotero.debug("Group " + this.id + " has not changed");
return false;
} }
yield Zotero.DB.executeTransaction(function* () { return Zotero.Group._super.prototype._set.call(this, prop, val);
var isNew = !this.exists(); }
Zotero.debug("Saving group " + this.id); Zotero.Group.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
let sql = Zotero.Group._rowSQL + " WHERE G.groupID=?";
var sqlColumns = [ let row = yield Zotero.DB.rowQueryAsync(sql, [this.groupID]);
'groupID', this._loadDataFromRow(row);
'name',
'description',
'version'
];
var sqlValues = [
this.id,
this.name,
this.description,
this.version
];
if (isNew) {
let { id: libraryID } = yield Zotero.Libraries.add(
'group', this.editable, this.filesEditable
);
sqlColumns.push('libraryID');
sqlValues.push(libraryID);
let sql = "INSERT INTO groups (" + sqlColumns.join(', ') + ") "
+ "VALUES (" + sqlColumns.map(() => '?').join(', ') + ")";
yield Zotero.DB.queryAsync(sql, sqlValues);
}
else {
sqlColumns.shift();
sqlValues.push(sqlValues.shift());
let sql = "UPDATE groups SET " + sqlColumns.map(function (val) val + '=?').join(', ')
+ " WHERE groupID=?";
yield Zotero.DB.queryAsync(sql, sqlValues);
yield Zotero.Libraries.setEditable(this.libraryID, this.editable);
yield Zotero.Libraries.setFilesEditable(this.libraryID, this.filesEditable);
}
if (isNew) {
Zotero.DB.addCurrentCallback("commit", Zotero.Promise.coroutine(function* () {
yield this.load();
Zotero.Groups.register(this)
}.bind(this)));
Zotero.Notifier.queue('add', 'group', this.id);
}
else {
Zotero.Notifier.queue('modify', 'group', this.id);
}
}.bind(this));
}); });
Zotero.Group.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
let proceed = yield Zotero.Group._super.prototype._initSave.call(this, env);
if (!proceed) return false;
if (!this._groupName) throw new Error("Group name not set");
if (typeof this._groupDescription != 'string') throw new Error("Group description not set");
if (!(this._groupVersion >= 0)) throw new Error("Group version not set");
if (!this._groupID) throw new Error("Group ID not set");
return true;
});
/** Zotero.Group.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
* Deletes group and all descendant objects yield Zotero.Group._super.prototype._saveData.call(this, env);
**/
Zotero.Group.prototype.erase = Zotero.Promise.coroutine(function* () {
Zotero.debug("Removing group " + this.id);
Zotero.DB.requireTransaction(); let changedCols = [], params = [];
for (let i=0; i<Zotero.Group._dbColumns.length; i++) {
// Delete items let col = Zotero.Group._dbColumns[i];
var types = ['item', 'collection', 'search']; let prop = Zotero.Group._colToProp(col);
for (let type of types) {
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); if (!this._changed[prop]) continue;
let sql = "SELECT " + objectsClass.idColumn + " FROM " + objectsClass.table
+ " WHERE libraryID=?"; changedCols.push(col);
ids = yield Zotero.DB.columnQueryAsync(sql, this.libraryID); params.push(this[prop]);
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let obj = yield objectsClass.getAsync(id, { noCache: true });
// Descendent object may have already been deleted
if (!obj) {
continue;
}
yield obj.erase({
skipNotifier: true
});
}
} }
// Delete library row, which deletes from tags, syncDeleteLog, syncedSettings, and groups if (env.isNew) {
// tables via cascade. If any of those gain caching, they should be deleted separately. changedCols.push('groupID', 'libraryID');
var sql = "DELETE FROM libraries WHERE libraryID=?"; params.push(this.groupID, this.libraryID);
yield Zotero.DB.queryAsync(sql, this.libraryID)
let sql = "INSERT INTO groups (" + changedCols.join(', ') + ") "
+ "VALUES (" + Array(params.length).fill('?').join(', ') + ")";
yield Zotero.DB.queryAsync(sql, params);
Zotero.Notifier.queue('add', 'group', this.groupID);
}
else if (changedCols.length) {
let sql = "UPDATE groups SET " + changedCols.map(function (v) v + '=?').join(', ')
+ " WHERE groupID=?";
params.push(this.groupID);
yield Zotero.DB.queryAsync(sql, params);
Zotero.Notifier.queue('modify', 'group', this.groupID);
}
else {
Zotero.debug("Group data did not change for group " + this.groupID, 5);
}
});
Zotero.Group.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
yield Zotero.Group._super.prototype._finalizeSave.call(this, env);
Zotero.DB.addCurrentCallback('commit', function () { if (env.isNew) {
Zotero.Groups.unregister(this.id); Zotero.Groups.register(this);
//yield Zotero.purgeDataObjects(); }
}.bind(this)) });
var notifierData = {};
notifierData[this.id] = { Zotero.Group.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
let notifierData = {};
notifierData[this.groupID] = {
libraryID: this.libraryID libraryID: this.libraryID
}; };
Zotero.Notifier.queue('delete', 'group', this.id, notifierData); Zotero.Notifier.queue('delete', 'group', this.groupID, notifierData);
Zotero.Groups.unregister(this.groupID);
yield Zotero.Group._super.prototype._finalizeErase.call(this, env);
}); });
Zotero.Group.prototype.eraseTx = function () {
return Zotero.DB.executeTransaction(function* () {
return this.erase();
}.bind(this));
}
Zotero.Group.prototype.fromJSON = function (json, userID) { Zotero.Group.prototype.fromJSON = function (json, userID) {
this._requireLoad(); if (json.name !== undefined) this.name = json.name;
if (json.description !== undefined) this.description = json.description;
this.name = json.name;
this.description = json.description;
var editable = false; var editable = false;
var filesEditable = false; var filesEditable = false;
@ -335,14 +260,6 @@ Zotero.Group.prototype.fromJSON = function (json, userID) {
this.filesEditable = filesEditable; this.filesEditable = filesEditable;
} }
Zotero.Group.prototype._requireLoad = function () {
if (!this._loaded && Zotero.Groups.exists(this._id)) {
throw new Error("Group has not been loaded");
}
}
Zotero.Group.prototype._prepFieldChange = function (field) { Zotero.Group.prototype._prepFieldChange = function (field) {
if (!this._changed) { if (!this._changed) {
this._changed = {}; this._changed = {};

View file

@ -25,24 +25,48 @@
Zotero.Groups = new function () { Zotero.Groups = new function () {
this.__defineGetter__('addGroupURL', function () ZOTERO_CONFIG.WWW_BASE_URL + 'groups/new/'); Zotero.defineProperty(this, 'addGroupURL', {
value: ZOTERO_CONFIG.WWW_BASE_URL + 'groups/new/'
var _cache = {};
var _groupIDsByLibraryID = {};
var _libraryIDsByGroupID = {};
this.init = Zotero.Promise.coroutine(function* () {
yield _load();
}); });
this._cache = null;
this._makeCache = function() {
return {
groupIDByLibraryID: {},
libraryIDByGroupID: {}
};
}
this.register = function (group) {
if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
Zotero.debug("Zotero.Groups: Registering group " + group.id + " (" + group.libraryID + ")", 5);
this._addToCache(this._cache, group);
}
this._addToCache = function (cache, group) {
cache.libraryIDByGroupID[group.id] = group.libraryID;
cache.groupIDByLibraryID[group.libraryID] = group.id;
}
this.unregister = function (groupID) {
if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
let libraryID = this._cache.libraryIDByGroupID[groupID];
Zotero.debug("Zotero.Groups: Unegistering group " + groupID + " (" + libraryID + ")", 5);
delete this._cache.groupIDByLibraryID[libraryID];
delete this._cache.libraryIDByGroupID[groupID];
}
this.init = Zotero.Promise.method(function() {
// Cache initialized in Zotero.Libraries
})
/** /**
* @param {Integer} id - Group id * @param {Integer} id - Group id
* @return {Zotero.Group} * @return {Zotero.Group}
*/ */
this.get = function (id) { this.get = function (id) {
if (!id) throw new Error("groupID not provided"); return Zotero.Libraries.get(this.getLibraryIDFromGroupID(id));
return _cache[id] ? _cache[id] : false;
} }
@ -52,7 +76,10 @@ Zotero.Groups = new function () {
* @return {Zotero.Group[]} * @return {Zotero.Group[]}
*/ */
this.getAll = function () { this.getAll = function () {
var groups = [for (id of Object.keys(_cache)) _cache[id]]; if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
var groups = Object.keys(this._cache.groupIDByLibraryID)
.map(id => Zotero.Libraries.get(id));
var collation = Zotero.getLocaleCollation(); var collation = Zotero.getLocaleCollation();
groups.sort(function(a, b) { groups.sort(function(a, b) {
return collation.compareString(1, a.name, b.name); return collation.compareString(1, a.name, b.name);
@ -62,18 +89,21 @@ Zotero.Groups = new function () {
this.getByLibraryID = function (libraryID) { this.getByLibraryID = function (libraryID) {
var groupID = this.getGroupIDFromLibraryID(libraryID); return Zotero.Libraries.get(libraryID);
return this.get(groupID);
} }
this.exists = function (groupID) { this.exists = function (groupID) {
return !!_libraryIDsByGroupID[groupID]; if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
return !!this._cache.libraryIDByGroupID[groupID];
} }
this.getGroupIDFromLibraryID = function (libraryID) { this.getGroupIDFromLibraryID = function (libraryID) {
var groupID = _groupIDsByLibraryID[libraryID]; if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
var groupID = this._cache.groupIDByLibraryID[libraryID];
if (!groupID) { if (!groupID) {
throw new Error("Group with libraryID " + libraryID + " does not exist"); throw new Error("Group with libraryID " + libraryID + " does not exist");
} }
@ -82,40 +112,8 @@ Zotero.Groups = new function () {
this.getLibraryIDFromGroupID = function (groupID) { this.getLibraryIDFromGroupID = function (groupID) {
var libraryID = _libraryIDsByGroupID[groupID]; if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
if (!libraryID) {
throw new Error("Group with groupID " + groupID + " does not exist"); return this._cache.libraryIDByGroupID[groupID] || false;
}
return libraryID;
} }
this.register = function (group) {
_libraryIDsByGroupID[group.id] = group.libraryID;
_groupIDsByLibraryID[group.libraryID] = group.id;
_cache[group.id] = group;
}
this.unregister = function (groupID) {
var libraryID = _libraryIDsByGroupID[groupID];
delete _groupIDsByLibraryID[libraryID];
delete _libraryIDsByGroupID[groupID];
delete _cache[groupID];
}
var _load = Zotero.Promise.coroutine(function* () {
var sql = "SELECT libraryID, groupID FROM groups";
var rows = yield Zotero.DB.queryAsync(sql)
for (let i=0; i<rows.length; i++) {
let row = rows[i];
_groupIDsByLibraryID[row.libraryID] = row.groupID;
_libraryIDsByGroupID[row.groupID] = row.libraryID;
let group = new Zotero.Group;
group.id = row.groupID;
yield group.load();
_cache[row.groupID] = group;
}
});
} }

View file

@ -99,6 +99,8 @@ Zotero.Items = function() {
+ "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID) " + "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID) "
+ "LEFT JOIN charsets CS ON (IA.charsetID=CS.charsetID)"; + "LEFT JOIN charsets CS ON (IA.charsetID=CS.charsetID)";
this._relationsTable = "itemRelations";
/** /**
* Return items marked as deleted * Return items marked as deleted
* *
@ -505,7 +507,7 @@ Zotero.Items = function() {
} }
if (!item.isEditable()) { if (!item.isEditable()) {
throw new Error(item._ObjectType + " (" + item.id + ") is not editable"); throw new Error(item._ObjectType + " " + item.libraryKey + " is not editable");
} }
if (!Zotero.Libraries.hasTrash(item.libraryID)) { if (!Zotero.Libraries.hasTrash(item.libraryID)) {

View file

@ -24,49 +24,135 @@
*/ */
Zotero.Libraries = new function () { Zotero.Libraries = new function () {
let _libraryData = {}, let _userLibraryID;
_userLibraryID,
_publicationsLibraryID,
_libraryDataLoaded = false;
Zotero.defineProperty(this, 'userLibraryID', { Zotero.defineProperty(this, 'userLibraryID', {
get: function() { get: function() {
if (!_libraryDataLoaded) { if (_userLibraryID === undefined) {
throw new Error("Library data not yet loaded"); throw new Error("Library data not yet loaded");
} }
return _userLibraryID; return _userLibraryID;
} }
}); });
let _publicationsLibraryID;
Zotero.defineProperty(this, 'publicationsLibraryID', { Zotero.defineProperty(this, 'publicationsLibraryID', {
get: function() { get: function() {
if (!_libraryDataLoaded) { if (_publicationsLibraryID === undefined) {
throw new Error("Library data not yet loaded"); throw new Error("Library data not yet loaded");
} }
return _publicationsLibraryID; return _publicationsLibraryID;
} }
}); });
/**
* Manage cache
*/
this._cache = null;
this._makeCache = function() {
return {};
}
this.register = function(library) {
if (!this._cache) throw new Error("Zotero.Libraries cache is not initialized");
Zotero.debug("Zotero.Libraries: Registering library " + library.libraryID, 5);
this._addToCache(this._cache, library);
};
this._addToCache = function(cache, library) {
if (!library.libraryID) throw new Error("Cannot register an unsaved library");
cache[library.libraryID] = library;
}
this.unregister = function(libraryID) {
if (!this._cache) throw new Error("Zotero.Libraries cache is not initialized");
Zotero.debug("Zotero.Libraries: Unregistering library " + libraryID, 5);
delete this._cache[libraryID];
};
/**
* Loads all libraries from DB. Groups, Feeds, etc. should not maintain an
* independent cache.
*/
this.init = Zotero.Promise.coroutine(function* () { this.init = Zotero.Promise.coroutine(function* () {
// Library data let specialLoading = ['feed', 'group'];
var sql = "SELECT * FROM libraries";
var rows = yield Zotero.DB.queryAsync(sql); // Invalidate caches until we're done loading everything
let libTypes = ['library'].concat(specialLoading);
let newCaches = {};
for (let i=0; i<libTypes.length; i++) {
let objs = Zotero.DataObjectUtilities.getObjectsClassForObjectType(libTypes[i]);
delete objs._cache;
newCaches[libTypes[i]] = objs._makeCache();
}
let sql = Zotero.Library._rowSQL
// Exclude libraries that require special loading
+ " WHERE type NOT IN "
+ "(" + Array(specialLoading.length).fill('?').join(',') + ")";
let rows = yield Zotero.DB.queryAsync(sql, specialLoading);
for (let i=0; i<rows.length; i++) { for (let i=0; i<rows.length; i++) {
let row = rows[i]; let row = rows[i];
_libraryData[row.libraryID] = parseDBRow(row);
if (row.libraryType == 'user') { let library;
_userLibraryID = row.libraryID; switch (row._libraryType) {
case 'user':
case 'publications':
library = new Zotero.Library();
library._loadDataFromRow(row); // Does not call save()
break;
default:
throw new Error('Unhandled library type "' + row._libraryType + '"');
} }
else if (row.libraryType == 'publications') {
_publicationsLibraryID = row.libraryID; if (library.libraryType == 'user') {
_userLibraryID = library.libraryID;
}
else if (library.libraryType == 'publications') {
_publicationsLibraryID = library.libraryID;
}
this._addToCache(newCaches.library, library);
}
// Load other libraries
for (let i=0; i<specialLoading.length; i++) {
let libType = specialLoading[i];
let LibType = Zotero.Utilities.capitalize(libType);
let libs = yield Zotero.DB.queryAsync(Zotero[LibType]._rowSQL);
for (let j=0; j<libs.length; j++) {
let lib = new Zotero[LibType]();
lib._loadDataFromRow(libs[j]);
this._addToCache(newCaches.library, lib);
Zotero[lib._ObjectTypePlural]._addToCache(newCaches[libType], lib);
} }
} }
_libraryDataLoaded = true;
// Set new caches
for (let libType in newCaches) {
Zotero.DataObjectUtilities.getObjectsClassForObjectType(libType)
._cache = newCaches[libType];
}
}); });
/**
* @param {Integer} libraryID
* @return {Boolean}
*/
this.exists = function(libraryID) {
if (!this._cache) throw new Error("Zotero.Libraries cache is not initialized");
return this._cache[libraryID] !== undefined;
}
this.exists = function (libraryID) {
return _libraryData[libraryID] !== undefined; this._ensureExists = function(libraryID) {
if (!this.exists(libraryID)) {
throw new Error("Invalid library ID " + libraryID);
}
} }
@ -74,166 +160,171 @@ Zotero.Libraries = new function () {
* @return {Integer[]} - All library IDs * @return {Integer[]} - All library IDs
*/ */
this.getAll = function () { this.getAll = function () {
return [for (x of Object.keys(_libraryData)) parseInt(x)] if (!this._cache) throw new Error("Zotero.Libraries cache is not initialized");
return Object.keys(this._cache).map(v => parseInt(v));
} }
/** /**
* @param {String} type - Library type * Get an existing library
* @param {Boolean} editable *
* @param {Boolean} filesEditable * @param {Integer} libraryID
* @return {Zotero.Library[] | Zotero.Library}
*/ */
this.add = Zotero.Promise.coroutine(function* (type, editable, filesEditable) { this.get = function(libraryID) {
Zotero.DB.requireTransaction(); return this._cache[libraryID] || false;
switch (type) {
case 'group':
break;
default:
throw new Error("Invalid library type '" + type + "'");
}
var libraryID = yield Zotero.ID.get('libraries');
var sql = "INSERT INTO libraries (libraryID, libraryType, editable, filesEditable) "
+ "VALUES (?, ?, ?, ?)";
var params = [
libraryID,
type,
editable ? 1 : 0,
filesEditable ? 1 : 0
];
yield Zotero.DB.queryAsync(sql, params);
// Re-fetch from DB to get auto-filled defaults
var sql = "SELECT * FROM libraries WHERE libraryID=?";
var row = yield Zotero.DB.rowQueryAsync(sql, [libraryID]);
return _libraryData[row.libraryID] = parseDBRow(row);
});
this.getName = function (libraryID) {
var type = this.getType(libraryID);
switch (type) {
case 'user':
return Zotero.getString('pane.collections.library');
case 'publications':
return Zotero.getString('pane.collections.publications');
case 'group':
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
var group = Zotero.Groups.get(groupID);
return group.name;
default:
throw new Error("Unsupported library type '" + type + "' in Zotero.Libraries.getName()");
}
}
this.getType = function (libraryID) {
if (!this.exists(libraryID)) {
throw new Error("Library data not loaded for library " + libraryID);
}
return _libraryData[libraryID].type;
} }
/** /**
* @deprecated
*/
this.getName = function (libraryID) {
Zotero.debug("Zotero.Libraries.getName() is deprecated. Use Zotero.Library.prototype.name instead");
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).name;
}
/**
* @deprecated
*/
this.getType = function (libraryID) {
Zotero.debug("Zotero.Libraries.getType() is deprecated. Use Zotero.Library.prototype.libraryType instead");
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).libraryType;
}
/**
* @deprecated
*
* @param {Integer} libraryID * @param {Integer} libraryID
* @return {Integer} * @return {Integer}
*/ */
this.getVersion = function (libraryID) { this.getVersion = function (libraryID) {
if (!this.exists(libraryID)) { Zotero.debug("Zotero.Libraries.getVersion() is deprecated. Use Zotero.Library.prototype.version instead");
throw new Error("Library data not loaded for library " + libraryID); this._ensureExists(libraryID);
} return Zotero.Libraries.get(libraryID).version;
return _libraryData[libraryID].version;
} }
/** /**
* @deprecated
*
* @param {Integer} libraryID * @param {Integer} libraryID
* @param {Integer} version - Library version, or -1 to indicate that a full sync is required * @param {Integer} version
* @return {Promise} * @return {Promise}
*/ */
this.setVersion = Zotero.Promise.coroutine(function* (libraryID, version) { this.setVersion = Zotero.Promise.method(function(libraryID, version) {
version = parseInt(version); Zotero.debug("Zotero.Libraries.setVersion() is deprecated. Use Zotero.Library.prototype.version instead");
var sql = "UPDATE libraries SET version=? WHERE libraryID=?"; this._ensureExists(libraryID);
yield Zotero.DB.queryAsync(sql, [version, libraryID]);
_libraryData[libraryID].version = version; let library = Zotero.Libraries.get(libraryID);
library.version = version;
return library.saveTx();
}); });
/**
* @deprecated
*/
this.getLastSyncTime = function (libraryID) { this.getLastSyncTime = function (libraryID) {
return _libraryData[libraryID].lastSyncTime; Zotero.debug("Zotero.Libraries.getLastSyncTime() is deprecated. Use Zotero.Library.prototype.lastSync instead");
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).lastSync;
}; };
/** /**
* @deprecated
*
* @param {Integer} libraryID * @param {Integer} libraryID
* @param {Date} lastSyncTime
* @return {Promise} * @return {Promise}
*/ */
this.updateLastSyncTime = function (libraryID) { this.setLastSyncTime = Zotero.Promise.method(function (libraryID, lastSyncTime) {
var d = new Date(); Zotero.debug("Zotero.Libraries.setLastSyncTime() is deprecated. Use Zotero.Library.prototype.lastSync instead");
_libraryData[libraryID].lastSyncTime = d; this._ensureExists(libraryID);
return Zotero.DB.queryAsync(
"UPDATE libraries SET lastsync=? WHERE libraryID=?", let library = Zotero.Libraries.get(libraryID);
[Math.round(d.getTime() / 1000), libraryID] library.lastSync = lastSyncTime;
); return library.saveTx();
});
/**
* @deprecated
*/
this.isEditable = function (libraryID) {
Zotero.debug("Zotero.Libraries.isEditable() is deprecated. Use Zotero.Library.prototype.editable instead");
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).editable;
}
/**
* @deprecated
*
* @return {Promise}
*/
this.setEditable = Zotero.Promise.method(function(libraryID, editable) {
Zotero.debug("Zotero.Libraries.setEditable() is deprecated. Use Zotero.Library.prototype.editable instead");
this._ensureExists(libraryID);
let library = Zotero.Libraries.get(libraryID);
library.editable = editable;
return library.saveTx();
});
/**
* @deprecated
*/
this.isFilesEditable = function (libraryID) {
Zotero.debug("Zotero.Libraries.isFilesEditable() is deprecated. Use Zotero.Library.prototype.filesEditable instead");
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).filesEditable;
}; };
this.isEditable = function (libraryID) {
return _libraryData[libraryID].editable;
}
/** /**
* @deprecated
*
* @return {Promise} * @return {Promise}
*/ */
this.setEditable = function (libraryID, editable) { this.setFilesEditable = Zotero.Promise.coroutine(function* (libraryID, filesEditable) {
if (editable == this.isEditable(libraryID)) { Zotero.debug("Zotero.Libraries.setFilesEditable() is deprecated. Use Zotero.Library.prototype.filesEditable instead");
return Zotero.Promise.resolve(); this._ensureExists(libraryID);
}
_libraryData[libraryID].editable = !!editable;
return Zotero.DB.queryAsync(
"UPDATE libraries SET editable=? WHERE libraryID=?", [editable ? 1 : 0, libraryID]
);
}
this.isFilesEditable = function (libraryID) {
return _libraryData[libraryID].filesEditable;
}
/**
* @return {Promise}
*/
this.setFilesEditable = function (libraryID, filesEditable) {
if (filesEditable == this.isFilesEditable(libraryID)) {
return Zotero.Promise.resolve();
}
_libraryData[libraryID].filesEditable = !!filesEditable;
return Zotero.DB.queryAsync(
"UPDATE libraries SET filesEditable=? WHERE libraryID=?", [filesEditable ? 1 : 0, libraryID]
);
}
this.isGroupLibrary = function (libraryID) {
if (!_libraryDataLoaded) {
throw new Error("Library data not yet loaded");
}
return this.getType(libraryID) == 'group'; let library = Zotero.Libraries.get(libraryID);
library.filesEditable = filesEditable;
return library.saveTx();
});
/**
* @deprecated
*/
this.isGroupLibrary = function (libraryID) {
Zotero.debug("Zotero.Libraries.isGroupLibrary() is deprecated. Use Zotero.Library.prototype.isGroup instead");
this._ensureExists(libraryID);
return !!Zotero.Libraries.get(libraryID).isGroup;
} }
function parseDBRow(row) { /**
return { * @deprecated
id: row.libraryID, */
type: row.libraryType, this.hasTrash = function (libraryID) {
editable: !!row.editable, Zotero.debug("Zotero.Libraries.hasTrash() is deprecated. Use Zotero.Library.prototype.hasTrash instead");
filesEditable: !!row.filesEditable, this._ensureExists(libraryID);
version: row.version, return Zotero.Libraries.get(libraryID).hasTrash;
lastSyncTime: row.lastsync != 0 ? new Date(row.lastsync * 1000) : false
};
} }
/**
* @deprecated
*/
this.updateLastSyncTime = Zotero.Promise.method(function(libraryID) {
Zotero.debug("Zotero.Libraries.updateLastSyncTime() is deprecated. Use Zotero.Library.prototype.updateLastSyncTime instead");
this._ensureExists(libraryID);
let library = Zotero.Libraries.get(libraryID);
library.updateLastSyncTime();
return library.saveTx()
.return();
})
} }

View file

@ -0,0 +1,518 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Library = function(params = {}) {
let objectType = this._objectType;
this._ObjectType = Zotero.Utilities.capitalize(objectType);
this._objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
this._ObjectTypePlural = Zotero.Utilities.capitalize(this._objectTypePlural);
this._changed = {};
this._hasCollections = null;
this._hasSearches = null;
Zotero.Utilities.assignProps(this, params, ['libraryType', 'editable',
'filesEditable', 'libraryVersion', 'lastSync']);
// Return a proxy so that we can disable the object once it's deleted
return new Proxy(this, {
get: function(obj, prop) {
if (obj._disabled && !(prop == 'libraryID' || prop == 'id')) {
throw new Error("Library (" + obj.libraryID + ") has been disabled");
}
return obj[prop];
}
});
};
/**
* Non-prototype properties
*/
// DB columns
Zotero.defineProperty(Zotero.Library, '_dbColumns', {
value: Object.freeze(['type', 'editable', 'filesEditable', 'version', 'lastSync'])
});
// Converts DB column name to (internal) object property
Zotero.Library._colToProp = function(c) {
return "_library" + Zotero.Utilities.capitalize(c);
}
// Select all columns in a unique manner, so we can JOIN tables with same column names (e.g. version)
Zotero.defineProperty(Zotero.Library, '_rowSQLSelect', {
value: "L.libraryID, " + Zotero.Library._dbColumns.map(function(c) "L." + c + " AS " + Zotero.Library._colToProp(c)).join(", ")
+ ", (SELECT COUNT(*)>0 FROM collections C WHERE C.libraryID=L.libraryID) AS hasCollections"
+ ", (SELECT COUNT(*)>0 FROM savedSearches S WHERE S.libraryID=L.libraryID) AS hasSearches"
});
// The actual select statement for above columns
Zotero.defineProperty(Zotero.Library, '_rowSQL', {
value: "SELECT " + Zotero.Library._rowSQLSelect + " FROM libraries L"
});
/**
* Prototype properties
*/
Zotero.defineProperty(Zotero.Library.prototype, '_objectType', {
value: 'library'
});
Zotero.defineProperty(Zotero.Library.prototype, '_childObjectTypes', {
value: Object.freeze(['item', 'collection', 'search'])
});
// Valid library types
Zotero.defineProperty(Zotero.Library.prototype, 'libraryTypes', {
value: Object.freeze(['user', 'publications'])
});
// Immutable libraries
Zotero.defineProperty(Zotero.Library.prototype, 'fixedLibraries', {
value: Object.freeze(['user', 'publications'])
});
Zotero.defineProperty(Zotero.Library.prototype, 'libraryID', {
get: function() this._libraryID,
set: function(id) { throw new Error("Cannot change library ID") }
});
Zotero.defineProperty(Zotero.Library.prototype, 'id', {
get: function() this.libraryID,
set: function(val) this.libraryID = val
});
Zotero.defineProperty(Zotero.Library.prototype, 'libraryType', {
get: function() this._get('_libraryType'),
set: function(v) this._set('_libraryType', v)
});
Zotero.defineProperty(Zotero.Library.prototype, 'libraryVersion', {
get: function() this._get('_libraryVersion'),
set: function(v) this._set('_libraryVersion', v)
});
Zotero.defineProperty(Zotero.Library.prototype, 'lastSync', {
get: function() this._get('_libraryLastSync')
});
Zotero.defineProperty(Zotero.Library.prototype, 'name', {
get: function() {
if (this._libraryType == 'user') {
return Zotero.getString('pane.collections.library');
}
if (this._libraryType == 'publications') {
return Zotero.getString('pane.collections.publications');
}
throw new Error('Unhandled library type "' + this._libraryType + '"');
}
});
Zotero.defineProperty(Zotero.Library.prototype, 'hasTrash', {
value: true
});
// Create other accessors
(function() {
let accessors = ['editable', 'filesEditable'];
for (let i=0; i<accessors.length; i++) {
let prop = Zotero.Library._colToProp(accessors[i]);
Zotero.defineProperty(Zotero.Library.prototype, accessors[i], {
get: function() this._get(prop),
set: function(v) this._set(prop, v)
})
}
})()
Zotero.Library.prototype._isValidProp = function(prop) {
let preffix = '_library';
if (prop.indexOf(preffix) !== 0 || prop.length == preffix.length) {
return false;
}
let col = prop.substr(preffix.length);
col = col.charAt(0).toLowerCase() + col.substr(1);
return Zotero.Library._dbColumns.indexOf(col) != -1;
}
Zotero.Library.prototype._get = function(prop) {
if (!this._isValidProp(prop)) {
throw new Error('Unknown property "' + prop + '"');
}
if (this._changed[prop]) {
// Catch attempts to retrieve unsaved property?
Zotero.debug('Warning: Attempting to retrieve unsaved ' + this._objectType + ' property "' + prop + '"', 2, true);
}
return this[prop];
}
Zotero.Library.prototype._set = function(prop, val) {
if (!this._isValidProp(prop)) {
throw new Error('Unknown property "' + prop + '"');
}
// Ensure proper format
switch(prop) {
case '_libraryType':
if (this.libraryTypes.indexOf(val) == -1) {
throw new Error('Invalid library type "' + val + '"');
}
if (this.libraryID !== undefined) {
throw new Error("Library type cannot be changed for a saved library");
}
if (this.fixedLibraries.indexOf(val) != -1) {
throw new Error('Cannot create library of type "' + val + '"');
}
break;
case '_libraryEditable':
case '_libraryFilesEditable':
if (['user', 'publications'].indexOf(this._libraryType) != -1) {
throw new Error('Cannot change ' + prop + ' for ' + this._libraryType + ' library');
}
val = !!val;
break;
case '_libraryVersion':
let newVal = Number.parseInt(val, 10);
if (newVal != val) throw new Error(prop + ' must be an integer');
val = newVal
// Allow -1 to indicate that a full sync is needed
if (val < -1) throw new Error(prop + ' must not be less than -1');
// Ensure that it is never decreasing, unless it is being set to -1
if (val != -1 && val < this._libraryVersion) throw new Error(prop + ' cannot decrease');
break;
case '_libraryLastSync':
if (!val) {
val = false;
} else if (!(val instanceof Date)) {
throw new Error(prop + ' must be a Date object or falsy');
} else {
// Storing to DB will drop milliseconds, so, for consistency, we drop it now
val = new Date(Math.floor(val.getTime()/1000) * 1000);
}
break;
}
if (this[prop] == val) return; // Unchanged
if (this._changed[prop]) {
// Catch attempts to re-set already set fields before saving
Zotero.debug('Warning: Attempting to set unsaved ' + this._objectType + ' property "' + prop + '"', 2, true);
}
this._changed[prop] = true;
this[prop] = val;
}
Zotero.Library.prototype._loadDataFromRow = function(row) {
if (this._libraryID !== undefined && this._libraryID !== row.libraryID) {
Zotero.debug("Warning: library ID changed in Zotero.Library._loadDataFromRow", 2, true);
}
this._libraryID = row.libraryID;
this._libraryType = row._libraryType;
this._libraryEditable = !!row._libraryEditable;
this._libraryFilesEditable = !!row._libraryFilesEditable;
this._libraryVersion = row._libraryVersion;
this._libraryLastSync = row._libraryLastSync != 0 ? new Date(row._libraryLastSync * 1000) : false;
this._hasCollections = !!row.hasCollections;
this._hasSearches = !!row.hasSearches;
this._changed = {};
}
Zotero.Library.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
let sql = Zotero.Library._rowSQL + ' WHERE libraryID=?';
let row = yield Zotero.DB.rowQueryAsync(sql, [this.libraryID]);
this._loadDataFromRow(row);
});
Zotero.Library.prototype.isChildObjectAllowed = function(type) {
return this._childObjectTypes.indexOf(type) != -1;
};
Zotero.Library.prototype.updateLastSyncTime = function() {
this._set('_libraryLastSync', new Date());
};
Zotero.Library.prototype.saveTx = function(options) {
options = options || {};
options.tx = true;
return this.save(options);
}
Zotero.Library.prototype.save = Zotero.Promise.coroutine(function* (options) {
options = options || {};
var env = {
options: options,
transactionOptions: options.transactionOptions || {}
};
if (!env.options.tx && !Zotero.DB.inTransaction()) {
Zotero.logError("save() called on Zotero.Library without a wrapping "
+ "transaction -- use saveTx() instead", 2, true);
env.options.tx = true;
}
var proceed = yield this._initSave(env)
.catch(Zotero.Promise.coroutine(function* (e) {
if (!env.isNew && Zotero.Libraries.exists(this.libraryID)) {
// Reload from DB and reset this._changed, so this is not a permanent failure
yield this._reloadFromDB();
}
throw e;
}).bind(this));
if (!proceed) return false;
if (env.isNew) {
Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4);
}
else {
Zotero.debug('Updating database with new ' + this._objectType + ' data', 4);
}
try {
env.notifierData = {};
// Create transaction
if (env.options.tx) {
return Zotero.DB.executeTransaction(function* () {
yield this._saveData(env);
yield this._finalizeSave(env);
}.bind(this), env.transactionOptions);
}
// Use existing transaction
else {
Zotero.DB.requireTransaction();
yield this._saveData(env);
yield this._finalizeSave(env);
}
} catch(e) {
Zotero.debug(e, 1);
throw e;
}
});
Zotero.Library.prototype._initSave = Zotero.Promise.method(function(env) {
if (this._libraryID === undefined) {
env.isNew = true;
if (!this._libraryType) {
throw new Error("libraryType must be set before saving");
}
if (typeof this._libraryEditable != 'boolean') {
throw new Error("editable must be set before saving");
}
if (typeof this._libraryFilesEditable != 'boolean') {
throw new Error("filesEditable must be set before saving");
}
} else {
Zotero.Libraries._ensureExists(this._libraryID);
if (!Object.keys(this._changed).length) {
Zotero.debug("No data changed in " + this._objectType + " " + this.id + ". Not saving.", 4);
return false;
}
}
return true;
});
Zotero.Library.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
// Collect changed columns
let changedCols = [],
params = [];
for (let i=0; i<Zotero.Library._dbColumns.length; i++) {
let col = Zotero.Library._dbColumns[i];
let prop = Zotero.Library._colToProp(col);
if (this._changed[prop]) {
changedCols.push(col);
let val = this[prop];
if (col == 'lastSync') {
// convert to integer
val = val ? Math.floor(val.getTime() / 1000) : 0;
}
if (typeof val == 'boolean') {
val = val ? 1 : 0;
}
params.push(val);
}
}
if (env.isNew) {
let id = yield Zotero.ID.get('libraries'); // Cannot retrieve auto-incremented ID with async queries
changedCols.unshift('libraryID');
params.unshift(id);
let sql = "INSERT INTO libraries (" + changedCols.join(", ") + ") "
+ "VALUES (" + Array(params.length).fill("?").join(", ") + ")";
yield Zotero.DB.queryAsync(sql, params);
this._libraryID = id;
} else if (changedCols.length) {
params.push(this.libraryID);
let sql = "UPDATE libraries SET " + changedCols.map(function(v) v + "=?").join(", ")
+ " WHERE libraryID=?";
yield Zotero.DB.queryAsync(sql, params);
} else {
Zotero.debug("Library data did not change for " + this._objectType + " " + this.id, 5);
}
});
Zotero.Library.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
this._changed = {};
if (env.isNew) {
// Re-fetch from DB to get auto-filled defaults
yield this._reloadFromDB();
Zotero.Libraries.register(this);
}
});
Zotero.Library.prototype.eraseTx = function(options) {
options = options || {};
options.tx = true;
return this.erase(options);
};
Zotero.Library.prototype.erase = Zotero.Promise.coroutine(function* (options) {
options = options || {};
var env = {
options: options,
transactionOptions: options.transactionOptions || {}
};
if (!env.options.tx && !Zotero.DB.inTransaction()) {
Zotero.logError("erase() called on Zotero." + this._ObjectType + " without a wrapping "
+ "transaction -- use eraseTx() instead");
Zotero.debug((new Error).stack, 2);
env.options.tx = true;
}
var proceed = yield this._initErase(env);
if (!proceed) return false;
Zotero.debug('Deleting ' + this._objectType + ' ' + this.id);
try {
env.notifierData = {};
if (env.options.tx) {
yield Zotero.DB.executeTransaction(function* () {
yield this._eraseData(env);
yield this._finalizeErase(env);
}.bind(this), env.transactionOptions);
} else {
Zotero.DB.requireTransaction();
yield this._eraseData(env);
yield this._finalizeErase(env);
}
} catch(e) {
Zotero.debug(e, 1);
throw e;
}
});
Zotero.Library.prototype._initErase = Zotero.Promise.method(function(env) {
if (this.libraryID === undefined) {
throw new Error("Attempting to erase an unsaved library");
}
Zotero.Libraries._ensureExists(this.libraryID);
if (this.fixedLibraries.indexOf(this._libraryType) != -1) {
throw new Error("Cannot erase library of type '" + this._libraryType + "'");
}
return true;
});
Zotero.Library.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID=?", this.libraryID);
});
Zotero.Library.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
Zotero.Libraries.unregister(this.libraryID);
// Clear cached child objects
for (let i=0; i<this._childObjectTypes.length; i++) {
let type = this._childObjectTypes[i];
Zotero.DataObjectUtilities.getObjectsClassForObjectType(type)
.dropDeadObjectsFromCache();
}
this._disabled = true;
});
Zotero.Library.prototype.hasCollections = function () {
if (this._hasCollections === null) {
throw new Error("Collection data has not been loaded");
}
return this._hasCollections;
}
Zotero.Library.prototype.updateCollections = Zotero.Promise.coroutine(function* () {
let sql = 'SELECT COUNT(*)>0 FROM collections WHERE libraryID=?';
this._hasCollections = !!(yield Zotero.DB.valueQueryAsync(sql, this.libraryID));
});
Zotero.Library.prototype.hasSearches = function () {
if (this._hasSearches === null) {
throw new Error("Saved search data has not been loaded");
}
return this._hasSearches;
}
Zotero.Library.prototype.updateSearches = Zotero.Promise.coroutine(function* () {
let sql = 'SELECT COUNT(*)>0 FROM savedSearches WHERE libraryID=?';
this._hasSearches = !!(yield Zotero.DB.valueQueryAsync(sql, this.libraryID));
});
Zotero.Library.prototype.hasItem = function (item) {
if (!(item instanceof Zotero.Item)) {
throw new Error("item must be a Zotero.Item");
}
return item.libraryID == this.libraryID;
}

View file

@ -31,7 +31,7 @@ Zotero.Notifier = new function(){
var _types = [ var _types = [
'collection', 'search', 'share', 'share-items', 'item', 'file', 'collection', 'search', 'share', 'share-items', 'item', 'file',
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'publications', 'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'publications',
'bucket', 'relation' 'bucket', 'relation', 'feed', 'feedItem'
]; ];
var _inTransaction; var _inTransaction;
var _locked = false; var _locked = false;

View file

@ -1469,7 +1469,7 @@ Zotero.Schema = new function(){
}); });
yield _updateDBVersion('compatibility', _maxCompatibility); yield _updateDBVersion('compatibility', _maxCompatibility);
var sql = "INSERT INTO libraries (libraryID, libraryType, editable, filesEditable) " var sql = "INSERT INTO libraries (libraryID, type, editable, filesEditable) "
+ "VALUES " + "VALUES "
+ "(?, 'user', 1, 1), " + "(?, 'user', 1, 1), "
+ "(4, 'publications', 1, 1)" + "(4, 'publications', 1, 1)"
@ -1943,9 +1943,9 @@ Zotero.Schema = new function(){
yield _updateDBVersion('compatibility', 1); yield _updateDBVersion('compatibility', 1);
yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld"); yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld");
yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n libraryID INTEGER PRIMARY KEY,\n libraryType TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n lastsync INT NOT NULL DEFAULT 0\n)"); yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n libraryID INTEGER PRIMARY KEY,\n type TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n lastsync INT NOT NULL DEFAULT 0\n)");
yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, libraryType, editable, filesEditable) VALUES (1, 'user', 1, 1)"); yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (1, 'user', 1, 1)");
yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, libraryType, editable, filesEditable) VALUES (4, 'publications', 1, 1)"); yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (4, 'publications', 1, 1)");
yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, libraryType, editable, filesEditable, 0, 0 FROM librariesOld JOIN groups USING (libraryID)"); yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, libraryType, editable, filesEditable, 0, 0 FROM librariesOld JOIN groups USING (libraryID)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')"); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')");
@ -2271,6 +2271,10 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("DROP TABLE itemsOld"); yield Zotero.DB.queryAsync("DROP TABLE itemsOld");
yield Zotero.DB.queryAsync("DROP TABLE tagsOld"); yield Zotero.DB.queryAsync("DROP TABLE tagsOld");
yield Zotero.DB.queryAsync("DROP TABLE librariesOld"); yield Zotero.DB.queryAsync("DROP TABLE librariesOld");
// Feeds
yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n libraryID INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n url TEXT NOT NULL UNIQUE,\n lastUpdate TIMESTAMP,\n lastCheck TIMESTAMP,\n lastCheckError TEXT,\n cleanupAfter INT,\n refreshInterval INT,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE TABLE feedItems (\n itemID INTEGER PRIMARY KEY,\n guid TEXT NOT NULL UNIQUE,\n readTime TIMESTAMP,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
} }
} }

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
*/ */
Zotero.Search = function() { Zotero.Search = function(params = {}) {
Zotero.Search._super.apply(this); Zotero.Search._super.apply(this);
this._name = null; this._name = null;
@ -35,6 +35,8 @@ Zotero.Search = function() {
this._maxSearchConditionID = -1; this._maxSearchConditionID = -1;
this._conditions = {}; this._conditions = {};
this._hasPrimaryConditions = false; this._hasPrimaryConditions = false;
Zotero.Utilities.assignProps(this, params, ['name', 'libraryID']);
} }
Zotero.extendClass(Zotero.DataObject, Zotero.Search); Zotero.extendClass(Zotero.DataObject, Zotero.Search);
@ -208,6 +210,9 @@ Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
if (env.isNew) { if (env.isNew) {
// Update library searches status
yield Zotero.Libraries.get(this.libraryID).updateSearches();
Zotero.Notifier.queue('add', 'search', this.id, env.notifierData); Zotero.Notifier.queue('add', 'search', this.id, env.notifierData);
} }
else if (!env.options.skipNotifier) { else if (!env.options.skipNotifier) {
@ -262,6 +267,13 @@ Zotero.Search.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync(sql, this.id); yield Zotero.DB.queryAsync(sql, this.id);
}); });
Zotero.Search.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
yield Zotero.Search._super.prototype._finalizeErase.call(this, env);
// Update library searches status
yield Zotero.Libraries.get(this.libraryID).updateSearches();
});
Zotero.Search.prototype.addCondition = function (condition, operator, value, required) { Zotero.Search.prototype.addCondition = function (condition, operator, value, required) {
this._requireData('conditions'); this._requireData('conditions');

View file

@ -429,7 +429,7 @@ Zotero.Sync.Runner_Module = function () {
} }
group.version = info.version; group.version = info.version;
group.fromJSON(info.data, Zotero.Users.getCurrentUserID()); group.fromJSON(info.data, Zotero.Users.getCurrentUserID());
yield group.save(); yield group.saveTx();
// Add group to library list // Add group to library list
libraries.push(group.libraryID); libraries.push(group.libraryID);

View file

@ -25,23 +25,30 @@
Zotero.URI = new function () { Zotero.URI = new function () {
this.__defineGetter__('defaultPrefix', function () 'http://zotero.org/'); Zotero.defineProperty(this, 'defaultPrefix', {
value: 'http://zotero.org/'
var _baseURI = ZOTERO_CONFIG.BASE_URI; });
var _apiURI = ZOTERO_CONFIG.API_URI;
// This should match all possible URIs. Match groups are as follows:
// 1: users|groups
// 2: local/|NULL
// 3: userID|groupID|localUserKey
// 4: publications|feeds/libraryID|NULL
// 5: items|collections|NULL
// 6: itemKey|collectionKey|NULL
var uriPartsRe = new RegExp(
'^' + Zotero.Utilities.quotemeta(this.defaultPrefix)
+ '(users|groups)/(local/)?(\\w+)(?:/(publications|feeds/\\w+))?'
+ '(?:/(items|collections)/(\\w+))?'
);
/** /**
* Get a URI with the user's local key, if there is one * Get a URI with the user's local key, if there is one
* *
* @return {String|False} e.g., 'http://zotero.org/users/v3aG8nQf' * @return {String|False} e.g., 'http://zotero.org/users/local/v3aG8nQf'
*/ */
this.getLocalUserURI = function () { this.getLocalUserURI = function () {
var key = Zotero.Users.getLocalUserKey(); return this.defaultPrefix + "users/local/" + Zotero.Users.getLocalUserKey();
if (!key) {
return false;
}
return _baseURI + "users/local/" + key;
} }
@ -50,16 +57,13 @@ Zotero.URI = new function () {
* *
* @return {String} * @return {String}
*/ */
this.getCurrentUserURI = function (noLocal) { this.getCurrentUserURI = function () {
var userID = Zotero.Users.getCurrentUserID(); var userID = Zotero.Users.getCurrentUserID();
if (!userID && noLocal) {
throw new Error("Local userID not available and noLocal set in Zotero.URI.getCurrentUserURI()");
}
if (userID) { if (userID) {
return _baseURI + "users/" + userID; return this.defaultPrefix + "users/" + userID;
} }
return _baseURI + "users/local/" + Zotero.Users.getLocalUserKey(); return this.getLocalUserURI();
} }
@ -68,21 +72,12 @@ Zotero.URI = new function () {
if (!userID) { if (!userID) {
return false; return false;
} }
return _baseURI + "users/" + userID + "/items"; return this.getCurrentUserURI() + "/items";
} }
this.getLibraryURI = function (libraryID) { this.getLibraryURI = function (libraryID) {
try { return this.defaultPrefix + this.getLibraryPath(libraryID);
var path = this.getLibraryPath(libraryID);
}
catch (e) {
if (e.error == Zotero.Error.ERROR_USER_NOT_AVAILABLE) {
return this.getCurrentUserURI();
}
throw e;
}
return _baseURI + path;
} }
@ -97,14 +92,19 @@ Zotero.URI = new function () {
case 'publications': case 'publications':
var id = Zotero.Users.getCurrentUserID(); var id = Zotero.Users.getCurrentUserID();
if (!id) { if (!id) {
throw new Zotero.Error("User id not available", "USER_NOT_AVAILABLE"); id = 'local/' + Zotero.Users.getLocalUserKey();
} }
if (libraryType == 'publications') { if (libraryType == 'publications') {
return "users/" + id + "/publications"; return "users/" + id + "/" + libraryType;
} }
break; break;
case 'feed':
// Since feeds are not currently synced, generate a local URI
return "users/local/" + Zotero.Users.getLocalUserKey() + "/feeds/" + libraryID;
case 'group': case 'group':
var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID); var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
break; break;
@ -121,8 +121,7 @@ Zotero.URI = new function () {
* Return URI of item, which might be a local URI if user hasn't synced * Return URI of item, which might be a local URI if user hasn't synced
*/ */
this.getItemURI = function (item) { this.getItemURI = function (item) {
var baseURI = this.getLibraryURI(item.libraryID); return this._getObjectURI(item);
return baseURI + "/items/" + item.key;
} }
@ -130,7 +129,7 @@ Zotero.URI = new function () {
* Get path portion of item URI (e.g., users/6/items/ABCD1234 or groups/1/items/ABCD1234) * Get path portion of item URI (e.g., users/6/items/ABCD1234 or groups/1/items/ABCD1234)
*/ */
this.getItemPath = function (item) { this.getItemPath = function (item) {
return this.getLibraryPath(item.libraryID) + "/items/" + item.key; return this._getObjectPath(item);
} }
@ -138,8 +137,7 @@ Zotero.URI = new function () {
* Return URI of collection, which might be a local URI if user hasn't synced * Return URI of collection, which might be a local URI if user hasn't synced
*/ */
this.getCollectionURI = function (collection) { this.getCollectionURI = function (collection) {
var baseURI = this.getLibraryURI(collection.libraryID); return this._getObjectURI(item);
return baseURI + "/collections/" + collection.key;
} }
@ -147,7 +145,7 @@ Zotero.URI = new function () {
* Get path portion of collection URI (e.g., users/6/collections/ABCD1234 or groups/1/collections/ABCD1234) * Get path portion of collection URI (e.g., users/6/collections/ABCD1234 or groups/1/collections/ABCD1234)
*/ */
this.getCollectionPath = function (collection) { this.getCollectionPath = function (collection) {
return this.getLibraryPath(collection.libraryID) + "/collections/" + collection.key; return this._getObjectPath(collection);
} }
@ -161,25 +159,44 @@ Zotero.URI = new function () {
* @return {String} * @return {String}
*/ */
this.getGroupURI = function (group, webRoot) { this.getGroupURI = function (group, webRoot) {
var uri = _baseURI + "groups/" + group.id; var uri = this._getObjectURI(group);
if (webRoot) { if (webRoot) {
uri = uri.replace(ZOTERO_CONFIG.BASE_URI, ZOTERO_CONFIG.WWW_BASE_URL); uri = uri.replace(ZOTERO_CONFIG.BASE_URI, ZOTERO_CONFIG.WWW_BASE_URL);
} }
return uri; return uri;
} }
this._getObjectPath = function(obj) {
let path = this.getLibraryPath(obj.libraryID);
if (obj instanceof Zotero.Library) {
return path;
}
if (obj instanceof Zotero.Item) {
return path + '/items/' + obj.key;
}
if (obj instanceof Zotero.Collection) {
return path + '/collections/' + obj.key;
}
throw new Error("Unsupported object type '" + obj._objectType + "'");
}
this._getObjectURI = function(obj) {
return this.defaultPrefix + this._getObjectPath(obj);
}
/** /**
* Convert an item URI into an item * Convert an item URI into an item
* *
* @param {String} itemURI * @param {String} itemURI
* @param {Zotero.Item|FALSE}
* @return {Promise<Zotero.Item|FALSE>} * @return {Promise<Zotero.Item|FALSE>}
*/ */
this.getURIItem = Zotero.Promise.method(function (itemURI) { this.getURIItem = Zotero.Promise.method(function (itemURI) {
var {libraryID, key} = this._getURIObject(itemURI, 'item'); var obj = this._getURIObject(itemURI, 'item');
if (!key) return false; if (!obj) return false;
return Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); return Zotero.Items.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
}); });
@ -197,9 +214,9 @@ Zotero.URI = new function () {
* @return {Integer|FALSE} - itemID of matching item, or FALSE if none * @return {Integer|FALSE} - itemID of matching item, or FALSE if none
*/ */
this.getURIItemID = function (itemURI) { this.getURIItemID = function (itemURI) {
var {libraryID, key} = this._getURIObject(itemURI, 'item'); var obj = this._getURIObject(itemURI, 'item');
if (!key) return false; if (!obj) return false;
return Zotero.Items.getIDFromLibraryAndKey(libraryID, key); return Zotero.Items.getIDFromLibraryAndKey(obj.libraryID, obj.key);
} }
@ -211,9 +228,9 @@ Zotero.URI = new function () {
* @return {Promise<Zotero.Collection|FALSE>} * @return {Promise<Zotero.Collection|FALSE>}
*/ */
this.getURICollection = Zotero.Promise.method(function (collectionURI) { this.getURICollection = Zotero.Promise.method(function (collectionURI) {
var {libraryID, key} = this._getURIObject(collectionURI, 'collection'); var obj = this._getURIObject(collectionURI, 'collection');
if (!key) return false; if (!obj) return false;
return Zotero.Collections.getByLibraryAndKeyAsync(libraryID, key); return Zotero.Collections.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
}); });
@ -222,7 +239,7 @@ Zotero.URI = new function () {
* @return {Object|FALSE} - Object with 'libraryID' and 'key', or FALSE if item not found * @return {Object|FALSE} - Object with 'libraryID' and 'key', or FALSE if item not found
*/ */
this.getURICollectionLibraryKey = function (collectionURI) { this.getURICollectionLibraryKey = function (collectionURI) {
return this._getURIObject(collectionURI, 'collection'); return this._getURIObject(collectionURI, 'collection');;
} }
@ -231,9 +248,9 @@ Zotero.URI = new function () {
* @return {Integer|FALSE} - collectionID of matching collection, or FALSE if none * @return {Integer|FALSE} - collectionID of matching collection, or FALSE if none
*/ */
this.getURICollectionID = function (collectionURI) { this.getURICollectionID = function (collectionURI) {
var {libraryID, key} = this._getURIObject(collectionURI, 'item'); var obj = this._getURIObject(collectionURI, 'collection');
if (!key) return false; if (!obj) return false;
return Zotero.Collections.getIDFromLibraryAndKey(libraryID, key); return Zotero.Collections.getIDFromLibraryAndKey(obj.libraryID, obj.key);
} }
@ -244,113 +261,81 @@ Zotero.URI = new function () {
* @return {Integer|FALSE} - libraryID, or FALSE if no matching library * @return {Integer|FALSE} - libraryID, or FALSE if no matching library
*/ */
this.getURILibrary = function (libraryURI) { this.getURILibrary = function (libraryURI) {
var {libraryID} = this._getURIObject(libraryURI, "library"); let library = this._getURIObjectLibrary(libraryURI);
return libraryID !== undefined ? libraryID : false; return libraryID ? library.libraryID : false;
} }
/** /**
* Convert an object URI into an object (item, collection, etc.) * Convert an object URI into an object containing libraryID and key
*
* @param {String} objectURI
* @param {String} [type] Object type to expect
* @return {Object|FALSE} - An object containing libraryID, objectType and
* key. Key and objectType may not be present if the URI references a
* library itself
*/
this._getURIObject = function (objectURI, type) {
let uri = objectURI.replace(/\/+$/, ''); // Drop trailing /
let uriParts = uri.match(uriPartsRe);
if (!uriParts) {
throw new Error("Could not parse object URI " + objectURI);
}
let library = this._getURIObjectLibrary(objectURI);
if (!library) return false;
let retObj = {libraryID: library.libraryID};
if (!uriParts[5]) {
// References the library itself
return retObj;
}
retObj.objectType = uriParts[5] == 'items' ? 'item' : 'collection';
retObj.key = uriParts[6];
if (type && type != retObj.objectType) return false;
return retObj;
};
/**
* Convert an object URI into a Zotero.Library that the object is in
* *
* @param {String} objectURI * @param {String} objectURI
* @param {'library'|'collection'|'item'} - The type of URI to expect * @return {Zotero.Library|FALSE} - An object referenced by the URI
* @return {Object|FALSE} - An object containing 'libraryID' and, if applicable, 'key',
* or FALSE if library not found
*/ */
this._getURIObject = function (objectURI, type) { this._getURIObjectLibrary = function (objectURI) {
var libraryType; let uri = objectURI.replace(/\/+$/, ''); // Drop trailing "/"
var libraryTypeID; let uriParts = uri.match(uriPartsRe);
// If this is a local URI, compare to the local user key if (!uriParts) {
if (objectURI.match(/\/users\/local\//)) { throw new Error("Could not parse object URI " + objectURI);
// For now, at least, don't check local id
/*
var localUserURI = this.getLocalUserURI();
if (localUserURI) {
localUserURI += "/";
if (objectURI.indexOf(localUserURI) == 0) {
objectURI = objectURI.substr(localUserURI.length);
var libraryType = 'user';
var id = null;
}
}
*/
libraryType = 'user';
libraryTypeID = null;
} }
// If not found, try global URI let library;
if (!libraryType) { if (uriParts[1] == 'users') {
if (!objectURI.startsWith(_baseURI)) { let type = uriParts[4];
throw new Error("Invalid base URI '" + objectURI + "'"); if (type == 'publications') {
} library = Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
objectURI = objectURI.substr(_baseURI.length); } else if (!type) {
let typeRE = /^(users|groups)\/([0-9]+)(?:\/|$)/; // Handles local and synced libraries
let matches = objectURI.match(typeRE); library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
if (!matches) { } else {
throw new Error("Invalid library URI '" + objectURI + "'"); let feedID = type.split('/')[1];
} library = Zotero.Libraries.get(feedID);
libraryType = matches[1].substr(0, matches[1].length-1);
libraryTypeID = matches[2];
objectURI = objectURI.replace(typeRE, '');
}
if (libraryType == 'user' && objectURI.startsWith('publications/')) {
libraryType = 'publications';
}
if (libraryType == 'user') {
var libraryID = Zotero.Libraries.userLibraryID;
}
else if (libraryType == 'group') {
if (!Zotero.Groups.exists(libraryTypeID)) {
return false;
}
var libraryID = Zotero.Groups.getLibraryIDFromGroupID(libraryTypeID);
}
else if (libraryType == 'publications') {
var libraryID = Zotero.Libraries.publicationsLibraryID;
}
if(type === 'library') {
if (libraryType == 'user') {
if (libraryTypeID) {
if (libraryTypeID == Zotero.Users.getCurrentUserID()) {
return {
libraryID: libraryID
};
}
}
else {
var localUserURI = this.getLocalUserURI();
if (localUserURI) {
localUserURI += "/";
if (objectURI.startsWith(localUserURI)) {
return {
libraryID: Zotero.Libraries.userLibraryID
};
}
}
}
return false;
}
if (libraryType == 'group') {
return {
libraryID: libraryID
};
} }
} else { } else {
var re = /(?:items|collections)\/([A-Z0-9]{8})/; // Group libraries
var matches = objectURI.match(re); library = Zotero.Groups.get(uriParts[3]);
if (!matches) {
throw ("Invalid object URI '" + objectURI + "' in Zotero.URI._getURIObject()");
}
let objectKey = matches[1];
return {
libraryID: libraryID,
key: objectKey
};
} }
if (!library) {
Zotero.debug("Could not find a library for URI " + objectURI, 2, true);
return false;
}
return library;
} }
} }

View file

@ -30,52 +30,55 @@ Zotero.Users = new function () {
var _localUserKey; var _localUserKey;
this.init = Zotero.Promise.coroutine(function* () { this.init = Zotero.Promise.coroutine(function* () {
var sql = "SELECT value FROM settings WHERE setting='account' AND key='userID'"; let sql = "SELECT key, value FROM settings WHERE setting='account'";
_userID = yield Zotero.DB.valueQueryAsync(sql); let rows = yield Zotero.DB.queryAsync(sql);
if (_userID) { let settings = {};
sql = "SELECT value FROM settings WHERE setting='account' AND key='libraryID'"; for (let i=0; i<rows.length; i++) {
_libraryID = yield Zotero.DB.valueQueryAsync(sql); settings[rows[i].key] = rows[i].value;
sql = "SELECT value FROM settings WHERE setting='account' AND key='username'";
_username = yield Zotero.DB.valueQueryAsync(sql);
} }
// If we don't have a global user id, generate a local user key
else { if (settings.userID) {
sql = "SELECT value FROM settings WHERE setting='account' AND key='localUserKey'"; _userID = settings.userID;
let key = yield Zotero.DB.valueQueryAsync(sql); _libraryID = settings.libraryID;
// Generate a local user key if we don't have one _username = settings.username;
if (!key) { }
key = Zotero.randomString(8);
sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)"; if (settings.localUserKey) {
yield Zotero.DB.queryAsync(sql, key); _localUserKey = settings.localUserKey;
} } else {
let key = Zotero.randomString(8);
sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)";
yield Zotero.DB.queryAsync(sql, key);
_localUserKey = key; _localUserKey = key;
} }
}); });
this.getCurrentUserID = () => _userID; this.getCurrentUserID = function() { return _userID };
this.setCurrentUserID = function (val) { this.setCurrentUserID = Zotero.Promise.coroutine(function* (val) {
val = parseInt(val); val = parseInt(val);
_userID = val; if (!(val > 0)) throw new Error("userID must be a positive integer");
var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)"; var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)";
return Zotero.DB.queryAsync(sql, val); yield Zotero.DB.queryAsync(sql, val);
}; _userID = val;
});
this.getCurrentUsername = () => _username; this.getCurrentUsername = () => _username;
this.setCurrentUsername = function (val) { this.setCurrentUsername = Zotero.Promise.coroutine(function* (val) {
_username = val; if (!val || typeof val != 'string') throw new Error('username must be a non-empty string');
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)"; var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
return Zotero.DB.queryAsync(sql, val); yield Zotero.DB.queryAsync(sql, val);
}; _username = val;
});
this.getLocalUserKey = function () { this.getLocalUserKey = function () {
if (!_localUserKey) {
throw new Error("Local user key not available");
}
return _localUserKey; return _localUserKey;
}; };
}; };

View file

@ -742,6 +742,21 @@ Zotero.Utilities = {
return retValues; return retValues;
}, },
/**
* Assign properties to an object
*
* @param {Object} target
* @param {Object} source
* @param {String[]} [props] Properties to assign. Assign all otherwise
*/
"assignProps": function(target, source, props) {
if (!props) props = Object.keys(source);
for (var i=0; i<props.length; i++) {
if (source[props[i]] === undefined) continue;
target[props[i]] = source[props[i]];
}
},
/** /**
* Generate a random integer between min and max inclusive * Generate a random integer between min and max inclusive

View file

@ -166,6 +166,7 @@ pane.collections.savedSearchName = Enter a name for this saved search:
pane.collections.rename = Rename collection: pane.collections.rename = Rename collection:
pane.collections.library = My Library pane.collections.library = My Library
pane.collections.publications = My Publications pane.collections.publications = My Publications
pane.collections.feeds = Feeds
pane.collections.groupLibraries = Group Libraries pane.collections.groupLibraries = Group Libraries
pane.collections.trash = Trash pane.collections.trash = Trash
pane.collections.untitled = Untitled pane.collections.untitled = Untitled

View file

@ -61,6 +61,8 @@ const xpcomFilesLocal = [
'attachments', 'attachments',
'cite', 'cite',
'cookieSandbox', 'cookieSandbox',
'data/library',
'data/libraries',
'data/dataObject', 'data/dataObject',
'data/dataObjects', 'data/dataObjects',
'data/dataObjectUtilities', 'data/dataObjectUtilities',
@ -70,11 +72,14 @@ const xpcomFilesLocal = [
'data/items', 'data/items',
'data/collection', 'data/collection',
'data/collections', 'data/collections',
'data/feedItem',
'data/feedItems',
'data/feed',
'data/feeds',
'data/creators', 'data/creators',
'data/group', 'data/group',
'data/groups', 'data/groups',
'data/itemFields', 'data/itemFields',
'data/libraries',
'data/relations', 'data/relations',
'data/tags', 'data/tags',
'db', 'db',

View file

@ -205,6 +205,25 @@ CREATE TABLE collectionRelations (
CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID); CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID);
CREATE INDEX collectionRelations_object ON collectionRelations(object); CREATE INDEX collectionRelations_object ON collectionRelations(object);
CREATE TABLE feeds (
libraryID INTEGER PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
lastUpdate TIMESTAMP,
lastCheck TIMESTAMP,
lastCheckError TEXT,
cleanupAfter INT,
refreshInterval INT,
FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE
);
CREATE TABLE feedItems (
itemID INTEGER PRIMARY KEY,
guid TEXT NOT NULL UNIQUE,
readTime TIMESTAMP,
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE
);
CREATE TABLE savedSearches ( CREATE TABLE savedSearches (
savedSearchID INTEGER PRIMARY KEY, savedSearchID INTEGER PRIMARY KEY,
savedSearchName TEXT NOT NULL, savedSearchName TEXT NOT NULL,
@ -238,11 +257,11 @@ CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted);
CREATE TABLE libraries ( CREATE TABLE libraries (
libraryID INTEGER PRIMARY KEY, libraryID INTEGER PRIMARY KEY,
libraryType TEXT NOT NULL, type TEXT NOT NULL,
editable INT NOT NULL, editable INT NOT NULL,
filesEditable INT NOT NULL, filesEditable INT NOT NULL,
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0,
lastsync INT NOT NULL DEFAULT 0 lastSync INT NOT NULL DEFAULT 0
); );
CREATE TABLE users ( CREATE TABLE users (

View file

@ -256,16 +256,15 @@ var getGroup = Zotero.Promise.method(function () {
}); });
var createGroup = Zotero.Promise.coroutine(function* (props) { var createGroup = Zotero.Promise.coroutine(function* (props = {}) {
props = props || {};
var group = new Zotero.Group; var group = new Zotero.Group;
group.id = props.id || Zotero.Utilities.rand(10000, 1000000); group.id = props.id || Zotero.Utilities.rand(10000, 1000000);
group.name = props.name || "Test " + Zotero.Utilities.randomString(); group.name = props.name || "Test " + Zotero.Utilities.randomString();
group.description = props.description || ""; group.description = props.description || "";
group.editable = props.editable || true; group.editable = props.editable === undefined ? true : props.editable;
group.filesEditable = props.filesEditable || true; group.filesEditable = props.filesEditable === undefined ? true : props.filesEditable;
group.version = props.version || Zotero.Utilities.rand(1000, 10000); group.version = props.version === undefined ? Zotero.Utilities.rand(1000, 10000) : props.version;
yield group.save(); yield group.saveTx();
return group; return group;
}); });
@ -292,15 +291,24 @@ function createUnsavedDataObject(objectType, params = {}) {
throw new Error("Object type not provided"); throw new Error("Object type not provided");
} }
if (objectType == 'item') { var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version'];
var param = params.itemType || 'book';
var itemType;
if (objectType == 'item' || objectType == 'feedItem') {
itemType = params.itemType || 'book';
allowedParams.push('dateAdded', 'dateModified');
} }
var obj = new Zotero[Zotero.Utilities.capitalize(objectType)](param);
if (params.libraryID) { if (objectType == 'feedItem') {
obj.libraryID = params.libraryID; params.guid = params.guid || Zotero.randomString();
allowedParams.push('guid');
} }
var obj = new Zotero[Zotero.Utilities.capitalize(objectType)](itemType);
switch (objectType) { switch (objectType) {
case 'item': case 'item':
case 'feedItem':
if (params.title !== undefined || params.setTitle) { if (params.title !== undefined || params.setTitle) {
obj.setField('title', params.title !== undefined ? params.title : Zotero.Utilities.randomString()); obj.setField('title', params.title !== undefined ? params.title : Zotero.Utilities.randomString());
} }
@ -311,21 +319,19 @@ function createUnsavedDataObject(objectType, params = {}) {
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString(); obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
break; break;
} }
var allowedParams = ['parentID', 'parentKey', 'synced', 'version'];
if (objectType == 'item') { Zotero.Utilities.assignProps(obj, params, allowedParams);
allowedParams.push('dateAdded', 'dateModified');
}
allowedParams.forEach(function (param) {
if (params[param] !== undefined) {
obj[param] = params[param];
}
})
return obj; return obj;
} }
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) { var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) {
var obj = createUnsavedDataObject(objectType, params); var obj = createUnsavedDataObject(objectType, params);
yield obj.saveTx(saveOptions); if (objectType == 'feedItem') {
yield obj.forceSaveTx(saveOptions);
} else {
yield obj.saveTx(saveOptions);
}
return obj; return obj;
}); });

View file

@ -61,4 +61,15 @@ describe("Zotero.Collections", function () {
assert.includeMembers(cols.map(col => col.id), [col3.id, col4.id]); assert.includeMembers(cols.map(col => col.id), [col3.id, col4.id]);
}) })
}) })
describe("#getAsync()", function() {
it("should return a collection item for a collection ID", function* () {
let collection = new Zotero.Collection({ name: 'foo' });
collection = yield Zotero.Collections.getAsync(yield collection.saveTx());
assert.notOk(collection.isFeed);
assert.instanceOf(collection, Zotero.Collection);
assert.notInstanceOf(collection, Zotero.Feed);
});
});
}) })

178
test/tests/feedItemTest.js Normal file
View file

@ -0,0 +1,178 @@
describe("Zotero.FeedItem", function () {
let feed, libraryID;
before(function* () {
feed = new Zotero.Feed({ name: 'Test ' + Zotero.randomString(), url: 'http://' + Zotero.randomString() + '.com/' });
yield feed.saveTx();
libraryID = feed.libraryID;
});
after(function() {
return feed.eraseTx();
});
it("should be an instance of Zotero.Item", function() {
assert.instanceOf(new Zotero.FeedItem(), Zotero.Item);
});
describe("#libraryID", function() {
it("should reference a feed", function() {
let feedItem = new Zotero.FeedItem();
assert.doesNotThrow(function() {feedItem.libraryID = feed.libraryID});
assert.throws(function() {feedItem.libraryID = Zotero.Libraries.userLibraryID}, /^libraryID must reference a feed$/);
});
});
describe("#constructor()", function* () {
it("should accept required fields as arguments", function* () {
let guid = Zotero.randomString();
let feedItem = new Zotero.FeedItem();
yield assert.isRejected(feedItem.forceSaveTx());
feedItem = new Zotero.FeedItem('book', { guid });
feedItem.libraryID = libraryID;
yield assert.isFulfilled(feedItem.forceSaveTx());
assert.equal(feedItem.itemTypeID, Zotero.ItemTypes.getID('book'));
assert.equal(feedItem.guid, guid);
assert.equal(feedItem.libraryID, libraryID);
});
});
describe("#isFeedItem", function() {
it("should be true", function() {
let feedItem = new Zotero.FeedItem();
assert.isTrue(feedItem.isFeedItem);
});
it("should be falsy for regular item", function() {
let item = new Zotero.Item();
assert.notOk(item.isFeedItem);
})
});
describe("#guid", function() {
it("should not be settable to a non-string value", function() {
let feedItem = new Zotero.FeedItem();
assert.throws(() => feedItem.guid = 1);
});
it("should be settable to any string", function() {
let feedItem = new Zotero.FeedItem();
feedItem.guid = 'foo';
assert.equal(feedItem.guid, 'foo');
});
it("should not be possible to change guid after saving item", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID });
assert.throws(() => feedItem.guid = 'bar');
});
});
describe("#isRead", function() {
it("should be false by default", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID });
assert.isFalse(feedItem.isRead);
});
it("should be settable and persist after saving", function* () {
this.timeout(5000);
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
assert.isFalse(feedItem.isRead);
let expectedTimestamp = Date.now();
feedItem.isRead = true;
assert.isTrue(feedItem.isRead);
let readTime = Zotero.Date.sqlToDate(feedItem._feedItemReadTime, true).getTime();
assert.closeTo(readTime, expectedTimestamp, 2000, 'sets the read timestamp to current time');
feedItem.isRead = false;
assert.isFalse(feedItem.isRead);
assert.notOk(feedItem._feedItemReadTime);
expectedTimestamp = Date.now();
feedItem.isRead = true;
yield Zotero.Promise.delay(2001);
yield feedItem.forceSaveTx();
readTime = yield Zotero.DB.valueQueryAsync('SELECT readTime FROM feedItems WHERE itemID=?', feedItem.id);
readTime = Zotero.Date.sqlToDate(readTime, true).getTime();
assert.closeTo(readTime, expectedTimestamp, 2000, 'read timestamp is correct in the DB');
});
});
describe("#save()", function() {
it("should require edit check override", function* () {
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
yield assert.isRejected(feedItem.saveTx(), /^Error: Cannot edit feedItem in read-only Zotero library$/);
});
it("should require feed being set", function* () {
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
// Defaults to user library ID
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: Cannot add /);
});
it("should require GUID being set", function* () {
let feedItem = new Zotero.FeedItem('book');
feedItem.libraryID = feed.libraryID;
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: GUID must be set before saving FeedItem$/);
});
it("should require a unique GUID", function* () {
let guid = Zotero.randomString();
let feedItem1 = yield createDataObject('feedItem', { libraryID, guid });
let feedItem2 = createUnsavedDataObject('feedItem', { libraryID, guid });
yield assert.isRejected(feedItem2.forceSaveTx());
// But we should be able to save it after deleting the original feed
yield feedItem1.forceEraseTx();
yield assert.isFulfilled(feedItem2.forceSaveTx());
});
it("should require item type being set", function* () {
let feedItem = new Zotero.FeedItem(null, { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: Item type must be set before saving$/);
});
it("should save feed item", function* () {
let guid = Zotero.randomString();
let feedItem = createUnsavedDataObject('feedItem', { libraryID, guid });
yield assert.isFulfilled(feedItem.forceSaveTx());
feedItem = yield Zotero.FeedItems.getAsync(feedItem.id);
assert.ok(feedItem);
assert.equal(feedItem.guid, guid);
});
it.skip("should support saving feed items with all types and fields", function* () {
this.timeout(60000);
let allTypesAndFields = loadSampleData('allTypesAndFields'),
feedItems = [];
for (let type in allTypesAndFields) {
let feedItem = new Zotero.FeedItem(null, type, feed.libraryID);
feedItem.fromJSON(allTypesAndFields[type]);
yield feedItem.forceSaveTx();
feedItems.push(feedItem);
}
let feedItemsJSON = {};
for (let i=0; i<feedItems.length; i++) {
let feed = feedItems[i];
feedItemsJSON[feed.guid] = feed.toJSON();
}
assert.deepEqual(feedItemsJSON, allTypesAndFields);
});
it("should allow saving after editing data", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID });
feedItem.setField('title', 'bar');
yield assert.isFulfilled(feedItem.forceSaveTx());
assert.equal(feedItem.getField('title'), 'bar');
});
});
describe("#erase()", function() {
it("should erase an existing feed item", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID });
yield feedItem.forceEraseTx();
assert.isFalse(yield Zotero.FeedItems.getAsync(feedItem.id));
//yield assert.isRejected(feedItem.forceEraseTx(), "does not allow erasing twice");
});
it("should require edit check override to erase", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID });
yield assert.isRejected(feedItem.eraseTx(), /^Error: Cannot edit feedItem in read-only Zotero library$/);
});
});
});

View file

@ -0,0 +1,38 @@
describe("Zotero.FeedItems", function () {
let feed;
before(function() {
feed = new Zotero.Feed({ name: 'foo', url: 'http://' + Zotero.randomString() + '.com' });
return feed.saveTx();
});
after(function() {
return feed.eraseTx();
});
describe("#getIDFromGUID()", function() {
it("should return false for non-existent GUID", function* () {
let id = yield Zotero.FeedItems.getIDFromGUID(Zotero.randomString());
assert.isFalse(id);
});
it("should return feed item id from GUID", function* () {
let feedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
yield feedItem.forceSaveTx();
let id2 = yield Zotero.FeedItems.getIDFromGUID(feedItem.guid);
assert.equal(id2, feedItem.id);
});
});
describe("#getAsyncByGUID()", function() {
it("should return feed item from GUID", function* () {
let guid = Zotero.randomString();
let feedItem = yield createDataObject('feedItem', { guid, libraryID: feed.libraryID });
yield feedItem.forceSaveTx();
let feedItem2 = yield Zotero.FeedItems.getAsyncByGUID(guid);
assert.equal(feedItem2.id, feedItem.id);
});
it("should return false for non-existent GUID", function* () {
let feedItem = yield Zotero.FeedItems.getAsyncByGUID(Zotero.randomString());
assert.isFalse(feedItem);
});
});
});

206
test/tests/feedTest.js Normal file
View file

@ -0,0 +1,206 @@
describe("Zotero.Feed", function() {
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
let feed = new Zotero.Feed({
name: props.name || 'Test ' + Zotero.randomString(),
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
refreshInterval: props.refreshInterval,
cleanupAfter: props.cleanupAfter
});
yield feed.saveTx();
return feed;
});
// Clean up after after tests
after(function* () {
let feeds = Zotero.Feeds.getAll();
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<feeds.length; i++) {
yield feeds[i].erase();
}
});
});
it("should be an instance of Zotero.Library", function() {
let feed = new Zotero.Feed();
assert.instanceOf(feed, Zotero.Library);
});
describe("#constructor()", function() {
it("should accept required fields as arguments", function* () {
let feed = new Zotero.Feed();
yield assert.isRejected(feed.saveTx(), /^Error: Feed name not set$/);
feed = new Zotero.Feed({
name: 'Test ' + Zotero.randomString(),
url: 'http://www.' + Zotero.randomString() + '.com'
});
yield assert.isFulfilled(feed.saveTx());
});
});
describe("#isFeed", function() {
it("should be true", function() {
let feed = new Zotero.Feed();
assert.isTrue(feed.isFeed);
});
it("should be falsy for regular Library", function() {
let library = new Zotero.Library();
assert.notOk(library.isFeed);
});
});
describe("#editable", function() {
it("should always be not editable", function* () {
let feed = yield createFeed();
assert.isFalse(feed.editable);
feed.editable = true;
assert.isFalse(feed.editable);
yield feed.saveTx();
assert.isFalse(feed.editable);
});
it("should not allow adding items without editCheck override", function* () {
let feed = yield createFeed();
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
yield assert.isRejected(feedItem.saveTx(), /^Error: Cannot edit feedItem in read-only Zotero library$/);
yield assert.isFulfilled(feedItem.saveTx({ skipEditCheck: true }));
});
});
describe("#url", function() {
it("should throw if trying to set an invalid URL", function *() {
let feed = new Zotero.Feed({ name: 'Test ' + Zotero.randomString() });
assert.throws(function() {feed.url = 'foo'}, /^Invalid feed URL /);
assert.throws(function() {feed.url = 'ftp://example.com'}, /^Invalid feed URL /);
});
});
describe("#save()", function() {
it("should save a new feed to the feed library", function* () {
let props = {
name: 'Test ' + Zotero.randomString(),
url: 'http://' + Zotero.randomString() + '.com/'
};
let feed = yield createFeed(props);
assert.equal(feed.name, props.name, "name is correct");
assert.equal(feed.url.toLowerCase(), props.url.toLowerCase(), "url is correct");
});
it("should save a feed with all fields set", function* () {
let props = {
name: 'Test ' + Zotero.randomString(),
url: 'http://' + Zotero.randomString() + '.com/',
refreshInterval: 30,
cleanupAfter: 2
};
let feed = yield createFeed(props);
assert.equal(feed.name, props.name, "name is correct");
assert.equal(feed.url.toLowerCase(), props.url.toLowerCase(), "url is correct");
assert.equal(feed.refreshInterval, props.refreshInterval, "refreshInterval is correct");
assert.equal(feed.cleanupAfter, props.cleanupAfter, "cleanupAfter is correct");
assert.isNull(feed.lastCheck, "lastCheck is null");
assert.isNull(feed.lastUpdate, "lastUpdate is null");
assert.isNull(feed.lastCheckError, "lastCheckError is null");
});
it("should throw if name or url are missing", function *() {
let feed = new Zotero.Feed();
yield assert.isRejected(feed.saveTx(), /^Error: Feed name not set$/);
feed.name = 'Test ' + Zotero.randomString();
yield assert.isRejected(feed.saveTx(), /^Error: Feed URL not set$/);
feed = new Zotero.Feed();
feed.url = 'http://' + Zotero.randomString() + '.com';
yield assert.isRejected(feed.saveTx(), /^Error: Feed name not set$/);
});
it("should not allow saving a feed with the same url", function *() {
let url = 'http://' + Zotero.randomString() + '.com';
let feed1 = yield createFeed({ url });
let feed2 = new Zotero.Feed({ name: 'Test ' + Zotero.randomString(), url });
yield assert.isRejected(feed2.saveTx(), /^Error: Feed for URL already exists: /);
// Perform check with normalized URL
feed2.url = url + '/';
yield assert.isRejected(feed2.saveTx(), /^Error: Feed for URL already exists: /);
feed2.url = url.toUpperCase();
yield assert.isRejected(feed2.saveTx(), /^Error: Feed for URL already exists: /);
});
it("should allow saving a feed with the same name", function *() {
let name = 'Test ' + Zotero.randomString();
let feed1 = yield createFeed({ name });
let feed2 = new Zotero.Feed({ name , url: 'http://' + Zotero.randomString() + '.com' });
yield assert.isFulfilled(feed2.saveTx(), "allow saving feed with an existing name");
assert.equal(feed1.name, feed2.name, "feed names remain the same");
});
it("should save field to DB after editing", function* () {
let feed = yield createFeed();
feed.name = 'bar';
yield feed.saveTx();
let dbVal = yield Zotero.DB.valueQueryAsync('SELECT name FROM feeds WHERE libraryID=?', feed.libraryID);
assert.equal(feed.name, 'bar');
assert.equal(dbVal, feed.name);
});
});
describe("#erase()", function() {
it("should erase a saved feed", function* () {
let feed = yield createFeed();
let id = feed.libraryID;
let url = feed.url;
yield feed.eraseTx();
assert.isFalse(Zotero.Libraries.exists(id));
assert.isFalse(Zotero.Feeds.existsByURL(url));
let dbValue = yield Zotero.DB.valueQueryAsync('SELECT COUNT(*) FROM feeds WHERE libraryID=?', id);
assert.equal(dbValue, '0');
});
it("should clear feedItems from cache", function* () {
let feed = yield createFeed();
let feedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
assert.ok(yield Zotero.FeedItems.getAsync(feedItem.id));
yield feed.eraseTx();
assert.notOk(yield Zotero.FeedItems.getAsync(feedItem.id));
});
});
describe("Adding items", function() {
let feed;
before(function* () {
feed = yield createFeed();
})
it("should not allow adding regular items", function* () {
let item = new Zotero.Item('book');
item.libraryID = feed.libraryID;
yield assert.isRejected(item.saveTx({ skipEditCheck: true }), /^Error: Cannot add /);
});
it("should not allow adding collections", function* () {
let collection = new Zotero.Collection({ name: 'test', libraryID: feed.libraryID });
yield assert.isRejected(collection.saveTx({ skipEditCheck: true }), /^Error: Cannot add /);
});
it("should not allow adding saved search", function* () {
let search = new Zotero.Search({ name: 'test', libraryID: feed.libraryID });
yield assert.isRejected(search.saveTx({ skipEditCheck: true }), /^Error: Cannot add /);
});
it("should allow adding feed item", function* () {
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
yield assert.isFulfilled(feedItem.forceSaveTx());
});
});
})

59
test/tests/feedsTest.js Normal file
View file

@ -0,0 +1,59 @@
describe("Zotero.Feeds", function () {
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
let feed = new Zotero.Feed({
name: props.name || 'Test ' + Zotero.randomString(),
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
refreshInterval: props.refreshInterval,
cleanupAfter: props.cleanupAfter
});
yield feed.saveTx();
return feed;
});
let clearFeeds = Zotero.Promise.coroutine(function* () {
let feeds = Zotero.Feeds.getAll();
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<feeds.length; i++) {
yield feeds[i].erase();
}
});
});
describe("#haveFeeds()", function() {
it("should return false for a DB without feeds", function* () {
yield clearFeeds();
assert.isFalse(Zotero.Feeds.haveFeeds(), 'no feeds in empty DB');
let group = yield createGroup();
assert.isFalse(Zotero.Feeds.haveFeeds(), 'no feeds in DB with groups');
});
it("should return true for a DB containing feeds", function* () {
let feed = yield createFeed();
assert.isTrue(Zotero.Feeds.haveFeeds());
});
});
describe("#getAll()", function() {
it("should return an empty array for a DB without feeds", function* () {
yield clearFeeds();
let feeds = Zotero.Feeds.getAll();
assert.lengthOf(feeds, 0, 'no feeds in an empty DB');
let group = yield createGroup();
feeds = Zotero.Feeds.getAll();
assert.lengthOf(feeds, 0, 'no feeds in DB with group libraries');
});
it("should return an array of feeds", function* () {
yield clearFeeds();
let feed1 = yield createFeed();
let feed2 = yield createFeed();
let feeds = Zotero.Feeds.getAll();
assert.lengthOf(feeds, 2);
assert.sameMembers(feeds, [feed1, feed2]);
});
});
})

View file

@ -1,13 +1,54 @@
"use strict"; "use strict";
describe("Zotero.Group", function () { describe("Zotero.Group", function () {
describe("#constructor()", function() {
it("should accept required parameters", function* () {
let group = new Zotero.Group();
yield assert.isRejected(group.saveTx(), "fails without required parameters");
let groupID = Zotero.Utilities.rand(10000, 1000000);
let groupName = "Test " + Zotero.Utilities.randomString();
let groupVersion = Zotero.Utilities.rand(10000, 1000000);
group = new Zotero.Group({
groupID: groupID,
name: groupName,
description: "",
version:groupVersion,
editable: true,
filesEditable: true
});
yield assert.isFulfilled(group.saveTx(), "saves given required parameters");
assert.isTrue(Zotero.Libraries.exists(group.libraryID));
assert.equal(group, Zotero.Groups.get(group.groupID));
assert.equal(group.name, groupName);
assert.equal(group.groupID, groupID);
assert.equal(group.version, groupVersion);
});
});
describe("#version", function() {
it("should be settable to increasing values", function() {
let library = new Zotero.Group();
assert.throws(() => library.version = -1);
assert.throws(() => library.version = "a");
assert.throws(() => library.version = 1.1);
assert.doesNotThrow(() => library.version = 0);
assert.doesNotThrow(() => library.version = 5);
});
it("should not be possible to decrement", function() {
let library = new Zotero.Group();
library.version = 5;
assert.throws(() => library.version = 0);
});
});
describe("#erase()", function () { describe("#erase()", function () {
it("should unregister group", function* () { it("should unregister group", function* () {
var group = yield createGroup(); var group = yield createGroup();
var id = group.id; var id = group.id;
yield Zotero.DB.executeTransaction(function* () { yield group.eraseTx();
return group.erase()
}.bind(this));
assert.isFalse(Zotero.Groups.exists(id)); assert.isFalse(Zotero.Groups.exists(id));
}) })

View file

@ -2,6 +2,7 @@ describe("Zotero.Items", function () {
var win, collectionsView, zp; var win, collectionsView, zp;
before(function* () { before(function* () {
this.timeout(10000);
win = yield loadZoteroPane(); win = yield loadZoteroPane();
collectionsView = win.ZoteroPane.collectionsView; collectionsView = win.ZoteroPane.collectionsView;
zp = win.ZoteroPane; zp = win.ZoteroPane;
@ -114,4 +115,28 @@ describe("Zotero.Items", function () {
//assert.equal(zp.itemsView.rowCount, 0) //assert.equal(zp.itemsView.rowCount, 0)
}) })
}) })
describe("#getAsync()", function() {
it("should return Zotero.Item for item ID", function* () {
let item = new Zotero.Item('journalArticle');
let id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
assert.notOk(item.isFeedItem);
assert.instanceOf(item, Zotero.Item);
assert.notInstanceOf(item, Zotero.FeedItem);
});
it("should return Zotero.FeedItem for feed item ID", function* () {
let feed = new Zotero.Feed({ name: 'foo', url: 'http://www.' + Zotero.randomString() + '.com' });
yield feed.saveTx();
let feedItem = new Zotero.FeedItem('journalArticle', { guid: Zotero.randomString() });
feedItem.libraryID = feed.libraryID;
let id = yield feedItem.forceSaveTx();
feedItem = yield Zotero.Items.getAsync(id);
assert.isTrue(feedItem.isFeedItem);
assert.instanceOf(feedItem, Zotero.FeedItem);
});
});
}); });

238
test/tests/librariesTest.js Normal file
View file

@ -0,0 +1,238 @@
describe("Zotero.Libraries", function() {
let groupName = 'test',
group,
builtInLibraries;
before(function* () {
builtInLibraries = [
Zotero.Libraries.userLibraryID,
Zotero.Libraries.publicationsLibraryID
];
group = yield createGroup({ name: groupName });
});
it("should provide user library ID as .userLibraryID", function() {
assert.isDefined(Zotero.Libraries.userLibraryID);
assert(Number.isInteger(Zotero.Libraries.userLibraryID), ".userLibraryID is an integer");
assert.isAbove(Zotero.Libraries.userLibraryID, 0);
});
it("should provide publications library ID as .publicationsLibraryID", function() {
assert.isDefined(Zotero.Libraries.publicationsLibraryID);
assert(Number.isInteger(Zotero.Libraries.publicationsLibraryID), ".publicationsLibraryID is an integer");
assert.isAbove(Zotero.Libraries.publicationsLibraryID, 0);
});
describe("#getAll()", function() {
it("should return an array of valid library IDs", function() {
let ids = Zotero.Libraries.getAll();
assert.isArray(ids);
assert(ids.reduce(function(res, id) { return res && Number.isInteger(id) && id > 0 }, true), "All IDs are positive integers");
})
it("should return all library IDs", function* () {
// Add/remove a few group libraries beforehand to ensure that data is kept in sync
let library = yield createGroup();
let tempLib = yield createGroup();
yield tempLib.eraseTx();
let dbIDs = yield Zotero.DB.columnQueryAsync("SELECT libraryID FROM libraries");
let ids = Zotero.Libraries.getAll();
assert.sameMembers(dbIDs, ids);
assert.equal(dbIDs.length, ids.length, "returns correct number of IDs");
// remove left-over library
yield library.eraseTx();
});
it("should return a deep copy of ID array", function() {
let ids = Zotero.Libraries.getAll();
ids.push(-1);
assert.notDeepEqual(ids, Zotero.Libraries.getAll());
});
});
describe("#exists()", function() {
it("should return true for all existing IDs", function() {
let ids = Zotero.Libraries.getAll();
assert.isTrue(ids.reduce(function(res, id) { return res && Zotero.Libraries.exists(id) }, true));
});
it("should return false for a non-existing ID", function() {
assert.isFalse(Zotero.Libraries.exists(-1), "returns boolean false for a negative ID");
let badID = Zotero.Libraries.getAll().sort().pop() + 1;
assert.isFalse(Zotero.Libraries.exists(badID), "returns boolean false for a non-existent positive ID");
});
});
describe("#getName()", function() {
it("should return correct library name for built-in libraries", function() {
assert.equal(Zotero.Libraries.getName(Zotero.Libraries.userLibraryID), Zotero.getString('pane.collections.library'), "user library name is correct");
assert.equal(Zotero.Libraries.getName(Zotero.Libraries.publicationsLibraryID), Zotero.getString('pane.collections.publications'), "publications library name is correct");
});
it("should return correct name for a group library", function() {
assert.equal(Zotero.Libraries.getName(group.libraryID), groupName);
});
it("should throw for invalid library ID", function() {
assert.throws(() => Zotero.Libraries.getName(-1), /^Invalid library ID /);
});
});
describe("#getType()", function() {
it("should return correct library type for built-in libraries", function() {
assert.equal(Zotero.Libraries.getType(Zotero.Libraries.userLibraryID), 'user', "user library type is correct");
assert.equal(Zotero.Libraries.getType(Zotero.Libraries.publicationsLibraryID), 'publications', "publications library type is correct");
});
it("should return correct library type for a group library", function() {
assert.equal(Zotero.Libraries.getType(group.libraryID), 'group');
});
it("should throw for invalid library ID", function() {
assert.throws(() => Zotero.Libraries.getType(-1), /^Invalid library ID /);
});
});
describe("#isEditable()", function() {
it("should always return true for user library", function() {
assert.isTrue(Zotero.Libraries.isEditable(Zotero.Libraries.userLibraryID));
});
it("should always return true for publications library", function() {
assert.isTrue(Zotero.Libraries.isEditable(Zotero.Libraries.publicationsLibraryID));
});
it("should return correct state for a group library", function* () {
group.editable = true;
yield group.saveTx();
assert.isTrue(Zotero.Libraries.isEditable(group.libraryID));
group.editable = false;
yield group.saveTx();
assert.isFalse(Zotero.Libraries.isEditable(group.libraryID));
});
it("should throw for invalid library ID", function() {
assert.throws(Zotero.Libraries.isEditable.bind(Zotero.Libraries, -1), /^Invalid library ID /);
});
it("should not depend on filesEditable", function* () {
let editableStartState = Zotero.Libraries.isEditable(group.libraryID),
filesEditableStartState = Zotero.Libraries.isFilesEditable(group.libraryID);
// Test all combinations
// E: true, FE: true => true
yield Zotero.Libraries.setEditable(group.libraryID, true);
yield Zotero.Libraries.setFilesEditable(group.libraryID, true);
assert.isTrue(Zotero.Libraries.isEditable(group.libraryID));
// E: false, FE: true => false
yield Zotero.Libraries.setEditable(group.libraryID, false);
assert.isFalse(Zotero.Libraries.isEditable(group.libraryID));
// E: false, FE: false => false
yield Zotero.Libraries.setFilesEditable(group.libraryID, false);
assert.isFalse(Zotero.Libraries.isEditable(group.libraryID));
// E: true, FE: false => true
yield Zotero.Libraries.setEditable(group.libraryID, true);
assert.isTrue(Zotero.Libraries.isEditable(group.libraryID));
// Revert settings
yield Zotero.Libraries.setFilesEditable(group.libraryID, filesEditableStartState);
yield Zotero.Libraries.setEditable(group.libraryID, editableStartState);
});
});
describe("#setEditable()", function() {
it("should not allow changing editable state of built-in libraries", function* () {
for (let i=0; i<builtInLibraries.length; i++) {
yield assert.isRejected(Zotero.Libraries.setEditable(builtInLibraries[i]));
}
});
it("should allow changing editable state for group library", function* () {
let startState = Zotero.Libraries.isEditable(group.libraryID);
yield Zotero.Libraries.setEditable(group.libraryID, !startState);
assert.equal(Zotero.Libraries.isEditable(group.libraryID), !startState, 'changes state');
yield Zotero.Libraries.setEditable(group.libraryID, startState);
assert.equal(Zotero.Libraries.isEditable(group.libraryID), startState, 'reverts state');
});
it("should throw for invalid library ID", function() {
return assert.isRejected(Zotero.Libraries.setEditable(-1), /^Error: Invalid library ID /);
});
});
describe("#isFilesEditable()", function() {
it("should always return true for user library", function() {
assert.isTrue(Zotero.Libraries.isFilesEditable(Zotero.Libraries.userLibraryID));
});
it("should always return true for publications library", function() {
assert.isTrue(Zotero.Libraries.isFilesEditable(Zotero.Libraries.publicationsLibraryID));
});
it("should throw for invalid library ID", function() {
assert.throws(Zotero.Libraries.isFilesEditable.bind(Zotero.Libraries, -1), /^Invalid library ID /);
});
it("should not depend on editable", function* () {
let editableStartState = Zotero.Libraries.isEditable(group.libraryID),
filesEditableStartState = Zotero.Libraries.isFilesEditable(group.libraryID);
// Test all combinations
// E: true, FE: true => true
yield Zotero.Libraries.setEditable(group.libraryID, true);
yield Zotero.Libraries.setFilesEditable(group.libraryID, true);
assert.isTrue(Zotero.Libraries.isFilesEditable(group.libraryID));
// E: false, FE: true => true
yield Zotero.Libraries.setEditable(group.libraryID, false);
assert.isTrue(Zotero.Libraries.isFilesEditable(group.libraryID));
// E: false, FE: false => false
yield Zotero.Libraries.setFilesEditable(group.libraryID, false);
assert.isFalse(Zotero.Libraries.isFilesEditable(group.libraryID));
// E: true, FE: false => false
yield Zotero.Libraries.setEditable(group.libraryID, true);
assert.isFalse(Zotero.Libraries.isFilesEditable(group.libraryID));
// Revert settings
yield Zotero.Libraries.setFilesEditable(group.libraryID, filesEditableStartState);
yield Zotero.Libraries.setEditable(group.libraryID, editableStartState);
});
});
describe("#setFilesEditable()", function() {
it("should not allow changing files editable state of built-in libraries", function* () {
for (let i=0; i<builtInLibraries.length; i++) {
yield assert.isRejected(Zotero.Libraries.setFilesEditable(builtInLibraries[i]));
}
});
it("should allow changing files editable state for group library", function* () {
let startState = Zotero.Libraries.isFilesEditable(group.libraryID),
editableStartState = Zotero.Libraries.isEditable(group.libraryID);
// Since filesEditable is false for all non-editable libraries
yield Zotero.Libraries.setEditable(group.libraryID, true);
yield Zotero.Libraries.setFilesEditable(group.libraryID, !startState);
assert.equal(Zotero.Libraries.isFilesEditable(group.libraryID), !startState, 'changes state');
yield Zotero.Libraries.setFilesEditable(group.libraryID, startState);
assert.equal(Zotero.Libraries.isFilesEditable(group.libraryID), startState, 'reverts state');
yield Zotero.Libraries.setEditable(group.libraryID, editableStartState);
});
it("should throw for invalid library ID", function* () {
return assert.isRejected(Zotero.Libraries.setFilesEditable(-1), /^Error: Invalid library ID /);
});
});
describe("#isGroupLibrary()", function() {
it("should return false for non-group libraries", function() {
for (let i=0; i<builtInLibraries.length; i++) {
let id = builtInLibraries[i],
type = Zotero.Libraries.getType(id);
assert.isFalse(Zotero.Libraries.isGroupLibrary(id), "returns false for " + type + " library");
}
});
if("should return true for group library", function(){
assert.isTrue(Zotero.Libraries.isGroupLibrary(group.libraryID));
})
it("should throw for invalid library ID", function() {
assert.throws(Zotero.Libraries.isGroupLibrary.bind(Zotero.Libraries, -1), /^Invalid library ID /);
});
});
describe("#hasTrash()", function() {
it("should return true for all library types", function() {
assert.isTrue(Zotero.Libraries.hasTrash(Zotero.Libraries.userLibraryID));
assert.isTrue(Zotero.Libraries.hasTrash(Zotero.Libraries.publicationsLibraryID));
assert.isTrue(Zotero.Libraries.hasTrash(group.libraryID));
});
it("should throw for invalid library ID", function() {
assert.throws(Zotero.Libraries.hasTrash.bind(Zotero.Libraries, -1), /^Invalid library ID /);
});
});
})

266
test/tests/libraryTest.js Normal file
View file

@ -0,0 +1,266 @@
describe("Zotero.Library", function() {
describe("#constructor()", function() {
it("should allow no arguments", function() {
assert.doesNotThrow(() => new Zotero.Library());
});
});
describe("#libraryID", function() {
it("should not allow setting a library ID", function() {
let library = new Zotero.Library();
assert.throws(() => library.libraryID = 1);
});
it("should return a library ID for a saved library", function() {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
assert.isAbove(library.libraryID, 0);
})
});
describe("#libraryType", function() {
it("should not allow creating a non-basic library", function() {
let library = new Zotero.Library();
assert.throws(() => library.libraryType = 'group', /^Invalid library type /);
});
it("should not allow setting a library type for a saved library", function* () {
let library = yield createGroup();
assert.throws(() => library.libraryType = 'feed');
});
it("should not allow creating new unique libraries", function* () {
for (let i=0; i<Zotero.Library.prototype.fixedLibraries.length; i++) {
let libraryType = Zotero.Library.prototype.fixedLibraries[i];
assert.throws(function() {new Zotero.Library({ libraryType })}, /^Cannot create library of type /, 'cannot create a new ' + libraryType + ' library');
}
});
});
describe("#libraryVersion", function() {
it("should be settable to increasing values", function() {
let library = new Zotero.Library();
assert.throws(() => library.libraryVersion = -2);
assert.throws(() => library.libraryVersion = "a");
assert.throws(() => library.libraryVersion = 1.1);
assert.doesNotThrow(() => library.libraryVersion = 0);
assert.doesNotThrow(() => library.libraryVersion = 5);
});
it("should not be possible to decrement", function() {
let library = new Zotero.Library();
library.libraryVersion = 5;
assert.throws(() => library.libraryVersion = 0);
});
it("should be possible to set to -1", function() {
let library = new Zotero.Library();
library.libraryVersion = 5;
assert.doesNotThrow(() => library.libraryVersion = -1);
});
});
describe("#editable", function() {
it("should return editable status", function() {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
assert.isTrue(library.editable, 'user library is editable');
});
it("should allow setting editable status", function* () {
let library = yield createGroup({ editable: true });
assert.isTrue(library.editable);
assert.isTrue(Zotero.Libraries.isEditable(library.libraryID), "sets editable in cache to true");
library.editable = false;
yield library.saveTx();
assert.isFalse(library.editable);
assert.isFalse(Zotero.Libraries.isEditable(library.libraryID), "sets editable in cache to false");
});
it("should not be settable for user and publications libraries", function* () {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
assert.throws(function() {library.editable = false}, /^Cannot change _libraryEditable for user library$/, "does not allow setting user library as not editable");
library = Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
assert.throws(function() {library.editable = false}, /^Cannot change _libraryEditable for publications library$/, "does not allow setting publications library as not editable");
});
});
describe("#filesEditable", function() {
it("should return files editable status", function() {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
assert.isTrue(library.filesEditable, 'user library is files editable');
});
it("should allow setting files editable status", function* () {
let library = yield createGroup({ filesEditable: true });
assert.isTrue(library.filesEditable);
assert.isTrue(Zotero.Libraries.isFilesEditable(library.libraryID), "sets files editable in cache to true");
library.filesEditable = false;
yield library.saveTx();
assert.isFalse(library.filesEditable);
assert.isFalse(Zotero.Libraries.isFilesEditable(library.libraryID), "sets files editable in cache to false");
});
it("should not be settable for user and publications libraries", function* () {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
assert.throws(function() {library.filesEditable = false}, /^Cannot change _libraryFilesEditable for user library$/, "does not allow setting user library as not files editable");
library = Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
assert.throws(function() {library.filesEditable = false}, /^Cannot change _libraryFilesEditable for publications library$/, "does not allow setting publications library as not files editable");
});
});
describe("#save()", function() {
it("should require mandatory parameters to be set", function* () {
let library = new Zotero.Library({ editable: true, filesEditable: true });
yield assert.isRejected(library.saveTx(), /^Error: libraryType must be set before saving/, 'libraryType is mandatory');
// Required group params
let groupID = Zotero.Utilities.rand(1000, 10000);
let name = 'foo';
let description = '';
let version = Zotero.Utilities.rand(1000, 10000);
library = new Zotero.Group({ filesEditable: true, groupID, name , description, version });
yield assert.isRejected(library.saveTx(), /^Error: editable must be set before saving/, 'editable is mandatory');
library = new Zotero.Group({ editable: true, groupID, name , description, version });
yield assert.isRejected(library.saveTx(), /^Error: filesEditable must be set before saving/, 'filesEditable is mandatory');
library = new Zotero.Group({ editable: true, filesEditable: true, groupID, name , description, version });
yield assert.isFulfilled(library.saveTx());
});
it("should save new library to DB", function* () {
let library = yield createGroup({});
assert.isAbove(library.libraryID, 0, "sets a libraryID");
assert.isTrue(Zotero.Libraries.exists(library.libraryID));
assert.equal(library.libraryType, 'group');
let inDB = yield Zotero.DB.valueQueryAsync('SELECT COUNT(*) FROM libraries WHERE libraryID=?', library.libraryID);
assert.ok(inDB, 'added to DB');
});
it("should save library changes to DB", function* () {
let library = yield createGroup({ editable: true });
library.editable = false;
yield library.saveTx();
assert.isFalse(Zotero.Libraries.isEditable(library.libraryID));
});
});
describe("#erase()", function() {
it("should erase a group library", function* () {
let library = yield createGroup();
let libraryID = library.libraryID;
yield library.eraseTx();
assert.isFalse(Zotero.Libraries.exists(libraryID), "library no longer exists in cache");assert.isFalse(Zotero.Libraries.exists(libraryID));
let inDB = yield Zotero.DB.valueQueryAsync('SELECT COUNT(*) FROM libraries WHERE libraryID=?', libraryID);
assert.notOk(inDB, 'removed from DB');
});
it("should erase a read-only library", function* () {
let library = yield createGroup({ editable:false, filesEditable:false });
yield assert.isFulfilled(library.eraseTx());
});
it("should not allow erasing permanent libraries", function* () {
let library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
yield assert.isRejected(library.eraseTx(), /^Error: Cannot erase library of type 'user'$/, "does not allow erasing user library");
library = Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
yield assert.isRejected(library.eraseTx(), /^Error: Cannot erase library of type 'publications'$/, "does not allow erasing publications library");
});
it("should not allow erasing unsaved libraries", function* () {
let library = new Zotero.Library();
yield assert.isRejected(library.eraseTx());
});
it("should throw when accessing erased library methods, except for #libraryID", function* () {
let library = yield createGroup();
yield library.eraseTx();
assert.doesNotThrow(() => library.libraryID);
assert.throws(() => library.name, /^Group \(\d+\) has been disabled$/);
assert.throws(() => library.editable = false, /^Group \(\d+\) has been disabled$/);
});
it("should clear child items from caches and DB", function* () {
let group = yield createGroup();
let libraryID = group.libraryID;
let collection = yield createDataObject('collection', { libraryID });
assert.ok(yield Zotero.Collections.getAsync(collection.id));
let item = yield createDataObject('item', { libraryID });
assert.ok(yield Zotero.Items.getAsync(item.id));
let search = yield createDataObject('search', { libraryID });
assert.ok(yield Zotero.Searches.getAsync(search.id));
yield group.eraseTx();
assert.notOk((yield Zotero.Searches.getAsync(search.id)), 'search was unloaded');
assert.notOk((yield Zotero.Collections.getAsync(collection.id)), 'collection was unloaded');
assert.notOk((yield Zotero.Items.getAsync(item.id)), 'item was unloaded');
});
});
describe("#hasCollections()", function() {
it("should throw if called before saving a library", function() {
let library = new Zotero.Library();
assert.throws(() => library.hasCollections());
});
it("should stay up to date as collections are added and removed", function* () {
let library = yield createGroup({ editable: true });
let libraryID = library.libraryID;
assert.isFalse(library.hasCollections());
let c1 = yield createDataObject('collection', { libraryID });
assert.isTrue(library.hasCollections());
let c2 = yield createDataObject('collection', { libraryID });
assert.isTrue(library.hasCollections());
yield c1.eraseTx();
assert.isTrue(library.hasCollections());
yield c2.eraseTx();
assert.isFalse(library.hasCollections());
})
});
describe("#hasSearches()", function() {
it("should throw if called before saving a library", function() {
let library = new Zotero.Library();
assert.throws(() => library.hasSearches());
});
it("should stay up to date as searches are added and removed", function* () {
let library = yield createGroup({ editable: true });
let libraryID = library.libraryID;
assert.isFalse(library.hasSearches());
let s1 = yield createDataObject('search', { libraryID });
assert.isTrue(library.hasSearches());
let s2 = yield createDataObject('search', { libraryID });
assert.isTrue(library.hasSearches());
yield s1.eraseTx();
assert.isTrue(library.hasSearches());
yield s2.eraseTx();
assert.isFalse(library.hasSearches());
})
});
describe("#updateLastSyncTime()", function() {
it("should set sync time to current time", function* () {
let group = yield createGroup();
assert.isFalse(group.lastSync);
group.updateLastSyncTime();
assert.ok(group.lastSync);
assert.closeTo(Date.now(), group.lastSync.getTime(), 1000);
yield group.saveTx();
let dbTime = yield Zotero.DB.valueQueryAsync('SELECT lastSync FROM libraries WHERE libraryID=?', group.libraryID);
assert.equal(dbTime*1000, group.lastSync.getTime());
})
});
})

View file

@ -303,7 +303,7 @@ describe("Zotero.Sync.Runner", function () {
yield Zotero.DB.queryAsync( yield Zotero.DB.queryAsync(
"UPDATE groups SET version=0 WHERE groupID IN (?, ?)", [group1.id, group2.id] "UPDATE groups SET version=0 WHERE groupID IN (?, ?)", [group1.id, group2.id]
); );
yield Zotero.Groups.init(); yield Zotero.Libraries.init();
group1 = Zotero.Groups.get(group1.id); group1 = Zotero.Groups.get(group1.id);
group2 = Zotero.Groups.get(group2.id); group2 = Zotero.Groups.get(group2.id);
@ -443,7 +443,7 @@ describe("Zotero.Sync.Runner", function () {
skipBundledFiles: true skipBundledFiles: true
}); });
yield Zotero.Groups.init(); yield Zotero.Libraries.init();
}) })
after(function* () { after(function* () {
this.timeout(60000); this.timeout(60000);