diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js
index 581c61614d..54469b11c9 100644
--- a/chrome/content/zotero/xpcom/data/collection.js
+++ b/chrome/content/zotero/xpcom/data/collection.js
@@ -23,7 +23,7 @@
***** END LICENSE BLOCK *****
*/
-Zotero.Collection = function() {
+Zotero.Collection = function(params = {}) {
Zotero.Collection._super.apply(this);
this._name = null;
@@ -33,6 +33,9 @@ Zotero.Collection = function() {
this._hasChildItems = false;
this._childItems = [];
+
+ Zotero.Utilities.assignProps(this, params, ['name', 'libraryID', 'parentID',
+ 'parentKey', 'lastSync']);
}
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) {
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);
@@ -338,12 +341,6 @@ Zotero.Collection.prototype._saveData = 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.isNew) {
Zotero.Notifier.queue('add', 'collection', this.id, env.notifierData);
@@ -362,6 +359,10 @@ Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (
this._clearChanged();
}
+ if (env.isNew) {
+ yield Zotero.Libraries.get(this.libraryID).updateCollections();
+ }
+
return env.isNew ? this.id : true;
});
@@ -610,7 +611,19 @@ Zotero.Collection.prototype._eraseData = Zotero.Promise.coroutine(function* (env
}
}
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.
+
+ ***** 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 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);
+});
\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/data/feedItem.js b/chrome/content/zotero/xpcom/data/feedItem.js
new file mode 100644
index 0000000000..08d66bd727
--- /dev/null
+++ b/chrome/content/zotero/xpcom/data/feedItem.js
@@ -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 .
+
+ ***** 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);
+}
diff --git a/chrome/content/zotero/xpcom/data/feedItems.js b/chrome/content/zotero/xpcom/data/feedItems.js
new file mode 100644
index 0000000000..e3cc315223
--- /dev/null
+++ b/chrome/content/zotero/xpcom/data/feedItems.js
@@ -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 .
+
+ ***** 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.
+
+ ***** 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 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
+ }
+}
diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js
index 22615d1eef..e3e19dad32 100644
--- a/chrome/content/zotero/xpcom/data/group.js
+++ b/chrome/content/zotero/xpcom/data/group.js
@@ -23,292 +23,217 @@
***** END LICENSE BLOCK *****
*/
-
-Zotero.Group = function () {
- if (arguments[0]) {
- throw ("Zotero.Group constructor doesn't take any parameters");
- }
+Zotero.Group = function (params = {}) {
+ params.libraryType = 'group';
+ Zotero.Group._super.call(this, params);
- this._init();
-}
-
-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;
+ Zotero.Utilities.assignProps(this, params, ['groupID', 'name', 'description',
+ 'version']);
- this._loaded = false;
- this._changed = false;
- this._hasCollections = null;
- this._hasSearches = null;
-}
-
-
-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 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("Group (" + obj.libraryID + ") has been disabled");
}
-
- 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;
+ return obj[prop];
}
- }
+ });
}
-/*
- * Build group from database
+/**
+ * Non-prototype properties
*/
-Zotero.Group.prototype.load = Zotero.Promise.coroutine(function* () {
- var id = this._id;
-
- if (!id) {
- throw new Error("ID not set");
- }
-
- var sql = "SELECT G.* FROM groups G WHERE groupID=?";
- var data = yield Zotero.DB.rowQueryAsync(sql, id);
- if (!data) {
- this._loaded = true;
+
+Zotero.defineProperty(Zotero.Group, '_dbColumns', {
+ value: Object.freeze(['name', 'description', 'version'])
+});
+
+Zotero.Group._colToProp = function(c) {
+ return "_group" + Zotero.Utilities.capitalize(c);
+}
+
+Zotero.defineProperty(Zotero.Group, '_rowSQLSelect', {
+ 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 '?').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));
+ return Zotero.Group._super.prototype._set.call(this, prop, val);
+}
+
+Zotero.Group.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
+ let sql = Zotero.Group._rowSQL + " WHERE G.groupID=?";
+ let row = yield Zotero.DB.rowQueryAsync(sql, [this.groupID]);
+ this._loadDataFromRow(row);
});
+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;
+});
-/**
-* Deletes group and all descendant objects
-**/
-Zotero.Group.prototype.erase = Zotero.Promise.coroutine(function* () {
- Zotero.debug("Removing group " + this.id);
+Zotero.Group.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
+ yield Zotero.Group._super.prototype._saveData.call(this, env);
- Zotero.DB.requireTransaction();
-
- // Delete items
- var types = ['item', 'collection', 'search'];
- for (let type of types) {
- let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
- let sql = "SELECT " + objectsClass.idColumn + " FROM " + objectsClass.table
- + " WHERE libraryID=?";
- ids = yield Zotero.DB.columnQueryAsync(sql, this.libraryID);
- 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
- });
- }
+ let changedCols = [], params = [];
+ for (let i=0; i Zotero.Libraries.get(id));
var collation = Zotero.getLocaleCollation();
groups.sort(function(a, b) {
return collation.compareString(1, a.name, b.name);
@@ -62,18 +89,21 @@ Zotero.Groups = new function () {
this.getByLibraryID = function (libraryID) {
- var groupID = this.getGroupIDFromLibraryID(libraryID);
- return this.get(groupID);
+ return Zotero.Libraries.get(libraryID);
}
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) {
- var groupID = _groupIDsByLibraryID[libraryID];
+ if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
+
+ var groupID = this._cache.groupIDByLibraryID[libraryID];
if (!groupID) {
throw new Error("Group with libraryID " + libraryID + " does not exist");
}
@@ -82,40 +112,8 @@ Zotero.Groups = new function () {
this.getLibraryIDFromGroupID = function (groupID) {
- var libraryID = _libraryIDsByGroupID[groupID];
- if (!libraryID) {
- throw new Error("Group with groupID " + groupID + " does not exist");
- }
- return libraryID;
+ if (!this._cache) throw new Error("Zotero.Groups cache is not initialized");
+
+ return this._cache.libraryIDByGroupID[groupID] || false;
}
-
-
- 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 parseInt(v));
}
/**
- * @param {String} type - Library type
- * @param {Boolean} editable
- * @param {Boolean} filesEditable
+ * Get an existing library
+ *
+ * @param {Integer} libraryID
+ * @return {Zotero.Library[] | Zotero.Library}
*/
- this.add = Zotero.Promise.coroutine(function* (type, editable, filesEditable) {
- Zotero.DB.requireTransaction();
-
- 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;
+ this.get = function(libraryID) {
+ return this._cache[libraryID] || false;
}
/**
+ * @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
* @return {Integer}
*/
this.getVersion = function (libraryID) {
- if (!this.exists(libraryID)) {
- throw new Error("Library data not loaded for library " + libraryID);
- }
- return _libraryData[libraryID].version;
+ Zotero.debug("Zotero.Libraries.getVersion() is deprecated. Use Zotero.Library.prototype.version instead");
+ this._ensureExists(libraryID);
+ return Zotero.Libraries.get(libraryID).version;
}
/**
+ * @deprecated
+ *
* @param {Integer} libraryID
- * @param {Integer} version - Library version, or -1 to indicate that a full sync is required
+ * @param {Integer} version
* @return {Promise}
*/
- this.setVersion = Zotero.Promise.coroutine(function* (libraryID, version) {
- version = parseInt(version);
- var sql = "UPDATE libraries SET version=? WHERE libraryID=?";
- yield Zotero.DB.queryAsync(sql, [version, libraryID]);
- _libraryData[libraryID].version = version;
+ this.setVersion = Zotero.Promise.method(function(libraryID, version) {
+ Zotero.debug("Zotero.Libraries.setVersion() is deprecated. Use Zotero.Library.prototype.version instead");
+ this._ensureExists(libraryID);
+
+ let library = Zotero.Libraries.get(libraryID);
+ library.version = version;
+ return library.saveTx();
});
-
+ /**
+ * @deprecated
+ */
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 {Date} lastSyncTime
* @return {Promise}
- */
- this.updateLastSyncTime = function (libraryID) {
- var d = new Date();
- _libraryData[libraryID].lastSyncTime = d;
- return Zotero.DB.queryAsync(
- "UPDATE libraries SET lastsync=? WHERE libraryID=?",
- [Math.round(d.getTime() / 1000), libraryID]
- );
+ */
+ this.setLastSyncTime = Zotero.Promise.method(function (libraryID, lastSyncTime) {
+ Zotero.debug("Zotero.Libraries.setLastSyncTime() is deprecated. Use Zotero.Library.prototype.lastSync instead");
+ this._ensureExists(libraryID);
+
+ let library = Zotero.Libraries.get(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}
*/
- this.setEditable = function (libraryID, editable) {
- if (editable == this.isEditable(libraryID)) {
- return Zotero.Promise.resolve();
- }
- _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");
- }
+ this.setFilesEditable = Zotero.Promise.coroutine(function* (libraryID, filesEditable) {
+ Zotero.debug("Zotero.Libraries.setFilesEditable() is deprecated. Use Zotero.Library.prototype.filesEditable instead");
+ this._ensureExists(libraryID);
- 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 {
- id: row.libraryID,
- type: row.libraryType,
- editable: !!row.editable,
- filesEditable: !!row.filesEditable,
- version: row.version,
- lastSyncTime: row.lastsync != 0 ? new Date(row.lastsync * 1000) : false
- };
+ /**
+ * @deprecated
+ */
+ this.hasTrash = function (libraryID) {
+ Zotero.debug("Zotero.Libraries.hasTrash() is deprecated. Use Zotero.Library.prototype.hasTrash instead");
+ this._ensureExists(libraryID);
+ return Zotero.Libraries.get(libraryID).hasTrash;
}
+
+ /**
+ * @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();
+ })
}
\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js
new file mode 100644
index 0000000000..ac0fcdf7b7
--- /dev/null
+++ b/chrome/content/zotero/xpcom/data/library.js
@@ -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 .
+
+ ***** 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}
*/
this.getURIItem = Zotero.Promise.method(function (itemURI) {
- var {libraryID, key} = this._getURIObject(itemURI, 'item');
- if (!key) return false;
- return Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
+ var obj = this._getURIObject(itemURI, 'item');
+ if (!obj) return false;
+ 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
*/
this.getURIItemID = function (itemURI) {
- var {libraryID, key} = this._getURIObject(itemURI, 'item');
- if (!key) return false;
- return Zotero.Items.getIDFromLibraryAndKey(libraryID, key);
+ var obj = this._getURIObject(itemURI, 'item');
+ if (!obj) return false;
+ return Zotero.Items.getIDFromLibraryAndKey(obj.libraryID, obj.key);
}
@@ -211,9 +228,9 @@ Zotero.URI = new function () {
* @return {Promise}
*/
this.getURICollection = Zotero.Promise.method(function (collectionURI) {
- var {libraryID, key} = this._getURIObject(collectionURI, 'collection');
- if (!key) return false;
- return Zotero.Collections.getByLibraryAndKeyAsync(libraryID, key);
+ var obj = this._getURIObject(collectionURI, 'collection');
+ if (!obj) return false;
+ 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
*/
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
*/
this.getURICollectionID = function (collectionURI) {
- var {libraryID, key} = this._getURIObject(collectionURI, 'item');
- if (!key) return false;
- return Zotero.Collections.getIDFromLibraryAndKey(libraryID, key);
+ var obj = this._getURIObject(collectionURI, 'collection');
+ if (!obj) return false;
+ 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
*/
this.getURILibrary = function (libraryURI) {
- var {libraryID} = this._getURIObject(libraryURI, "library");
- return libraryID !== undefined ? libraryID : false;
+ let library = this._getURIObjectLibrary(libraryURI);
+ 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 {'library'|'collection'|'item'} - The type of URI to expect
- * @return {Object|FALSE} - An object containing 'libraryID' and, if applicable, 'key',
- * or FALSE if library not found
+ * @return {Zotero.Library|FALSE} - An object referenced by the URI
*/
- this._getURIObject = function (objectURI, type) {
- var libraryType;
- var libraryTypeID;
+ this._getURIObjectLibrary = function (objectURI) {
+ let uri = objectURI.replace(/\/+$/, ''); // Drop trailing "/"
+ let uriParts = uri.match(uriPartsRe);
- // If this is a local URI, compare to the local user key
- if (objectURI.match(/\/users\/local\//)) {
- // 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 (!uriParts) {
+ throw new Error("Could not parse object URI " + objectURI);
}
- // If not found, try global URI
- if (!libraryType) {
- if (!objectURI.startsWith(_baseURI)) {
- throw new Error("Invalid base URI '" + objectURI + "'");
- }
- objectURI = objectURI.substr(_baseURI.length);
- let typeRE = /^(users|groups)\/([0-9]+)(?:\/|$)/;
- let matches = objectURI.match(typeRE);
- if (!matches) {
- throw new Error("Invalid library URI '" + objectURI + "'");
- }
- 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
- };
+ let library;
+ if (uriParts[1] == 'users') {
+ let type = uriParts[4];
+ if (type == 'publications') {
+ library = Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
+ } else if (!type) {
+ // Handles local and synced libraries
+ library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
+ } else {
+ let feedID = type.split('/')[1];
+ library = Zotero.Libraries.get(feedID);
}
} else {
- var re = /(?:items|collections)\/([A-Z0-9]{8})/;
- var matches = objectURI.match(re);
- if (!matches) {
- throw ("Invalid object URI '" + objectURI + "' in Zotero.URI._getURIObject()");
- }
- let objectKey = matches[1];
- return {
- libraryID: libraryID,
- key: objectKey
- };
+ // Group libraries
+ library = Zotero.Groups.get(uriParts[3]);
}
+
+ if (!library) {
+ Zotero.debug("Could not find a library for URI " + objectURI, 2, true);
+ return false;
+ }
+
+ return library;
}
}
diff --git a/chrome/content/zotero/xpcom/users.js b/chrome/content/zotero/xpcom/users.js
index e764be3e33..d9293f52dd 100644
--- a/chrome/content/zotero/xpcom/users.js
+++ b/chrome/content/zotero/xpcom/users.js
@@ -30,52 +30,55 @@ Zotero.Users = new function () {
var _localUserKey;
this.init = Zotero.Promise.coroutine(function* () {
- var sql = "SELECT value FROM settings WHERE setting='account' AND key='userID'";
- _userID = yield Zotero.DB.valueQueryAsync(sql);
+ let sql = "SELECT key, value FROM settings WHERE setting='account'";
+ let rows = yield Zotero.DB.queryAsync(sql);
- if (_userID) {
- sql = "SELECT value FROM settings WHERE setting='account' AND key='libraryID'";
- _libraryID = yield Zotero.DB.valueQueryAsync(sql);
-
- sql = "SELECT value FROM settings WHERE setting='account' AND key='username'";
- _username = yield Zotero.DB.valueQueryAsync(sql);
+ let settings = {};
+ for (let i=0; i _userID;
- this.setCurrentUserID = function (val) {
+ this.getCurrentUserID = function() { return _userID };
+ this.setCurrentUserID = Zotero.Promise.coroutine(function* (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', ?)";
- return Zotero.DB.queryAsync(sql, val);
- };
+ yield Zotero.DB.queryAsync(sql, val);
+ _userID = val;
+ });
this.getCurrentUsername = () => _username;
- this.setCurrentUsername = function (val) {
- _username = val;
+ this.setCurrentUsername = Zotero.Promise.coroutine(function* (val) {
+ if (!val || typeof val != 'string') throw new Error('username must be a non-empty string');
+
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 () {
- if (!_localUserKey) {
- throw new Error("Local user key not available");
- }
return _localUserKey;
};
};
diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js
index 945a40ee6b..fa87c8f807 100644
--- a/chrome/content/zotero/xpcom/utilities.js
+++ b/chrome/content/zotero/xpcom/utilities.js
@@ -742,6 +742,21 @@ Zotero.Utilities = {
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 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);
+ });
+ });
})
diff --git a/test/tests/feedItemTest.js b/test/tests/feedItemTest.js
new file mode 100644
index 0000000000..dd7e964436
--- /dev/null
+++ b/test/tests/feedItemTest.js
@@ -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 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 () {
it("should unregister group", function* () {
var group = yield createGroup();
var id = group.id;
- yield Zotero.DB.executeTransaction(function* () {
- return group.erase()
- }.bind(this));
+ yield group.eraseTx();
assert.isFalse(Zotero.Groups.exists(id));
})
diff --git a/test/tests/itemsTest.js b/test/tests/itemsTest.js
index 2bec5aa707..486c12a9d7 100644
--- a/test/tests/itemsTest.js
+++ b/test/tests/itemsTest.js
@@ -2,6 +2,7 @@ describe("Zotero.Items", function () {
var win, collectionsView, zp;
before(function* () {
+ this.timeout(10000);
win = yield loadZoteroPane();
collectionsView = win.ZoteroPane.collectionsView;
zp = win.ZoteroPane;
@@ -114,4 +115,28 @@ describe("Zotero.Items", function () {
//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);
+ });
+ });
});
diff --git a/test/tests/librariesTest.js b/test/tests/librariesTest.js
new file mode 100644
index 0000000000..7ffb30b7f1
--- /dev/null
+++ b/test/tests/librariesTest.js
@@ -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 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 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 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());
+ })
+ });
+})
diff --git a/test/tests/syncRunnerTest.js b/test/tests/syncRunnerTest.js
index bbc1932ef8..726dfca80d 100644
--- a/test/tests/syncRunnerTest.js
+++ b/test/tests/syncRunnerTest.js
@@ -303,7 +303,7 @@ describe("Zotero.Sync.Runner", function () {
yield Zotero.DB.queryAsync(
"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);
group2 = Zotero.Groups.get(group2.id);
@@ -443,7 +443,7 @@ describe("Zotero.Sync.Runner", function () {
skipBundledFiles: true
});
- yield Zotero.Groups.init();
+ yield Zotero.Libraries.init();
})
after(function* () {
this.timeout(60000);