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);