www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 66a04a39db8ec6abc7e4664c14318aa3e4fc0801
parent e4451d90025ad6c8a40c400fed43510d7846b7e8
Author: Dan Stillman <dstillman@zotero.org>
Date:   Wed, 28 Jan 2015 15:07:32 -0500

Merge pull request #576 from aurimasv/async_db-av2

[Async DB] Modularize Zotero.DataObject.save()
Diffstat:
Mchrome/content/zotero/bindings/zoterosearch.xml | 2+-
Mchrome/content/zotero/xpcom/api.js | 2+-
Mchrome/content/zotero/xpcom/data/collection.js | 394+++++++++++++++++++++++++++++++++++--------------------------------------------
Mchrome/content/zotero/xpcom/data/collections.js | 139++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mchrome/content/zotero/xpcom/data/dataObject.js | 233++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mchrome/content/zotero/xpcom/data/dataObjectUtilities.js | 4++--
Mchrome/content/zotero/xpcom/data/dataObjects.js | 1034++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mchrome/content/zotero/xpcom/data/item.js | 2102+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mchrome/content/zotero/xpcom/data/itemFields.js | 2+-
Mchrome/content/zotero/xpcom/data/items.js | 176++++++++++++++++++++++---------------------------------------------------------
Mchrome/content/zotero/xpcom/data/libraries.js | 13+++++++++++--
Mchrome/content/zotero/xpcom/data/relations.js | 36+++++++++++++++++++++---------------
Mchrome/content/zotero/xpcom/search.js | 336++++++++++++++++++++++++++++++++++++-------------------------------------------
Mchrome/content/zotero/xpcom/utilities_internal.js | 18------------------
Mchrome/content/zotero/xpcom/zotero.js | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/zoteroPane.js | 10+++++++++-
16 files changed, 2336 insertions(+), 2245 deletions(-)

diff --git a/chrome/content/zotero/bindings/zoterosearch.xml b/chrome/content/zotero/bindings/zoterosearch.xml @@ -232,7 +232,7 @@ <body> <![CDATA[ this.updateSearch(); - return this.search.save(true); + return this.search.save({fixGaps: true}); ]]> </body> </method> diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js @@ -201,7 +201,7 @@ Zotero.API.Data = { var params = this.parsePath(path); //Zotero.debug(params); - return Zotero.DataObjectUtilities.getClassForObjectType(params.objectType) + return Zotero.DataObjectUtilities.getObjectsClassForObjectType(params.objectType) .apiDataGenerator(params); } }; diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -37,38 +37,51 @@ Zotero.Collection = function() { this._childItems = []; } -Zotero.Collection._super = Zotero.DataObject; -Zotero.Collection.prototype = Object.create(Zotero.Collection._super.prototype); -Zotero.Collection.constructor = Zotero.Collection; +Zotero.extendClass(Zotero.DataObject, Zotero.Collection); Zotero.Collection.prototype._objectType = 'collection'; Zotero.Collection.prototype._dataTypes = Zotero.Collection._super.prototype._dataTypes.concat([ - 'primaryData', 'childCollections', 'childItems' ]); -Zotero.Collection.prototype.__defineGetter__('id', function () { return this._get('id'); }); -Zotero.Collection.prototype.__defineSetter__('id', function (val) { this._set('id', val); }); -Zotero.Collection.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); }); -Zotero.Collection.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); }); -Zotero.Collection.prototype.__defineGetter__('key', function () { return this._get('key'); }); -Zotero.Collection.prototype.__defineSetter__('key', function (val) { this._set('key', val) }); -Zotero.Collection.prototype.__defineGetter__('name', function () { return this._get('name'); }); -Zotero.Collection.prototype.__defineSetter__('name', function (val) { this._set('name', val); }); -// .parentKey and .parentID defined in dataObject.js -Zotero.Collection.prototype.__defineGetter__('version', function () { return this._get('version'); }); -Zotero.Collection.prototype.__defineSetter__('version', function (val) { this._set('version', val); }); -Zotero.Collection.prototype.__defineGetter__('synced', function () { return this._get('synced'); }); -Zotero.Collection.prototype.__defineSetter__('synced', function (val) { this._set('synced', val); }); - -Zotero.Collection.prototype.__defineGetter__('parent', function (val) { - Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2); - return this.parentID; +Zotero.defineProperty(Zotero.Collection.prototype, 'ChildObjects', { + get: function() Zotero.Items }); -Zotero.Collection.prototype.__defineSetter__('parent', function (val) { - Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2); - this.parentID = val; + +Zotero.defineProperty(Zotero.Collection.prototype, 'id', { + get: function() this._get('id'), + set: function(val) this._set('id', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'libraryID', { + get: function() this._get('libraryID'), + set: function(val) this._set('libraryID', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'key', { + get: function() this._get('key'), + set: function(val) this._set('key', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'name', { + get: function() this._get('name'), + set: function(val) this._set('name', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'version', { + get: function() this._get('version'), + set: function(val) this._set('version', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'synced', { + get: function() this._get('synced'), + set: function(val) this._set('synced', val) +}); +Zotero.defineProperty(Zotero.Collection.prototype, 'parent', { + get: function() { + Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2); + return this.parentID; + }, + set: function(val) { + Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2); + this.parentID = val; + } }); Zotero.Collection.prototype._set = function (field, value) { @@ -115,44 +128,12 @@ Zotero.Collection.prototype.getName = function() { /* - * Build collection from database - */ -Zotero.Collection.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.primaryData && !reload) return; - - var id = this._id; - var key = this._key; - var libraryID = this._libraryID; - - var sql = Zotero.Collections.getPrimaryDataSQL(); - if (id) { - sql += " AND O.collectionID=?"; - var params = id; - } - else { - sql += " AND O.libraryID=? AND O.key=?"; - var params = [libraryID, key]; - } - var data = yield Zotero.DB.rowQueryAsync(sql, params); - - this._loaded.primaryData = true; - this._clearChanged('primaryData'); - - if (!data) { - return; - } - - this.loadFromRow(data); -}); - - -/* * Populate collection data from a database row */ Zotero.Collection.prototype.loadFromRow = function(row) { Zotero.debug("Loading collection from row"); - for each(let col in Zotero.Collections.primaryFields) { + for each(let col in this.ObjectsClass.primaryFields) { if (row[col] === undefined) { Zotero.debug('Skipping missing collection field ' + col); } @@ -267,168 +248,139 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) { return objs; } - -Zotero.Collection.prototype.save = Zotero.Promise.coroutine(function* () { - try { - Zotero.Collections.editCheck(this); +Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + if (!this.name) { + throw new Error('Collection name is empty'); + } + + var proceed = yield Zotero.Collection._super.prototype._initSave.apply(this, arguments); + if (!proceed) return false; + + // Verify parent + if (this._parentKey) { + let newParent = this.ObjectsClass.getByLibraryAndKey( + this.libraryID, this._parentKey + ); - if (!this.name) { - throw new Error('Collection name is empty'); + if (!newParent) { + throw new Error("Cannot set parent to invalid collection " + this._parentKey); } - if (Zotero.Utilities.isEmpty(this._changed)) { - Zotero.debug("Collection " + this.id + " has not changed"); - return false; + if (newParent.id == this.id) { + throw new Error('Cannot move collection into itself!'); } - var isNew = !this.id; - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var collectionID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Collections.registerIdentifiers(collectionID, libraryID, key); - } - }; - } - else { - var transactionOptions = null; + if (this.id && (yield this.hasDescendent('collection', newParent.id))) { + throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); } - return Zotero.DB.executeTransaction(function* () { - // how to know if date modified changed (in server code too?) - - collectionID = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); - libraryID = this.libraryID; - key = this._key = this.key ? this.key : this._generateKey(); - - Zotero.debug("Saving collection " + this.id); - - // Verify parent - if (this._parentKey) { - let newParent = Zotero.Collections.getByLibraryAndKey( - this.libraryID, this._parentKey - ); - - if (!newParent) { - throw new Error("Cannot set parent to invalid collection " + this._parentKey); - } - - if (newParent.id == this.id) { - throw new Error('Cannot move collection into itself!'); - } - - if (this.id && (yield this.hasDescendent('collection', newParent.id))) { - throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); - } - - var parent = newParent.id; - } - else { - var parent = null; - } - - var columns = [ - 'collectionID', - 'collectionName', - 'parentCollectionID', - 'clientDateModified', - 'libraryID', - 'key', - 'version', - 'synced' - ]; - var sqlValues = [ - collectionID ? { int: collectionID } : null, - { string: this.name }, - parent ? parent : null, - Zotero.DB.transactionDateTime, - this.libraryID ? this.libraryID : 0, - key, - this.version ? this.version : 0, - this.synced ? 1 : 0 - ]; - if (isNew) { - var placeholders = columns.map(function () '?').join(); - - var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " - + "VALUES (" + placeholders + ")"; - var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); - if (!collectionID) { - collectionID = insertID; - } - } - else { - columns.shift(); - sqlValues.push(sqlValues.shift()); - let sql = 'UPDATE collections SET ' - + columns.map(function (x) x + '=?').join(', ') - + ' WHERE collectionID=?'; - yield Zotero.DB.queryAsync(sql, sqlValues); - } - - if (this._changed.parentKey) { - var parentIDs = []; - if (this.id && this._previousData.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this._previousData.parentKey - )); - } - if (this.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this.parentKey - )); - } - if (this.id) { - Zotero.Notifier.trigger('move', 'collection', this.id); - } - } - - if (isNew && this.libraryID) { - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); - var group = Zotero.Groups.get(groupID); - group.clearCollectionCache(); - } - - if (isNew) { - Zotero.Notifier.trigger('add', 'collection', this.id); - } - else { - Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); - } - - // Invalidate cached child collections - if (parentIDs) { - Zotero.Collections.refreshChildCollections(parentIDs); - } - - // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled - if (isNew) { - var id = this.id; - this._disabled = true; - return id; - } - - yield this.reload(); - this._clearChanged(); - - return true; - }.bind(this), transactionOptions); + env.parent = newParent.id; + } + else { + env.parent = null; + } + + return true; +}); + +Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + + var collectionID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + Zotero.debug("Saving collection " + this.id); + + var columns = [ + 'collectionID', + 'collectionName', + 'parentCollectionID', + 'clientDateModified', + 'libraryID', + 'key', + 'version', + 'synced' + ]; + var sqlValues = [ + collectionID ? { int: collectionID } : null, + { string: this.name }, + env.parent ? env.parent : null, + Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ]; + if (isNew) { + var placeholders = columns.map(function () '?').join(); + + var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!collectionID) { + collectionID = env.id = insertID; + } } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); + else { + columns.shift(); + sqlValues.push(sqlValues.shift()); + let sql = 'UPDATE collections SET ' + + columns.map(function (x) x + '=?').join(', ') + + ' WHERE collectionID=?'; + yield Zotero.DB.queryAsync(sql, sqlValues); + } + + if (this._changed.parentKey) { + var parentIDs = []; + if (this.id && this._previousData.parentKey) { + parentIDs.push(this.ObjectsClass.getIDFromLibraryAndKey( + this.libraryID, this._previousData.parentKey + )); } - catch (e2) { - Zotero.debug(e2, 1); + if (this.parentKey) { + parentIDs.push(this.ObjectsClass.getIDFromLibraryAndKey( + this.libraryID, this.parentKey + )); } - - Zotero.debug(e, 1); - throw e; + if (this.id) { + Zotero.Notifier.trigger('move', 'collection', this.id); + } + env.parentIDs = parentIDs; + } +}); + +Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + if (isNew && Zotero.Libraries.isGroupLibrary(this.libraryID)) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); + var group = Zotero.Groups.get(groupID); + group.clearCollectionCache(); + } + + if (isNew) { + Zotero.Notifier.trigger('add', 'collection', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); + } + + // Invalidate cached child collections + if (env.parentIDs) { + this.ObjectsClass.refreshChildCollections(env.parentIDs); + } + + // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled + if (isNew) { + var id = this.id; + this._disabled = true; + return id; } + + yield this.reload(); + this._clearChanged(); + + return true; }); @@ -466,7 +418,7 @@ Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemI continue; } - let item = yield Zotero.Items.getAsync(itemID); + let item = yield this.ChildObjects.getAsync(itemID); yield item.loadCollections(); item.addToCollection(this.id); yield item.save({ @@ -513,7 +465,7 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it continue; } - let item = yield Zotero.Items.getAsync(itemID); + let item = yield this.ChildObjects.getAsync(itemID); yield item.loadCollections(); item.removeFromCollection(this.id); yield item.save({ @@ -565,7 +517,7 @@ Zotero.Collection.prototype.diff = function (collection, includeMatches) { var diff = []; var thisData = this.serialize(); var otherData = collection.serialize(); - var numDiffs = Zotero.Collections.diff(thisData, otherData, diff, includeMatches); + var numDiffs = this.ObjectsClass.diff(thisData, otherData, diff, includeMatches); // For the moment, just compare children and increase numDiffs if any differences var d1 = Zotero.Utilities.arrayDiff( @@ -625,7 +577,7 @@ Zotero.Collection.prototype.clone = function (includePrimary, newCollection) { var sameLibrary = newCollection.libraryID == this.libraryID; } else { - var newCollection = new Zotero.Collection; + var newCollection = new this.constructor; var sameLibrary = true; if (includePrimary) { @@ -661,7 +613,7 @@ Zotero.Collection.prototype.erase = function(deleteItems) { // Descendent collections if (descendents[i].type == 'collection') { collections.push(descendents[i].id); - var c = yield Zotero.Collections.getAsync(descendents[i].id); + var c = yield this.ObjectsClass.getAsync(descendents[i].id); if (c) { notifierData[c.id] = { old: c.toJSON() }; } @@ -675,7 +627,7 @@ Zotero.Collection.prototype.erase = function(deleteItems) { } } if (del.length) { - yield Zotero.Items.trash(del); + yield this.ChildObjects.trash(del); } // Remove relations @@ -698,9 +650,9 @@ Zotero.Collection.prototype.erase = function(deleteItems) { // TODO: Update member items }.bind(this)) - .then(function () { + .then(() => { // Clear deleted collection from internal memory - Zotero.Collections.unload(collections); + this.ObjectsClass.unload(collections); //return Zotero.Collections.reloadAll(); }) .then(function () { @@ -815,7 +767,7 @@ Zotero.Collection.prototype.getChildren = Zotero.Promise.coroutine(function* (re } if (recursive) { - let child = yield Zotero.Collections.getAsync(children[i].id); + let child = yield this.ObjectsClass.getAsync(children[i].id); let descendents = yield child.getChildren( true, nested, type, includeDeletedItems, level+1 ); @@ -871,7 +823,7 @@ Zotero.Collection.prototype.addLinkedCollection = Zotero.Promise.coroutine(funct var predicate = Zotero.Relations.linkedObjectPredicate; if ((yield Zotero.Relations.getByURIs(url1, predicate, url2)).length || (yield Zotero.Relations.getByURIs(url2, predicate, url1)).length) { - Zotero.debug("Collections " + this.key + " and " + collection.key + " are already linked"); + Zotero.debug(this._ObjectTypePlural + " " + this.key + " and " + collection.key + " are already linked"); return false; } @@ -901,9 +853,9 @@ Zotero.Collection.prototype.loadChildCollections = Zotero.Promise.coroutine(func this._childCollections = []; - if (ids) { + if (ids.length) { for each(var id in ids) { - var col = yield Zotero.Collections.getAsync(id); + var col = yield this.ObjectsClass.getAsync(id); if (!col) { throw new Error('Collection ' + id + ' not found'); } @@ -943,7 +895,7 @@ Zotero.Collection.prototype.loadChildItems = Zotero.Promise.coroutine(function* this._childItems = []; if (ids) { - var items = yield Zotero.Items.getAsync(ids) + var items = yield this.ChildObjects.getAsync(ids) if (items) { this._childItems = items; } diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js @@ -27,9 +27,10 @@ /* * Primary interface for accessing Zotero collection */ -Zotero.Collections = new function() { - Zotero.DataObjects.apply(this, ['collection']); - this.constructor.prototype = new Zotero.DataObjects(); +Zotero.Collections = function() { + this.constructor = null; + + this._ZDO_object = 'collection'; this._primaryDataSQLParts = { collectionID: "O.collectionID", @@ -45,9 +46,13 @@ Zotero.Collections = new function() { hasChildCollections: "(SELECT COUNT(*) FROM collections WHERE " + "parentCollectionID=O.collectionID) != 0 AS hasChildCollections", hasChildItems: "(SELECT COUNT(*) FROM collectionItems WHERE " - + "collectionID=O.collectionID) != 0 AS hasChildItems " + + "collectionID=O.collectionID) != 0 AS hasChildItems" }; + + this._primaryDataSQLFrom = "FROM collections O " + + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)"; + /** * Add new collection to DB and return Collection object * @@ -74,55 +79,51 @@ Zotero.Collections = new function() { * Takes parent collectionID as optional parameter; * by default, returns root collections */ - this.getByParent = Zotero.Promise.coroutine(function* (libraryID, parent, recursive) { - var toReturn = []; + this.getByParent = Zotero.Promise.coroutine(function* (libraryID, parentID, recursive) { + let children; - if (!parent) { - parent = null; + if (parentID) { + let parent = yield this.getAsync(parentID); + yield parent.loadChildCollections(); + children = parent.getChildCollections(); + if (!children.length) Zotero.debug('No child collections in collection ' + parentID, 5); + } else if (libraryID || libraryID === 0) { + children = this.getCollectionsInLibrary(libraryID); + if (!children.length) Zotero.debug('No child collections in library ' + libraryID, 5); + } else { + throw new Error("Either library ID or parent collection ID must be provided to getNumCollectionsByParent"); } - var sql = "SELECT collectionID AS id, collectionName AS name FROM collections C " - + "WHERE libraryID=? AND parentCollectionID " + (parent ? '= ' + parent : 'IS NULL'); - var children = yield Zotero.DB.queryAsync(sql, [libraryID]); - - if (!children) { - Zotero.debug('No child collections of collection ' + parent, 5); - return toReturn; + if (!children.length) { + return children; } // Do proper collation sort - var collation = Zotero.getLocaleCollation(); - children.sort(function (a, b) { - return collation.compareString(1, a.name, b.name); - }); + children.sort(function (a, b) Zotero.localeCompare(a.name, b.name)); + + if (!recursive) return children; + let toReturn = []; for (var i=0, len=children.length; i<len; i++) { - var obj = yield this.getAsync(children[i].id); - if (!obj) { - throw ('Collection ' + children[i].id + ' not found'); - } - + var obj = children[i]; toReturn.push(obj); - // If recursive, get descendents - if (recursive) { - var desc = obj.getDescendents(false, 'collection'); - for (var j in desc) { - var obj2 = yield this.getAsync(desc[j]['id']); - if (!obj2) { - throw new Error('Collection ' + desc[j] + ' not found'); - } - - // TODO: This is a quick hack so that we can indent subcollections - // in the search dialog -- ideally collections would have a - // getLevel() method, but there's no particularly quick way - // of calculating that without either storing it in the DB or - // changing the schema to Modified Preorder Tree Traversal, - // and I don't know if we'll actually need it anywhere else. - obj2.level = desc[j].level; - - toReturn.push(obj2); + var desc = obj.getDescendents(false, 'collection'); + for (var j in desc) { + var obj2 = yield this.getAsync(desc[j]['id']); + if (!obj2) { + throw new Error('Collection ' + desc[j] + ' not found'); } + + // TODO: This is a quick hack so that we can indent subcollections + // in the search dialog -- ideally collections would have a + // getLevel() method, but there's no particularly quick way + // of calculating that without either storing it in the DB or + // changing the schema to Modified Preorder Tree Traversal, + // and I don't know if we'll actually need it anywhere else. + obj2.level = desc[j].level; + + toReturn.push(obj2); } } @@ -130,6 +131,17 @@ Zotero.Collections = new function() { }); + this.getCollectionsInLibrary = Zotero.Promise.coroutine(function* (libraryID) { + let sql = "SELECT collectionID AS id FROM collections C " + + "WHERE libraryID=? AND parentCollectionId IS NULL"; + let ids = yield Zotero.DB.queryAsync(sql, [libraryID]); + let collections = yield this.getAsync(ids.map(function(row) row.id)); + if (!collections.length) return collections; + + return collections.sort(function (a, b) Zotero.localeCompare(a.name, b.name)); + }); + + this.getCollectionsContainingItems = function (itemIDs, asIDs) { // If an unreasonable number of items, don't try if (itemIDs.length > 100) { @@ -145,8 +157,8 @@ Zotero.Collections = new function() { } sql = sql.substring(0, sql.length - 5); return Zotero.DB.columnQueryAsync(sql, sqlParams) - .then(function (collectionIDs) { - return asIDs ? collectionIDs : Zotero.Collections.get(collectionIDs); + .then(collectionIDs => { + return asIDs ? collectionIDs : this.get(collectionIDs); }); } @@ -186,32 +198,23 @@ Zotero.Collections = new function() { }); - this.erase = function (ids) { + this.erase = function(ids) { ids = Zotero.flattenArguments(ids); - Zotero.DB.beginTransaction(); - for each(var id in ids) { - var collection = this.getAsync(id); - if (collection) { - collection.erase(); + return Zotero.DB.executeTransaction(function* () { + for each(var id in ids) { + var collection = yield this.getAsync(id); + if (collection) { + yield collection.erase(); + } + collection = undefined; } - collection = undefined; - } - - this.unload(ids); - - Zotero.DB.commitTransaction(); - } + + this.unload(ids); + }); + }; + Zotero.DataObjects.call(this); - this.getPrimaryDataSQL = function () { - // This should be the same as the query in Zotero.Collection.load(), - // just without a specific collectionID - return "SELECT " - + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " - + "FROM collections O " - + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID) " - + "WHERE 1"; - } -} - + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js @@ -34,6 +34,8 @@ Zotero.DataObject = function () { let objectType = this._objectType; this._ObjectType = objectType[0].toUpperCase() + objectType.substr(1); this._objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); + this._ObjectTypePlural = this._objectTypePlural[0].toUpperCase() + this._objectTypePlural.substr(1); + this._ObjectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); this._id = null; this._libraryID = null; @@ -53,23 +55,36 @@ Zotero.DataObject = function () { }; Zotero.DataObject.prototype._objectType = 'dataObject'; -Zotero.DataObject.prototype._dataTypes = []; +Zotero.DataObject.prototype._dataTypes = ['primaryData']; -Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'objectType', { +Zotero.defineProperty(Zotero.DataObject.prototype, 'objectType', { get: function() this._objectType }); -Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'libraryKey', { +Zotero.defineProperty(Zotero.DataObject.prototype, 'id', { + get: function() this._id +}); +Zotero.defineProperty(Zotero.DataObject.prototype, 'libraryID', { + get: function() this._libraryID +}); +Zotero.defineProperty(Zotero.DataObject.prototype, 'key', { + get: function() this._key +}); +Zotero.defineProperty(Zotero.DataObject.prototype, 'libraryKey', { get: function() this._libraryID + "/" + this._key }); -Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'parentKey', { +Zotero.defineProperty(Zotero.DataObject.prototype, 'parentKey', { get: function() this._parentKey, set: function(v) this._setParentKey(v) }); -Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'parentID', { +Zotero.defineProperty(Zotero.DataObject.prototype, 'parentID', { get: function() this._getParentID(), set: function(v) this._setParentID(v) }); +Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', { + get: function() this._ObjectsClass +}); + Zotero.DataObject.prototype._get = function (field) { if (this['_' + field] !== null) { @@ -135,7 +150,7 @@ Zotero.DataObject.prototype._getParentID = function () { if (!this._parentKey) { return false; } - return this._parentID = this._getClass().getIDFromLibraryAndKey(this._libraryID, this._parentKey); + return this._parentID = this.ObjectsClass.getIDFromLibraryAndKey(this._libraryID, this._parentKey); } @@ -148,7 +163,7 @@ Zotero.DataObject.prototype._getParentID = function () { Zotero.DataObject.prototype._setParentID = function (id) { return this._setParentKey( id - ? this._getClass().getLibraryAndKeyFromID(Zotero.DataObjectUtilities.checkDataID(id))[1] + ? this.ObjectsClass.getLibraryAndKeyFromID(Zotero.DataObjectUtilities.checkDataID(id))[1] : null ); } @@ -309,6 +324,60 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function return false; }); +/* + * Build object from database + */ +Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) { + if (this._loaded.primaryData && !reload) return; + + var id = this.id; + var key = this.key; + var libraryID = this.libraryID; + + if (!id && !key) { + throw new Error('ID or key not set in Zotero.' + this._ObjectType + '.loadPrimaryData()'); + } + + var columns = [], join = [], where = []; + var primaryFields = this.ObjectsClass.primaryFields; + var idField = this.ObjectsClass.idColumn; + for (let i=0; i<primaryFields.length; i++) { + let field = primaryFields[i]; + // If field not already set + if (field == idField || this['_' + field] === null || reload) { + columns.push(this.ObjectsClass.getPrimaryDataSQLPart(field)); + } + } + if (!columns.length) { + return; + } + + // This should match Zotero.*.primaryDataSQL, but without + // necessarily including all columns + var sql = "SELECT " + columns.join(", ") + this.ObjectsClass.primaryDataSQLFrom; + if (id) { + sql += " AND O." + idField + "=? "; + var params = id; + } + else { + sql += " AND O.key=? AND O.libraryID=? "; + var params = [key, libraryID]; + } + sql += (where.length ? ' AND ' + where.join(' AND ') : ''); + var row = yield Zotero.DB.rowQueryAsync(sql, params); + + if (!row) { + if (failOnMissing) { + throw new Error(this._ObjectType + " " + (id ? id : libraryID + "/" + key) + + " not found in Zotero." + this._ObjectType + ".loadPrimaryData()"); + } + this._loaded.primaryData = true; + this._clearChanged('primaryData'); + return; + } + + this.loadFromRow(row, reload); +}); /** * Reloads loaded, changed data @@ -368,13 +437,6 @@ Zotero.DataObject.prototype._requireData = function (dataType) { } } -/** - * Returns a global Zotero class object given a data object. (e.g. Zotero.Items) - * @return {obj} One of Zotero data classes - */ -Zotero.DataObject.prototype._getClass = function () { - return Zotero.DataObjectUtilities.getClassForObjectType(this._objectType); -} /** * Loads data for a given data type @@ -385,6 +447,14 @@ Zotero.DataObject.prototype._loadDataType = function (dataType, reload) { return this["load" + dataType[0].toUpperCase() + dataType.substr(1)](reload); } +Zotero.DataObject.prototype.loadAllData = function (reload) { + let loadPromises = new Array(this._dataTypes.length); + for (let i=0; i<this._dataTypes.length; i++) { + loadPromises[i] = this._loadDataType(this._dataTypes[i], reload); + } + + return Zotero.Promise.all(loadPromises); +} /** * Save old version of data that's being changed, to pass to the notifier @@ -422,6 +492,141 @@ Zotero.DataObject.prototype._clearFieldChange = function (field) { delete this._previousData[field]; } + +Zotero.DataObject.prototype.isEditable = function () { + return Zotero.Libraries.isEditable(this.libraryID); +} + + +Zotero.DataObject.prototype.editCheck = function () { + if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable()) { + throw ("Cannot edit " + this._objectType + " in read-only Zotero library"); + } +} + +/** + * Save changes to database + * + * @return {Promise<Integer|Boolean>} Promise for itemID of new item, + * TRUE on item update, or FALSE if item was unchanged + */ +Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) { + var env = { + transactionOptions: null, + options: options || {} + }; + + var proceed = yield this._initSave(env); + if (!proceed) return false; + + if (env.isNew) { + Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4); + } + else { + Zotero.debug('Updating database with new ' + this._objectType + ' data', 4); + } + + return Zotero.DB.executeTransaction(function* () { + yield this._saveData(env); + return yield this._finalizeSave(env); + }.bind(this), env.transactionOptions) + .catch(e => { + return this._recoverFromSaveError(env, e) + .catch(function(e2) { + Zotero.debug(e2, 1); + }) + .then(function() { + Zotero.debug(e, 1); + throw e; + }) + }); +}); + +Zotero.DataObject.prototype.hasChanged = function() { + Zotero.debug(this._changed); + return !!Object.keys(this._changed).filter(dataType => this._changed[dataType]).length +} + +Zotero.DataObject.prototype._saveData = function() { + throw new Error("Zotero.DataObject.prototype._saveData is an abstract method"); +} + +Zotero.DataObject.prototype._finalizeSave = function() { + throw new Error("Zotero.DataObject.prototype._finalizeSave is an abstract method"); +} + +Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* () { + yield this.reload(null, true); + this._clearChanged(); +}); + +Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + env.isNew = !this.id; + + if (!env.options.skipEditCheck) this.editCheck(); + + if (!this.hasChanged()) { + Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4); + return false; + } + + // Register this object's identifiers in Zotero.DataObjects on transaction commit, + // before other callbacks run + if (env.isNew) { + env.transactionOptions = { + onCommit: () => { + this.ObjectsClass.registerIdentifiers(env.id, env.libraryID, env.key); + } + }; + } + + return true; +}); + +/** + * Delete object from database + */ +Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* () { + var env = {}; + + var proceed = yield this._eraseInit(env); + if (!proceed) return false; + + Zotero.debug('Deleting ' + this.objectType + ' ' + this.id); + + yield Zotero.DB.executeTransaction(function* () { + yield this._eraseData(env); + yield this._erasePreCommit(env); + }.bind(this)) + .catch(e => { + return this._eraseRecoverFromFailure(env); + }); + + return this._erasePostCommit(env); +}); + +Zotero.DataObject.prototype._eraseInit = function(env) { + if (!this.id) return Zotero.Promise.resolve(false); + + return Zotero.Promise.resolve(true); +}; + +Zotero.DataObject.prototype._eraseData = function(env) { + throw new Error("Zotero.DataObject.prototype._eraseData is an abstract method"); +}; + +Zotero.DataObject.prototype._erasePreCommit = function(env) { + return Zotero.Promise.resolve(); +}; + +Zotero.DataObject.prototype._erasePostCommit = function(env) { + return Zotero.Promise.resolve(); +}; + +Zotero.DataObject.prototype._eraseRecoverFromFailure = function(env) { + throw new Error("Zotero.DataObject.prototype._eraseRecoverFromFailure is an abstract method"); +}; + /** * Generates data object key * @return {String} key diff --git a/chrome/content/zotero/xpcom/data/dataObjectUtilities.js b/chrome/content/zotero/xpcom/data/dataObjectUtilities.js @@ -59,12 +59,12 @@ Zotero.DataObjectUtilities = { }, - "getObjectTypePlural": function getObjectTypePlural(objectType) { + "getObjectTypePlural": function(objectType) { return objectType == 'search' ? 'searches' : objectType + 's'; }, - "getClassForObjectType": function getClassForObjectType(objectType) { + "getObjectsClassForObjectType": function(objectType) { var objectTypePlural = this.getObjectTypePlural(objectType); var className = objectTypePlural[0].toUpperCase() + objectTypePlural.substr(1); return Zotero[className] diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js @@ -24,606 +24,608 @@ */ -Zotero.DataObjects = function (object, objectPlural, id, table) { - var self = this; +Zotero.DataObjects = function () { + if (!this._ZDO_object) throw new Error('this._ZDO_object must be set before calling Zotero.DataObjects constructor'); - if (!object) { - object = ''; + if (!this._ZDO_objects) { + this._ZDO_objects = Zotero.DataObjectUtilities.getObjectTypePlural(this._ZDO_object); + } + if (!this._ZDO_Object) { + this._ZDO_Object = this._ZDO_object.substr(0, 1).toUpperCase() + + this._ZDO_object.substr(1); + } + if (!this._ZDO_Objects) { + this._ZDO_Objects = this._ZDO_objects.substr(0, 1).toUpperCase() + + this._ZDO_objects.substr(1); + } + + if (!this._ZDO_id) { + this._ZDO_id = this._ZDO_object + 'ID'; } - // Override these variables in child objects - this._ZDO_object = object; - this._ZDO_objects = objectPlural ? objectPlural : object + 's'; - this._ZDO_Object = object.substr(0, 1).toUpperCase() + object.substr(1); - this._ZDO_Objects = this._ZDO_objects.substr(0, 1).toUpperCase() - + this._ZDO_objects.substr(1); - this._ZDO_id = (id ? id : object) + 'ID'; - this._ZDO_table = table ? table : this._ZDO_objects; + if (!this._ZDO_table) { + this._ZDO_table = this._ZDO_objects; + } - // Certain object types don't have a libary and key and only use an id - switch (object) { - case 'relation': - this._ZDO_idOnly = true; - break; - - default: - this._ZDO_idOnly = false; + if (!this.ObjectClass) { + this.ObjectClass = Zotero[this._ZDO_Object]; } + this.primaryDataSQLFrom = " " + this._primaryDataSQLFrom + " " + this._primaryDataSQLWhere; + this._objectCache = {}; this._objectKeys = {}; this._objectIDs = {}; this._loadedLibraries = {}; this._loadPromise = null; +} + +Zotero.DataObjects.prototype._ZDO_idOnly = false; + +// Public properties +Zotero.defineProperty(Zotero.DataObjects.prototype, 'idColumn', { + get: function() this._ZDO_id +}); +Zotero.defineProperty(Zotero.DataObjects.prototype, 'table', { + get: function() this._ZDO_table +}); + +Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryFields', { + get: function () Object.keys(this._primaryDataSQLParts) +}, {lazy: true}); + + +Zotero.DataObjects.prototype.init = function() { + return this._loadIDsAndKeys(); +} + + +Zotero.DataObjects.prototype.isPrimaryField = function (field) { + return this.primaryFields.indexOf(field) != -1; +} + + +/** + * Retrieves one or more already-loaded items + * + * If an item hasn't been loaded, an error is thrown + * + * @param {Array|Integer} ids An individual object id or an array of object ids + * @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed; + * otherwise, an array of Zotero.[Object] + */ +Zotero.DataObjects.prototype.get = function (ids) { + if (Array.isArray(ids)) { + var singleObject = false; + } + else { + var singleObject = true; + ids = [ids]; + } - // Public properties - this.table = this._ZDO_table; + var toReturn = []; + for (let i=0; i<ids.length; i++) { + let id = ids[i]; + // Check if already loaded + if (!this._objectCache[id]) { + throw new Zotero.Exception.UnloadedDataException(this._ZDO_Object + " " + id + " not yet loaded"); + } + toReturn.push(this._objectCache[id]); + } - this.init = function () { - return this._loadIDsAndKeys(); + // If single id, return the object directly + if (singleObject) { + return toReturn.length ? toReturn[0] : false; } + return toReturn; +}; - this.__defineGetter__('primaryFields', function () { - var primaryFields = Object.keys(this._primaryDataSQLParts); - - // Once primary fields have been cached, get rid of getter for speed purposes - delete this.primaryFields; - this.primaryFields = primaryFields; - - return primaryFields; - }); +/** + * Retrieves (and loads, if necessary) one or more items + * + * @param {Array|Integer} ids An individual object id or an array of object ids + * @param {Object} options 'noCache': Don't cache loaded objects + * @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed; + * otherwise, an array of Zotero.[Object] + */ +Zotero.DataObjects.prototype.getAsync = Zotero.Promise.coroutine(function* (ids, options) { + var toLoad = []; + var toReturn = []; - this.isPrimaryField = function (field) { - return this.primaryFields.indexOf(field) != -1; + if (!ids) { + throw new Error("No arguments provided to " + this._ZDO_Objects + ".get()"); } + if (Array.isArray(ids)) { + var singleObject = false; + } + else { + var singleObject = true; + ids = [ids]; + } - /** - * Retrieves one or more already-loaded items - * - * If an item hasn't been loaded, an error is thrown - * - * @param {Array|Integer} ids An individual object id or an array of object ids - * @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed; - * otherwise, an array of Zotero.[Object] - */ - this.get = function (ids) { - if (Array.isArray(ids)) { - var singleObject = false; - } - else { - var singleObject = true; - ids = [ids]; - } - - var toReturn = []; - - for (let i=0; i<ids.length; i++) { - let id = ids[i]; - // Check if already loaded - if (!this._objectCache[id]) { - throw new Zotero.Exception.UnloadedDataException(this._ZDO_Object + " " + id + " not yet loaded"); - } + for (let i=0; i<ids.length; i++) { + let id = ids[i]; + // Check if already loaded + if (this._objectCache[id]) { toReturn.push(this._objectCache[id]); } - - // If single id, return the object directly - if (singleObject) { - return toReturn.length ? toReturn[0] : false; + else { + toLoad.push(id); } - - return toReturn; - }; - + } - /** - * Retrieves (and loads, if necessary) one or more items - * - * @param {Array|Integer} ids An individual object id or an array of object ids - * @param {Object} options 'noCache': Don't cache loaded objects - * @return {Zotero.[Object]|Array<Zotero.[Object]>} A Zotero.[Object], if a scalar id was passed; - * otherwise, an array of Zotero.[Object] - */ - this.getAsync = Zotero.Promise.coroutine(function* (ids, options) { + // New object to load + if (toLoad.length) { // Serialize loads if (this._loadPromise && this._loadPromise.isPending()) { yield this._loadPromise; } - var deferred = Zotero.Promise.defer(); + let deferred = Zotero.Promise.defer(); this._loadPromise = deferred.promise; - var toLoad = []; - var toReturn = []; - - if (!ids) { - throw new Error("No arguments provided to " + this._ZDO_Objects + ".get()"); - } - - if (Array.isArray(ids)) { - var singleObject = false; - } - else { - var singleObject = true; - ids = [ids]; - } - - for (let i=0; i<ids.length; i++) { - let id = ids[i]; - // Check if already loaded - if (this._objectCache[id]) { - toReturn.push(this._objectCache[id]); - } - else { - toLoad.push(id); - } - } - - // New object to load - if (toLoad.length) { - let loaded = yield this._load(null, toLoad, options); - for (let i=0; i<toLoad.length; i++) { - let id = toLoad[i]; - let obj = loaded[id]; - if (!obj) { - Zotero.debug(this._ZDO_Object + " " + id + " doesn't exist", 2); - continue; - } - toReturn.push(obj); + let loaded = yield this._load(null, toLoad, options); + for (let i=0; i<toLoad.length; i++) { + let id = toLoad[i]; + let obj = loaded[id]; + if (!obj) { + Zotero.debug(this._ZDO_Object + " " + id + " doesn't exist", 2); + continue; } + toReturn.push(obj); } - deferred.resolve(); - - // If single id, return the object directly - if (singleObject) { - return toReturn.length ? toReturn[0] : false; - } - - return toReturn; - }); - - - /** - * @deprecated - use .libraryKey - */ - this.makeLibraryKeyHash = function (libraryID, key) { - Zotero.debug("WARNING: Zotero.DataObjects.makeLibraryKeyHash() is deprecated -- use obj.libraryKey instead"); - return libraryID + '_' + key; } - - /** - * @deprecated - use .libraryKey - */ - this.getLibraryKeyHash = function (obj) { - Zotero.debug("WARNING: Zotero.DataObjects.getLibraryKeyHash() is deprecated -- use obj.libraryKey instead"); - return this.makeLibraryKeyHash(obj.libraryID, obj.key); + // If single id, return the object directly + if (singleObject) { + return toReturn.length ? toReturn[0] : false; } - - this.parseLibraryKey = function (libraryKey) { - var [libraryID, key] = libraryKey.split('/'); - return { - libraryID: parseInt(libraryID), - key: key - }; + return toReturn; +}); + + +/** + * @deprecated - use .libraryKey + */ +Zotero.DataObjects.prototype.makeLibraryKeyHash = function (libraryID, key) { + Zotero.debug("WARNING: " + this._ZDO_Objects + ".makeLibraryKeyHash() is deprecated -- use .libraryKey instead"); + return libraryID + '_' + key; +} + + +/** + * @deprecated - use .libraryKey + */ +Zotero.DataObjects.prototype.getLibraryKeyHash = function (obj) { + Zotero.debug("WARNING: " + this._ZDO_Objects + ".getLibraryKeyHash() is deprecated -- use .libraryKey instead"); + return this.makeLibraryKeyHash(obj.libraryID, obj.key); +} + + +Zotero.DataObjects.prototype.parseLibraryKey = function (libraryKey) { + var [libraryID, key] = libraryKey.split('/'); + return { + libraryID: parseInt(libraryID), + key: key + }; +} + + +/** + * @deprecated - Use Zotero.DataObjects.parseLibraryKey() + */ +Zotero.DataObjects.prototype.parseLibraryKeyHash = function (libraryKey) { + Zotero.debug("WARNING: " + this._ZDO_Objects + ".parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead"); + var [libraryID, key] = libraryKey.split('_'); + if (!key) { + return false; } - - - /** - * @deprecated - Use Zotero.DataObjects.parseLibraryKey() - */ - this.parseLibraryKeyHash = function (libraryKey) { - Zotero.debug("WARNING: Zotero.DataObjects.parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead"); - var [libraryID, key] = libraryKey.split('_'); - if (!key) { - return false; - } - return { - libraryID: parseInt(libraryID), - key: key - }; - } - - - /** - * Retrieves an object by its libraryID and key - * - * @param {Integer} libraryID - * @param {String} key - * @return {Zotero.DataObject} Zotero data object, or FALSE if not found - */ - this.getByLibraryAndKey = function (libraryID, key, options) { - var id = this.getIDFromLibraryAndKey(libraryID, key); - if (!id) { - return false; - } - return Zotero[this._ZDO_Objects].get(id, options); + return { + libraryID: parseInt(libraryID), + key: key }; - - - /** - * Asynchronously retrieves an object by its libraryID and key - * - * @param {Integer} - libraryID - * @param {String} - key - * @return {Promise<Zotero.DataObject>} - Promise for a data object, or FALSE if not found - */ - this.getByLibraryAndKeyAsync = Zotero.Promise.coroutine(function* (libraryID, key, options) { - var id = this.getIDFromLibraryAndKey(libraryID, key); - if (!id) { - return false; - } - return Zotero[this._ZDO_Objects].getAsync(id, options); - }); - - - this.exists = function (itemID) { - return !!this.getLibraryAndKeyFromID(itemID); +} + + +/** + * Retrieves an object by its libraryID and key + * + * @param {Integer} libraryID + * @param {String} key + * @return {Zotero.DataObject} Zotero data object, or FALSE if not found + */ +Zotero.DataObjects.prototype.getByLibraryAndKey = function (libraryID, key, options) { + var id = this.getIDFromLibraryAndKey(libraryID, key); + if (!id) { + return false; } - - - /** - * @return {Array} Array with libraryID and key - */ - this.getLibraryAndKeyFromID = function (id) { - return this._objectKeys[id] ? this._objectKeys[id] : false; + return Zotero[this._ZDO_Objects].get(id, options); +}; + + +/** + * Asynchronously retrieves an object by its libraryID and key + * + * @param {Integer} - libraryID + * @param {String} - key + * @return {Promise<Zotero.DataObject>} - Promise for a data object, or FALSE if not found + */ +Zotero.DataObjects.prototype.getByLibraryAndKeyAsync = Zotero.Promise.coroutine(function* (libraryID, key, options) { + var id = this.getIDFromLibraryAndKey(libraryID, key); + if (!id) { + return false; + } + return Zotero[this._ZDO_Objects].getAsync(id, options); +}); + + +Zotero.DataObjects.prototype.exists = function (itemID) { + return !!this.getLibraryAndKeyFromID(itemID); +} + + +/** + * @return {Array} Array with libraryID and key + */ +Zotero.DataObjects.prototype.getLibraryAndKeyFromID = function (id) { + return this._objectKeys[id] ? this._objectKeys[id] : false; +} + + +Zotero.DataObjects.prototype.getIDFromLibraryAndKey = function (libraryID, key) { + if (libraryID === null) { + throw new Error("libraryID cannot be NULL (did you mean 0?)"); + } + return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key]) + ? this._objectIDs[libraryID][key] : false; +} + + +Zotero.DataObjects.prototype.getOlder = function (libraryID, date) { + if (!date || date.constructor.name != 'Date') { + throw ("date must be a JS Date in " + + "Zotero." + this._ZDO_Objects + ".getOlder()") } + var sql = "SELECT ROWID FROM " + this._ZDO_table + + " WHERE libraryID=? AND clientDateModified<?"; + return Zotero.DB.columnQuery(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]); +} + + +Zotero.DataObjects.prototype.getNewer = function (libraryID, date, ignoreFutureDates) { + if (!date || date.constructor.name != 'Date') { + throw ("date must be a JS Date in " + + "Zotero." + this._ZDO_Objects + ".getNewer()") + } - this.getIDFromLibraryAndKey = function (libraryID, key) { - if (libraryID === null) { - throw new Error("libraryID cannot be NULL (did you mean 0?)"); + var sql = "SELECT ROWID FROM " + this._ZDO_table + + " WHERE libraryID=? AND clientDateModified>?"; + if (ignoreFutureDates) { + sql += " AND clientDateModified<=CURRENT_TIMESTAMP"; + } + return Zotero.DB.columnQuery(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]); +} + + +/** + * @param {Integer} libraryID + * @return {Promise} A promise for an array of object ids + */ +Zotero.DataObjects.prototype.getUnsynced = function (libraryID) { + var sql = "SELECT " + this._ZDO_id + " FROM " + this._ZDO_table + + " WHERE libraryID=? AND synced=0"; + return Zotero.DB.columnQueryAsync(sql, [libraryID]); +} + + +/** + * Get JSON from the sync cache that hasn't yet been written to the + * main object tables + * + * @param {Integer} libraryID + * @return {Promise} A promise for an array of JSON objects + */ +Zotero.DataObjects.prototype.getUnwrittenData = function (libraryID) { + var sql = "SELECT data FROM syncCache SC " + + "LEFT JOIN " + this._ZDO_table + " " + + "USING (libraryID) " + + "WHERE SC.libraryID=? AND " + + "syncObjectTypeID IN (SELECT syncObjectTypeID FROM " + + "syncObjectTypes WHERE name='" + this._ZDO_object + "') " + + "AND IFNULL(O.version, 0) < SC.version"; + return Zotero.DB.columnQueryAsync(sql, [libraryID]); +} + + +/** + * Reload loaded data of loaded objects + * + * @param {Array|Number} ids - An id or array of ids + * @param {Array} [dataTypes] - Data types to reload (e.g., 'primaryData'), or all loaded + * types if not provided + * @param {Boolean} [reloadUnchanged=false] - Reload even data that hasn't changed internally. + * This should be set to true for data that was + * changed externally (e.g., globally renamed tags). + */ +Zotero.DataObjects.prototype.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) { + ids = Zotero.flattenArguments(ids); + + Zotero.debug('Reloading ' + (dataTypes ? dataTypes + ' for ' : '') + + this._ZDO_objects + ' ' + ids); + + for (let i=0; i<ids.length; i++) { + if (this._objectCache[ids[i]]) { + yield this._objectCache[ids[i]].reload(dataTypes, reloadUnchanged); } - return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key]) - ? this._objectIDs[libraryID][key] : false; } - - this.getOlder = function (libraryID, date) { - if (!date || date.constructor.name != 'Date') { - throw ("date must be a JS Date in " - + "Zotero." + this._ZDO_Objects + ".getOlder()") + return true; +}); + + +Zotero.DataObjects.prototype.reloadAll = function (libraryID) { + Zotero.debug("Reloading all " + this._ZDO_objects); + + // Remove objects not stored in database + var sql = "SELECT ROWID FROM " + this._ZDO_table; + var params = []; + if (libraryID !== undefined) { + sql += ' WHERE libraryID=?'; + params.push(libraryID); + } + return Zotero.DB.columnQueryAsync(sql, params) + .then(function (ids) { + for (var id in this._objectCache) { + if (!ids || ids.indexOf(parseInt(id)) == -1) { + delete this._objectCache[id]; + } } - var sql = "SELECT ROWID FROM " + this._ZDO_table - + " WHERE libraryID=? AND clientDateModified<?"; - return Zotero.DB.columnQuery(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]); + // Reload data + this._loadedLibraries[libraryID] = false; + return this._load(libraryID); + }); +} + + +Zotero.DataObjects.prototype.registerIdentifiers = function (id, libraryID, key) { + Zotero.debug("Registering " + this._ZDO_object + " " + id + " as " + libraryID + "/" + key); + if (!this._objectIDs[libraryID]) { + this._objectIDs[libraryID] = {}; } - - - this.getNewer = function (libraryID, date, ignoreFutureDates) { - if (!date || date.constructor.name != 'Date') { - throw ("date must be a JS Date in " - + "Zotero." + this._ZDO_Objects + ".getNewer()") + this._objectIDs[libraryID][key] = id; + this._objectKeys[id] = [libraryID, key]; +} + + +/** + * Clear object from internal array + * + * @param int[] ids objectIDs + */ +Zotero.DataObjects.prototype.unload = function () { + var ids = Zotero.flattenArguments(arguments); + for (var i=0; i<ids.length; i++) { + let id = ids[i]; + let [libraryID, key] = this.getLibraryAndKeyFromID(id); + if (key) { + delete this._objectIDs[libraryID][key]; + delete this._objectKeys[id]; + } + delete this._objectCache[id]; + } +} + + +/** + * @param {Object} data1 - JSON of first object + * @param {Object} data2 - JSON of second object + * @param {Array} diff - Empty array to put diff data in + * @param {Boolean} [includeMatches=false] - Include all fields, even those + * that aren't different + */ +Zotero.DataObjects.prototype.diff = function (data1, data2, diff, includeMatches) { + diff.push({}, {}); + var numDiffs = 0; + + var skipFields = ['collectionKey', 'itemKey', 'searchKey']; + + for (var field in data1) { + if (skipFields.indexOf(field) != -1) { + continue; } - var sql = "SELECT ROWID FROM " + this._ZDO_table - + " WHERE libraryID=? AND clientDateModified>?"; - if (ignoreFutureDates) { - sql += " AND clientDateModified<=CURRENT_TIMESTAMP"; + if (data1[field] === false && (data2[field] === false || data2[field] === undefined)) { + continue; } - return Zotero.DB.columnQuery(sql, [libraryID, Zotero.Date.dateToSQL(date, true)]); - } - - - /** - * @param {Integer} libraryID - * @return {Promise} A promise for an array of object ids - */ - this.getUnsynced = function (libraryID) { - var sql = "SELECT " + this._ZDO_id + " FROM " + this._ZDO_table - + " WHERE libraryID=? AND synced=0"; - return Zotero.DB.columnQueryAsync(sql, [libraryID]); - } - - - /** - * Get JSON from the sync cache that hasn't yet been written to the - * main object tables - * - * @param {Integer} libraryID - * @return {Promise} A promise for an array of JSON objects - */ - this.getUnwrittenData = function (libraryID) { - var sql = "SELECT data FROM syncCache SC " - + "LEFT JOIN " + this._ZDO_table + " " - + "USING (libraryID) " - + "WHERE SC.libraryID=? AND " - + "syncObjectTypeID IN (SELECT syncObjectTypeID FROM " - + "syncObjectTypes WHERE name='" + this._ZDO_object + "') " - + "AND IFNULL(O.version, 0) < SC.version"; - return Zotero.DB.columnQueryAsync(sql, [libraryID]); - } - - - /** - * Reload loaded data of loaded objects - * - * @param {Array|Number} ids - An id or array of ids - * @param {Array} [dataTypes] - Data types to reload (e.g., 'primaryData'), or all loaded - * types if not provided - * @param {Boolean} [reloadUnchanged=false] - Reload even data that hasn't changed internally. - * This should be set to true for data that was - * changed externally (e.g., globally renamed tags). - */ - this.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) { - ids = Zotero.flattenArguments(ids); - Zotero.debug('Reloading ' + (dataTypes ? dataTypes + ' for ' : '') - + this._ZDO_objects + ' ' + ids); + var changed = data1[field] !== data2[field]; - for (let i=0; i<ids.length; i++) { - if (this._objectCache[ids[i]]) { - yield this._objectCache[ids[i]].reload(dataTypes, reloadUnchanged); - } + if (includeMatches || changed) { + diff[0][field] = data1[field] !== false ? data1[field] : ''; + diff[1][field] = (data2[field] !== false && data2[field] !== undefined) + ? data2[field] : ''; } - return true; - }); - - - this.reloadAll = function (libraryID) { - Zotero.debug("Reloading all " + this._ZDO_objects); - - // Remove objects not stored in database - var sql = "SELECT ROWID FROM " + this._ZDO_table; - var params = []; - if (libraryID !== undefined) { - sql += ' WHERE libraryID=?'; - params.push(libraryID); + if (changed) { + numDiffs++; } - return Zotero.DB.columnQueryAsync(sql, params) - .then(function (ids) { - for (var id in this._objectCache) { - if (!ids || ids.indexOf(parseInt(id)) == -1) { - delete this._objectCache[id]; - } - } - - // Reload data - this._loadedLibraries[libraryID] = false; - return this._load(libraryID); - }); } - - this.registerIdentifiers = function (id, libraryID, key) { - Zotero.debug("Registering " + this._ZDO_object + " " + id + " as " + libraryID + "/" + key); - if (!this._objectIDs[libraryID]) { - this._objectIDs[libraryID] = {}; - } - this._objectIDs[libraryID][key] = id; - this._objectKeys[id] = [libraryID, key]; - } - - - /** - * Clear object from internal array - * - * @param int[] ids objectIDs - */ - this.unload = function () { - var ids = Zotero.flattenArguments(arguments); - for (var i=0; i<ids.length; i++) { - let id = ids[i]; - let [libraryID, key] = this.getLibraryAndKeyFromID(id); - if (key) { - delete this._objectIDs[libraryID][key]; - delete this._objectKeys[id]; - } - delete this._objectCache[id]; + // DEBUG: some of this is probably redundant + for (var field in data2) { + if (skipFields.indexOf(field) != -1) { + continue; } - } - - - /** - * @param {Object} data1 - JSON of first object - * @param {Object} data2 - JSON of second object - * @param {Array} diff - Empty array to put diff data in - * @param {Boolean} [includeMatches=false] - Include all fields, even those - * that aren't different - */ - this.diff = function (data1, data2, diff, includeMatches) { - diff.push({}, {}); - var numDiffs = 0; - var skipFields = ['collectionKey', 'itemKey', 'searchKey']; + if (diff[0][field] !== undefined) { + continue; + } - for (var field in data1) { - if (skipFields.indexOf(field) != -1) { - continue; - } - - if (data1[field] === false && (data2[field] === false || data2[field] === undefined)) { - continue; - } - - var changed = data1[field] !== data2[field]; - - if (includeMatches || changed) { - diff[0][field] = data1[field] !== false ? data1[field] : ''; - diff[1][field] = (data2[field] !== false && data2[field] !== undefined) - ? data2[field] : ''; - } - - if (changed) { - numDiffs++; - } + if (data2[field] === false && (data1[field] === false || data1[field] === undefined)) { + continue; } - // DEBUG: some of this is probably redundant - for (var field in data2) { - if (skipFields.indexOf(field) != -1) { - continue; - } - - if (diff[0][field] !== undefined) { - continue; - } - - if (data2[field] === false && (data1[field] === false || data1[field] === undefined)) { - continue; - } - - var changed = data1[field] !== data2[field]; - - if (includeMatches || changed) { - diff[0][field] = (data1[field] !== false && data1[field] !== undefined) - ? data1[field] : ''; - diff[1][field] = data2[field] !== false ? data2[field] : ''; - } - - if (changed) { - numDiffs++; - } + var changed = data1[field] !== data2[field]; + + if (includeMatches || changed) { + diff[0][field] = (data1[field] !== false && data1[field] !== undefined) + ? data1[field] : ''; + diff[1][field] = data2[field] !== false ? data2[field] : ''; } - return numDiffs; + if (changed) { + numDiffs++; + } + } + + return numDiffs; +} + + +Zotero.DataObjects.prototype.isEditable = function (obj) { + var libraryID = obj.libraryID; + if (!libraryID) { + return true; } + if (!Zotero.Libraries.isEditable(libraryID)) return false; - this.isEditable = function (obj) { - var libraryID = obj.libraryID; - if (!libraryID) { - return true; - } - var type = Zotero.Libraries.getType(libraryID); - switch (type) { - case 'user': - return true; - - case 'group': - var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID); - var group = Zotero.Groups.get(groupID); - if (!group.editable) { - return false; - } - if (obj.objectType == 'item' && obj.isAttachment() - && (obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL || - obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE)) { - return group.filesEditable; - } - return true; - - default: - throw ("Unsupported library type '" + type + "' in Zotero.DataObjects.isEditable()"); - } + if (obj.objectType == 'item' && obj.isAttachment() + && (obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL || + obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) + && !Zotero.Libraries.isFilesEditable(libraryID) + ) { + return false; } + return true; +} + + +Zotero.DataObjects.prototype.editCheck = function (obj) { + if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable(obj)) { + throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library"); + } +} + +Zotero.defineProperty(Zotero.DataObjects.prototype, "primaryDataSQL", { + get: function () { + return "SELECT " + + Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ') + + this.primaryDataSQLFrom; + } +}, {lazy: true}); + +Zotero.DataObjects.prototype._primaryDataSQLWhere = "WHERE 1"; + +Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) { + var sql = this._primaryDataSQLParts[part]; + if (!sql) { + throw new Error("Invalid primary data SQL part '" + part + "'"); + } + return sql; +} + + +Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (libraryID, ids, options) { + var loaded = {}; - this.editCheck = function (obj) { - if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable(obj)) { - throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library"); - } + // If library isn't an integer (presumably false or null), skip it + if (parseInt(libraryID) != libraryID) { + libraryID = false; } + if (libraryID === false && !ids) { + throw new Error("Either libraryID or ids must be provided"); + } - this.getPrimaryDataSQLPart = function (part) { - var sql = this._primaryDataSQLParts[part]; - if (!sql) { - throw new Error("Invalid primary data SQL part '" + part + "'"); - } - return sql; + if (libraryID !== false && this._loadedLibraries[libraryID]) { + return loaded; } + // getPrimaryDataSQL() should use "O" for the primary table alias + var sql = this.primaryDataSQL; + var params = []; + if (libraryID !== false) { + sql += ' AND O.libraryID=?'; + params.push(libraryID); + } + if (ids) { + sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')'; + } - this._load = Zotero.Promise.coroutine(function* (libraryID, ids, options) { - var loaded = {}; - - // If library isn't an integer (presumably false or null), skip it - if (parseInt(libraryID) != libraryID) { - libraryID = false; - } - - if (libraryID === false && !ids) { - throw new Error("Either libraryID or ids must be provided"); - } - - if (libraryID !== false && this._loadedLibraries[libraryID]) { - return loaded; - } - - // getPrimaryDataSQL() should use "O" for the primary table alias - var sql = this.getPrimaryDataSQL(); - var params = []; - if (libraryID !== false) { - sql += ' AND O.libraryID=?'; - params.push(libraryID); - } - if (ids) { - sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')'; - } - - var t = new Date(); - yield Zotero.DB.queryAsync( - sql, - params, - { - onRow: function (row) { - var id = row.getResultByIndex(this._ZDO_id); - var columns = Object.keys(this._primaryDataSQLParts); - var rowObj = {}; - for (let i=0; i<columns.length; i++) { - rowObj[columns[i]] = row.getResultByIndex(i); - } - var obj; - - // Existing object -- reload in place - if (this._objectCache[id]) { - this._objectCache[id].loadFromRow(rowObj, true); - obj = this._objectCache[id]; - } - // Object doesn't exist -- create new object and stuff in cache - else { - obj = new Zotero[this._ZDO_Object]; - obj.loadFromRow(rowObj, true); - if (!options || !options.noCache) { - this._objectCache[id] = obj; - } - } - loaded[id] = obj; - }.bind(this) - } - ); - Zotero.debug("Loaded " + this._ZDO_objects + " in " + ((new Date) - t) + "ms"); - - if (!ids) { - this._loadedLibraries[libraryID] = true; - - // If loading all objects, remove cached objects that no longer exist - for (let i in this._objectCache) { - let obj = this._objectCache[i]; - if (libraryID !== false && obj.libraryID !== libraryID) { - continue; + var t = new Date(); + yield Zotero.DB.queryAsync( + sql, + params, + { + onRow: function (row) { + var id = row.getResultByIndex(this._ZDO_id); + var columns = Object.keys(this._primaryDataSQLParts); + var rowObj = {}; + for (let i=0; i<columns.length; i++) { + rowObj[columns[i]] = row.getResultByIndex(i); } - if (!loaded[obj.id]) { - this.unload(obj.id); + var obj; + + // Existing object -- reload in place + if (this._objectCache[id]) { + this._objectCache[id].loadFromRow(rowObj, true); + obj = this._objectCache[id]; } + // Object doesn't exist -- create new object and stuff in cache + else { + obj = new Zotero[this._ZDO_Object]; + obj.loadFromRow(rowObj, true); + if (!options || !options.noCache) { + this._objectCache[id] = obj; + } + } + loaded[id] = obj; + }.bind(this) + } + ); + Zotero.debug("Loaded " + this._ZDO_objects + " in " + ((new Date) - t) + "ms"); + + if (!ids) { + this._loadedLibraries[libraryID] = true; + + // If loading all objects, remove cached objects that no longer exist + for (let i in this._objectCache) { + let obj = this._objectCache[i]; + if (libraryID !== false && obj.libraryID !== libraryID) { + continue; } - - if (this._postLoad) { - this._postLoad(libraryID, ids); + if (!loaded[obj.id]) { + this.unload(obj.id); } } - return loaded; - }); - - - this._loadIDsAndKeys = Zotero.Promise.coroutine(function* () { - var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table; - var rows = yield Zotero.DB.queryAsync(sql); - for (let i=0; i<rows.length; i++) { - let row = rows[i]; - this._objectKeys[row.id] = [row.libraryID, row.key]; - if (!this._objectIDs[row.libraryID]) { - this._objectIDs[row.libraryID] = {}; - } - this._objectIDs[row.libraryID][row.key] = row.id; + if (this._postLoad) { + this._postLoad(libraryID, ids); } - }); -} + } + + return loaded; +}); + + +Zotero.DataObjects.prototype._loadIDsAndKeys = Zotero.Promise.coroutine(function* () { + var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table; + var rows = yield Zotero.DB.queryAsync(sql); + for (let i=0; i<rows.length; i++) { + let row = rows[i]; + this._objectKeys[row.id] = [row.libraryID, row.key]; + if (!this._objectIDs[row.libraryID]) { + this._objectIDs[row.libraryID] = {}; + } + this._objectIDs[row.libraryID][row.key] = row.id; + } +}); diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -69,9 +69,9 @@ Zotero.Item = function(itemTypeOrID) { this._attachments = null; this._notes = null; - this._tags = {}; - this._collections = {}; - this._relations = {}; + this._tags = []; + this._collections = []; + this._relations = []; this._bestAttachmentState = null; this._fileExists = null; @@ -89,52 +89,81 @@ Zotero.Item = function(itemTypeOrID) { } } -Zotero.Item._super = Zotero.DataObject; -Zotero.Item.prototype = Object.create(Zotero.Item._super.prototype); -Zotero.Item.constructor = Zotero.Item; +Zotero.extendClass(Zotero.DataObject, Zotero.Item); Zotero.Item.prototype._objectType = 'item'; +Zotero.defineProperty(Zotero.Item.prototype, 'ContainerObjectsClass', { + get: function() Zotero.Collections +}); + Zotero.Item.prototype._dataTypes = Zotero.Item._super.prototype._dataTypes.concat([ - 'primaryData', 'itemData', 'note', 'creators', 'childItems', - 'relatedItems', // TODO: remove +// 'relatedItems', // TODO: remove 'tags', 'collections', 'relations' ]); -Zotero.Item.prototype.__defineGetter__('id', function () this._id); -Zotero.Item.prototype.__defineGetter__('itemID', function () { - Zotero.debug("Item.itemID is deprecated -- use Item.id"); - return this._id; +Zotero.defineProperty(Zotero.Item.prototype, 'id', { + get: function() this._id, + set: function(val) this.setField('id', val) +}); +Zotero.defineProperty(Zotero.Item.prototype, 'itemID', { + get: function() { + Zotero.debug("Item.itemID is deprecated -- use Item.id"); + return this._id; + } +}); +Zotero.defineProperty(Zotero.Item.prototype, 'libraryID', { + get: function() this._libraryID, + set: function(val) this.setField('libraryID', val) +}); +Zotero.defineProperty(Zotero.Item.prototype, 'key', { + get: function() this._key, + set: function(val) this.setField('key', val) +}); +Zotero.defineProperty(Zotero.Item.prototype, 'itemTypeID', { + get: function() this._itemTypeID +}); +Zotero.defineProperty(Zotero.Item.prototype, 'dateAdded', { + get: function() this._dateAdded +}); +Zotero.defineProperty(Zotero.Item.prototype, 'dateModified', { + get: function() this._dateModified, + set: function(val) this.setField('dateModified', val) +}); +Zotero.defineProperty(Zotero.Item.prototype, 'version', { + get: function() this._itemVersion, + set: function(val) this.setField('version', val) +}); +Zotero.defineProperty(Zotero.Item.prototype, 'synced', { + get: function() this._synced, + set: function(val) this.setField('synced', val) }); -Zotero.Item.prototype.__defineSetter__('id', function (val) { this.setField('id', val); }); -Zotero.Item.prototype.__defineGetter__('libraryID', function () this._libraryID ); -Zotero.Item.prototype.__defineSetter__('libraryID', function (val) { this.setField('libraryID', val); }); -Zotero.Item.prototype.__defineGetter__('key', function () this._key ); -Zotero.Item.prototype.__defineSetter__('key', function (val) { this.setField('key', val) }); -Zotero.Item.prototype.__defineGetter__('itemTypeID', function () this._itemTypeID); -Zotero.Item.prototype.__defineGetter__('dateAdded', function () this._dateAdded ); -Zotero.Item.prototype.__defineGetter__('dateModified', function () this._dateModified ); -Zotero.Item.prototype.__defineGetter__('version', function () this._itemVersion ); -Zotero.Item.prototype.__defineSetter__('version', function (val) { return this.setField('itemVersion', val); }); -Zotero.Item.prototype.__defineGetter__('synced', function () this._synced ); -Zotero.Item.prototype.__defineSetter__('synced', function (val) { return this.setField('synced', val); }); // .parentKey and .parentID defined in dataObject.js, but create aliases -Zotero.Item.prototype.__defineGetter__('parentItemKey', function () this._parentKey ); -Zotero.Item.prototype.__defineSetter__('parentItemKey', function (val) this._setParentKey(val) ); -Zotero.Item.prototype.__defineGetter__('parentItemID', function () this._getParentID() ); -Zotero.Item.prototype.__defineSetter__('parentItemID', function (val) this._setParentID(val) ); - -Zotero.Item.prototype.__defineGetter__('firstCreator', function () this._firstCreator ); -Zotero.Item.prototype.__defineGetter__('sortCreator', function () this._sortCreator ); +Zotero.defineProperty(Zotero.Item.prototype, 'parentItemID', { + get: function() this.parentID, + set: function(val) this.parentID = val +}); +Zotero.defineProperty(Zotero.Item.prototype, 'parentItemKey', { + get: function() this.parentKey, + set: function(val) this.parentKey = val +}); -Zotero.Item.prototype.__defineGetter__('relatedItems', function () { return this._getRelatedItems(true); }); -Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); }); +Zotero.defineProperty(Zotero.Item.prototype, 'firstCreator', { + get: function() this._firstCreator +}); +Zotero.defineProperty(Zotero.Item.prototype, 'sortCreator', { + get: function() this._sortCreator +}); +Zotero.defineProperty(Zotero.Item.prototype, 'relatedItems', { + get: function() this._getRelatedItems(true), + set: function(arr) this._setRelatedItems(arr) +}); Zotero.Item.prototype.getID = function() { Zotero.debug('Item.getID() is deprecated -- use Item.id'); @@ -148,7 +177,7 @@ Zotero.Item.prototype.getType = function() { Zotero.Item.prototype.isPrimaryField = function (fieldName) { Zotero.debug("Zotero.Item.isPrimaryField() is deprecated -- use Zotero.Items.isPrimaryField()"); - return Zotero.Items.isPrimaryField(fieldName); + return this.ObjectsClass.isPrimaryField(fieldName); } Zotero.Item.prototype._get = function (fieldName) { @@ -171,13 +200,13 @@ Zotero.Item.prototype._setParentKey = function() { /* * Retrieves (and loads from DB, if necessary) an itemData field value * - * Field can be passed as fieldID or fieldName - * - * If |unformatted| is true, skip any special processing of DB value - * (e.g. multipart date field) (default false) - * - * If |includeBaseMapped| is true and field is a base field, returns value of - * type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording') + * @param {String|Integer} field fieldID or fieldName + * @param {Boolean} [unformatted] Skip any special processing of DB value + * (e.g. multipart date field) + * @param {Boolean} includeBaseMapped If true and field is a base field, returns + * value of type-specific field instead + * (e.g. 'label' for 'publisher' in 'audioRecording') + * @return {String} Value as string or empty string if value is not present */ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) { // We don't allow access after saving to force use of the centrally cached @@ -203,10 +232,12 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) } else if (creators.length > 3) { return creatorsData[0].lastName + " " + Zotero.getString('general.etAl'); } - } else if (field === 'id' || Zotero.Items.isPrimaryField(field)) { + } else if (field === 'id' || this.ObjectsClass.isPrimaryField(field)) { var privField = '_' + field; //Zotero.debug('Returning ' + (this[privField] ? this[privField] : '') + ' (typeof ' + typeof this[privField] + ')'); return this[privField]; + } else if (field == 'year') { + return this.getField('date', true, true).substr(0,4); } if (this.isNote()) { @@ -263,72 +294,17 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) * @param {Boolean} asNames * @return {Integer{}|String[]} */ -Zotero.Item.prototype.getUsedFields = Zotero.Promise.coroutine(function* (asNames) { +Zotero.Item.prototype.getUsedFields = function(asNames) { this._requireData('itemData'); return Object.keys(this._itemData) - .filter(id => this._itemData[id] !== false) + .filter(id => this._itemData[id] !== false && this._itemData[id] !== null) .map(id => asNames ? Zotero.ItemFields.getName(id) : parseInt(id)); -}); +}; /* - * Build object from database - */ -Zotero.Item.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) { - if (this._loaded.primaryData && !reload) return; - - var id = this._id; - var key = this._key; - var libraryID = this._libraryID; - - if (!id && !key) { - throw new Error('ID or key not set in Zotero.Item.loadPrimaryData()'); - } - - var columns = [], join = [], where = []; - var primaryFields = Zotero.Items.primaryFields; - for (let i=0; i<primaryFields.length; i++) { - let field = primaryFields[i]; - // If field not already set - if (field == 'itemID' || this['_' + field] === null || reload) { - columns.push(Zotero.Items.getPrimaryDataSQLPart(field)); - } - } - if (!columns.length) { - return; - } - // This should match Zotero.Items.getPrimaryDataSQL(), but without - // necessarily including all columns - var sql = "SELECT " + columns.join(", ") + Zotero.Items.primaryDataSQLFrom; - if (id) { - sql += " AND O.itemID=? "; - var params = id; - } - else { - sql += " AND O.key=? AND O.libraryID=? "; - var params = [key, libraryID]; - } - sql += (where.length ? ' AND ' + where.join(' AND ') : ''); - var row = yield Zotero.DB.rowQueryAsync(sql, params); - - if (!row) { - if (failOnMissing) { - throw new Error("Item " + (id ? id : libraryID + "/" + key) - + " not found in Zotero.Item.loadPrimaryData()"); - } - this._loaded.primaryData = true; - this._clearChanged('primaryData'); - return; - } - - this.loadFromRow(row, reload); - return; -}); - - -/* * Populate basic item data from a database row */ Zotero.Item.prototype.loadFromRow = function(row, reload) { @@ -337,8 +313,13 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { this.setType(row.itemTypeID, true); } + this._parseRowData(row); + this._finalizeLoadFromRow(row); +} + +Zotero.Item.prototype._parseRowData = function(row) { if (false) { - var primaryFields = Zotero.Items.primaryFields; + var primaryFields = this.ObjectsClass.primaryFields; for (let i=0; i<primaryFields.length; i++) { if (primaryFields[i] === undefined) { Zotero.debug('Skipping missing field ' + primaryFields[i]); @@ -417,7 +398,7 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { } } else { - var primaryFields = Zotero.Items.primaryFields; + var primaryFields = this.ObjectsClass.primaryFields; for (let i=0; i<primaryFields.length; i++) { let col = primaryFields[i]; @@ -470,6 +451,9 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { } } } +} + +Zotero.Item.prototype._finalizeLoadFromRow = function(row) { this._loaded.primaryData = true; this._clearChanged('primaryData'); this._identified = true; @@ -477,15 +461,6 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { /* - * Check if any data fields have changed since last save - */ -Zotero.Item.prototype.hasChanged = function() { - Zotero.debug(this._changed); - return !!Object.keys(this._changed).filter((dataType) => this._changed[dataType]).length -} - - -/* * Set or change the item's type */ Zotero.Item.prototype.setType = function(itemTypeID, loadIn) { @@ -735,7 +710,7 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { } // Primary field - if (Zotero.Items.isPrimaryField(field)) { + if (this.ObjectsClass.isPrimaryField(field)) { this._requireData('primaryData'); if (loadIn) { @@ -745,9 +720,22 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { switch (field) { case 'itemTypeID': case 'dateAdded': + break; + case 'dateModified': + // Make sure it's valid + let date = Zotero.Date.sqlToDate(value, true); + if (!date) throw new Error("Invalid SQL date: " + value); + + value = Zotero.Date.dateToSQL(date); + break; + case 'version': + value = parseInt(value); + break; + case 'synced': + value = !!value; break; default: @@ -772,15 +760,6 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { this.setType(value, loadIn); } else { - switch (field) { - case 'version': - value = parseInt(value); - break; - - case 'synced': - value = !!value; - break; - } this['_' + field] = value; @@ -1059,28 +1038,27 @@ Zotero.Item.prototype.removeCreator = function(orderIndex, allowMissing) { return true; } - -Zotero.Item.prototype.__defineGetter__('deleted', function () { - if (!this.id) { - return false; - } - if (this._deleted !== null) { - return this._deleted; - } - this._requireData('primaryData'); -}); - - -Zotero.Item.prototype.__defineSetter__('deleted', function (val) { - var deleted = !!val; - - if (this._deleted == deleted) { - Zotero.debug("Deleted state hasn't changed for item " + this.id); - return; +Zotero.defineProperty(Zotero.Item.prototype, 'deleted', { + get: function() { + if (!this.id) { + return false; + } + if (this._deleted !== null) { + return this._deleted; + } + this._requireData('primaryData'); + }, + set: function(val) { + var deleted = !!val; + + if (this._deleted == deleted) { + Zotero.debug("Deleted state hasn't changed for item " + this.id); + return; + } + this._markFieldChange('deleted', !!this._deleted); + this._changed.deleted = true; + this._deleted = deleted; } - this._markFieldChange('deleted', !!this._deleted); - this._changed.deleted = true; - this._deleted = deleted; }); @@ -1103,7 +1081,7 @@ Zotero.Item.prototype.addRelatedItem = Zotero.Promise.coroutine(function* (itemI return false; } - var item = yield Zotero.Items.getAsync(itemID); + var item = yield this.ObjectsClass.getAsync(itemID); if (!item) { throw ("Can't relate item to invalid item " + itemID + " in Zotero.Item.addRelatedItem()"); @@ -1147,609 +1125,576 @@ Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(function* (it }); -/** - * Save changes to database - * - * @return {Promise<Integer|Boolean>} Promise for itemID of new item, - * TRUE on item update, or FALSE if item was unchanged - */ -Zotero.Item.prototype.save = Zotero.Promise.coroutine(function* (options) { - try { - if (!options) { - options = {}; - } - - var isNew = !this.id; - - Zotero.Items.editCheck(this); - - if (!this.hasChanged()) { - Zotero.debug('Item ' + this.id + ' has not changed', 4); - return false; - } +Zotero.Item.prototype.isEditable = function() { + var editable = Zotero.Item._super.prototype.isEditable.apply(this); + if (!editable) return false; + + // Check if we're allowed to save attachments + if (this.isAttachment() + && (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL || + this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) + && !Zotero.Libraries.isFilesEditable(this.libraryID) + ) { + return false; + } + + return true; +} + +Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + var options = env.options; + + var itemTypeID = this.itemTypeID; - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var itemID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Items.registerIdentifiers(itemID, libraryID, key); + var sqlColumns = []; + var sqlValues = []; + var reloadParentChildItems = {}; + + // + // Primary fields + // + // If available id value, use it -- otherwise we'll use autoincrement + var itemID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('items'); + Zotero.debug('='); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + sqlColumns.push( + 'itemTypeID', + 'dateAdded', + 'libraryID', + 'key', + 'version', + 'synced' + ); + + sqlValues.push( + { int: itemTypeID }, + this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ); + + if (this._changed.primaryData && this._changed.primaryData._dateModified) { + sqlColumns.push('dateModified', 'clientDateModified'); + sqlValues.push(this.dateModified, Zotero.DB.transactionDateTime); + } + else if (isNew) { + sqlColumns.push('dateModified', 'clientDateModified'); + sqlValues.push(Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime); + } + else { + for each (let field in ['dateModified', 'clientDateModified']) { + switch (field) { + case 'dateModified': + case 'clientDateModified': + let skipFlag = "skip" + field[0].toUpperCase() + field.substr(1) + "Update"; + if (!options[skipFlag]) { + sqlColumns.push(field); + sqlValues.push(Zotero.DB.transactionDateTime); } - }; + break; + } } - else { - var transactionOptions = null; + } + + if (isNew) { + sqlColumns.unshift('itemID'); + sqlValues.unshift(parseInt(itemID)); + + var sql = "INSERT INTO items (" + sqlColumns.join(", ") + ") " + + "VALUES (" + sqlValues.map(function () "?").join() + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!itemID) { + itemID = env.id = insertID; } - var itemTypeID = this.itemTypeID; + Zotero.Notifier.trigger('add', 'item', itemID); + } + else { + var sql = "UPDATE items SET " + sqlColumns.join("=?, ") + "=? WHERE itemID=?"; + sqlValues.push(parseInt(itemID)); + yield Zotero.DB.queryAsync(sql, sqlValues); - return Zotero.DB.executeTransaction(function* () { - if (isNew) { - Zotero.debug('Saving data for new item to database'); - } - else { - Zotero.debug('Updating database with new item data', 4); - } - - var sqlColumns = []; - var sqlValues = []; - var reloadParentChildItems = {}; - - // - // Primary fields - // - // If available id value, use it -- otherwise we'll use autoincrement - itemID = this._id = this.id ? this.id : yield Zotero.ID.get('items'); - Zotero.debug('='); - libraryID = this.libraryID; - key = this._key = this.key ? this.key : this._generateKey(); - - sqlColumns.push( - 'itemTypeID', - 'dateAdded', - 'libraryID', - 'key', - 'version', - 'synced' - ); + var notifierData = {}; + notifierData[itemID] = { changed: this._previousData }; + Zotero.Notifier.trigger('modify', 'item', itemID, notifierData); + } + + // + // ItemData + // + if (this._changed.itemData) { + let del = []; + + let valueSQL = "SELECT valueID FROM itemDataValues WHERE value=?"; + let insertValueSQL = "INSERT INTO itemDataValues VALUES (?,?)"; + let replaceSQL = "REPLACE INTO itemData VALUES (?,?,?)"; + + for (let fieldID in this._changed.itemData) { + fieldID = parseInt(fieldID); + let value = this.getField(fieldID, true); - sqlValues.push( - { int: itemTypeID }, - this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime, - this.libraryID ? this.libraryID : 0, - key, - this.version ? this.version : 0, - this.synced ? 1 : 0 - ); + // If field changed and is empty, mark row for deletion + if (!value) { + del.push(fieldID); + continue; + } - if (isNew) { - sqlColumns.push('dateModified', 'clientDateModified'); - sqlValues.push(Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime); + if (Zotero.ItemFields.getID('accessDate') == fieldID + && (this.getField(fieldID)) == 'CURRENT_TIMESTAMP') { + value = Zotero.DB.transactionDateTime; } - else { - for each (let field in ['dateModified', 'clientDateModified']) { - switch (field) { - case 'dateModified': - case 'clientDateModified': - let skipFlag = "skip" + field[0].toUpperCase() + field.substr(1) + "Update"; - if (!options[skipFlag]) { - sqlColumns.push(field); - sqlValues.push(Zotero.DB.transactionDateTime); - } - break; - } - } + + let valueID = yield Zotero.DB.valueQueryAsync(valueSQL, [value], { debug: true }) + if (!valueID) { + valueID = yield Zotero.ID.get('itemDataValues'); + yield Zotero.DB.queryAsync(insertValueSQL, [valueID, value], { debug: false }); } + yield Zotero.DB.queryAsync(replaceSQL, [itemID, fieldID, valueID], { debug: false }); + } + + // Delete blank fields + if (del.length) { + sql = 'DELETE from itemData WHERE itemID=? AND ' + + 'fieldID IN (' + del.map(function () '?').join() + ')'; + yield Zotero.DB.queryAsync(sql, [itemID].concat(del)); + } + } + + // + // Creators + // + if (this._changed.creators) { + for (let orderIndex in this._changed.creators) { + orderIndex = parseInt(orderIndex); + if (isNew) { - sqlColumns.unshift('itemID'); - sqlValues.unshift(parseInt(itemID)); - - var sql = "INSERT INTO items (" + sqlColumns.join(", ") + ") " - + "VALUES (" + sqlValues.map(function () "?").join() + ")"; - var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); - if (!itemID) { - itemID = insertID; - } - - Zotero.Notifier.trigger('add', 'item', itemID); + Zotero.debug('Adding creator in position ' + orderIndex, 4); } else { - var sql = "UPDATE items SET " + sqlColumns.join("=?, ") + "=? WHERE itemID=?"; - sqlValues.push(parseInt(itemID)); - yield Zotero.DB.queryAsync(sql, sqlValues); - - var notifierData = {}; - notifierData[itemID] = { changed: this._previousData }; - Zotero.Notifier.trigger('modify', 'item', itemID, notifierData); + Zotero.debug('Creator ' + orderIndex + ' has changed', 4); } - // - // ItemData - // - if (this._changed.itemData) { - let del = []; - - let valueSQL = "SELECT valueID FROM itemDataValues WHERE value=?"; - let insertValueSQL = "INSERT INTO itemDataValues VALUES (?,?)"; - let replaceSQL = "REPLACE INTO itemData VALUES (?,?,?)"; - - for (let fieldID in this._changed.itemData) { - fieldID = parseInt(fieldID); - let value = this.getField(fieldID, true); - - // If field changed and is empty, mark row for deletion - if (!value) { - del.push(fieldID); - continue; - } - - if (Zotero.ItemFields.getID('accessDate') == fieldID - && (this.getField(fieldID)) == 'CURRENT_TIMESTAMP') { - value = Zotero.DB.transactionDateTime; - } - - let valueID = yield Zotero.DB.valueQueryAsync(valueSQL, [value], { debug: true }) - if (!valueID) { - valueID = yield Zotero.ID.get('itemDataValues'); - yield Zotero.DB.queryAsync(insertValueSQL, [valueID, value], { debug: false }); - } - - yield Zotero.DB.queryAsync(replaceSQL, [itemID, fieldID, valueID], { debug: false }); - } - - // Delete blank fields - if (del.length) { - sql = 'DELETE from itemData WHERE itemID=? AND ' - + 'fieldID IN (' + del.map(function () '?').join() + ')'; - yield Zotero.DB.queryAsync(sql, [itemID].concat(del)); - } + let creatorData = this.getCreator(orderIndex); + // If no creator in this position, just remove the item-creator association + if (!creatorData) { + let sql = "DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?"; + yield Zotero.DB.queryAsync(sql, [itemID, orderIndex]); + Zotero.Prefs.set('purge.creators', true); + continue; } - // - // Creators - // - if (this._changed.creators) { - for (let orderIndex in this._changed.creators) { - orderIndex = parseInt(orderIndex); - - if (isNew) { - Zotero.debug('Adding creator in position ' + orderIndex, 4); - } - else { - Zotero.debug('Creator ' + orderIndex + ' has changed', 4); - } - - let creatorData = this.getCreator(orderIndex); - // If no creator in this position, just remove the item-creator association - if (!creatorData) { - let sql = "DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?"; - yield Zotero.DB.queryAsync(sql, [itemID, orderIndex]); - Zotero.Prefs.set('purge.creators', true); - continue; - } - - let previousCreatorID = this._previousData.creators[orderIndex] - ? this._previousData.creators[orderIndex].id - : false; - let newCreatorID = yield Zotero.Creators.getIDFromData(creatorData, true); - - // If there was previously a creator at this position and it's different from - // the new one, the old one might need to be purged. - if (previousCreatorID && previousCreatorID != newCreatorID) { - Zotero.Prefs.set('purge.creators', true); - } - - let sql = "INSERT OR REPLACE INTO itemCreators " - + "(itemID, creatorID, creatorTypeID, orderIndex) VALUES (?, ?, ?, ?)"; - yield Zotero.DB.queryAsync( - sql, - [ - itemID, - newCreatorID, - creatorData.creatorTypeID, - orderIndex - ] - ); - } - } + let previousCreatorID = !isNew && this._previousData.creators[orderIndex] + ? this._previousData.creators[orderIndex].id + : false; + let newCreatorID = yield Zotero.Creators.getIDFromData(creatorData, true); - // Parent item - let parentItem = this.parentKey; - parentItem = parentItem ? Zotero.Items.getByLibraryAndKey(this.libraryID, parentItem) : null; - if (this._changed.parentKey) { - if (isNew) { - if (!parentItem) { - // TODO: clear caches? - let msg = this._parentKey + " is not a valid item key"; - throw new Zotero.Error(msg, "MISSING_OBJECT"); - } - - let newParentItemNotifierData = {}; - //newParentItemNotifierData[newParentItem.id] = {}; - Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); - - switch (Zotero.ItemTypes.getName(itemTypeID)) { - case 'note': - case 'attachment': - reloadParentChildItems[parentItem.id] = true; - break; - } - } - else { - let type = Zotero.ItemTypes.getName(itemTypeID); - let Type = type[0].toUpperCase() + type.substr(1); - - if (this._parentKey) { - if (!parentItem) { - // TODO: clear caches - let msg = "Cannot set source to invalid item " + this._parentKey; - throw new Zotero.Error(msg, "MISSING_OBJECT"); - } - - let newParentItemNotifierData = {}; - //newParentItemNotifierData[newParentItem.id] = {}; - Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); - } - - var oldParentKey = this._previousData.parentKey; - if (oldParentKey) { - var oldParentItem = Zotero.Items.getByLibraryAndKey(this.libraryID, oldParentKey); - if (oldParentItem) { - let oldParentItemNotifierData = {}; - //oldParentItemNotifierData[oldParentItem.id] = {}; - Zotero.Notifier.trigger('modify', 'item', oldParentItem.id, oldParentItemNotifierData); - } - else { - Zotero.debug("Old source item " + oldParentKey - + " didn't exist in Zotero.Item.save()", 2); - } - } - - // If this was an independent item, remove from any collections - // where it existed previously and add parent instead - if (!oldParentKey) { - let sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; - let changedCollections = yield Zotero.DB.columnQueryAsync(sql, this.id); - if (changedCollections) { - for (let i=0; i<changedCollections.length; i++) { - yield parentItem.loadCollections(); - parentItem.addToCollection(changedCollections[i]); - yield this.loadCollections(); - this.removeFromCollection(changedCollections[i]); - - Zotero.Notifier.trigger( - 'remove', - 'collection-item', - changedCollections[i] + '-' + this.id - ); - } - parentItem.save({ - skipDateModifiedUpdate: true - }); - } - } - - // Update DB, if not a note or attachment we're changing below - if (!this._changed.attachmentData && - (!this._changed.note || !this.isNote())) { - var sql = "UPDATE item" + Type + "s SET parentItemID=? " - + "WHERE itemID=?"; - var bindParams = [parentItem ? parentItem.id : null, this.id]; - yield Zotero.DB.queryAsync(sql, bindParams); - } - - // Update the counts of the previous and new sources - if (oldParentItem) { - reloadParentChildItems[oldParentItem.id] = true; - } - if (parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } + // If there was previously a creator at this position and it's different from + // the new one, the old one might need to be purged. + if (previousCreatorID && previousCreatorID != newCreatorID) { + Zotero.Prefs.set('purge.creators', true); } - // Trashed status - if (this._changed.deleted) { - if (this.deleted) { - sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; - } - else { - // If undeleting, remove any merge-tracking relations - var relations = yield Zotero.Relations.getByURIs( - Zotero.URI.getItemURI(this), - Zotero.Relations.deletedItemPredicate, - false - ); - for each(let relation in relations) { - relation.erase(); - } - - sql = "DELETE FROM deletedItems WHERE itemID=?"; - } - yield Zotero.DB.queryAsync(sql, itemID); - - // Refresh trash - Zotero.Notifier.trigger('refresh', 'trash', this.libraryID); - if (this._deleted) { - Zotero.Notifier.trigger('trash', 'item', this.id); - } - - if (parentItem) { - reloadParentChildItems[parentItem.id] = true; - } + let sql = "INSERT OR REPLACE INTO itemCreators " + + "(itemID, creatorID, creatorTypeID, orderIndex) VALUES (?, ?, ?, ?)"; + yield Zotero.DB.queryAsync( + sql, + [ + itemID, + newCreatorID, + creatorData.creatorTypeID, + orderIndex + ] + ); + } + } + + // Parent item + let parentItem = this.parentKey; + parentItem = parentItem ? this.ObjectsClass.getByLibraryAndKey(this.libraryID, parentItem) : null; + if (this._changed.parentKey) { + if (isNew) { + if (!parentItem) { + // TODO: clear caches? + let msg = this._parentKey + " is not a valid item key"; + throw new Zotero.Error(msg, "MISSING_OBJECT"); } - // Note - if ((isNew && this.isNote()) || this._changed.note) { - if (!isNew) { - if (this._noteText === null || this._noteTitle === null) { - throw new Error("Cached note values not set with " - + "this._changed.note set to true"); - } - } - - let parent = this.isNote() ? this.parentID : null; - let noteText = this._noteText ? this._noteText : ''; - // Add <div> wrapper if not present - if (!noteText.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) { - // Keep consistent with getNote() - noteText = '<div class="zotero-note znv1">' + noteText + '</div>'; - } - - let params = [ - parent ? parent : null, - noteText, - this._noteTitle ? this._noteTitle : '' - ]; - let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; - if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { - sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; - params.push(itemID); - } - else { - sql = "INSERT INTO itemNotes " - + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; - params.unshift(itemID); - } - yield Zotero.DB.queryAsync(sql, params); - - if (parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); - // - // Attachment - // - if (!isNew) { - // If attachment title changes, update parent attachments - if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { + switch (Zotero.ItemTypes.getName(itemTypeID)) { + case 'note': + case 'attachment': reloadParentChildItems[parentItem.id] = true; - } + break; } + } + else { + let type = Zotero.ItemTypes.getName(itemTypeID); + let Type = type[0].toUpperCase() + type.substr(1); - if (this.isAttachment() || this._changed.attachmentData) { - let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " - + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; - let parent = this.parentID; - let linkMode = this.attachmentLinkMode; - let contentType = this.attachmentContentType; - let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); - let path = this.attachmentPath; - let syncState = this.attachmentSyncState; - - if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { - // Save attachment within attachment base directory as relative path - if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { - path = Zotero.Attachments.getBaseDirectoryRelativePath(path); - } - // If possible, convert relative path to absolute - else { - let file = Zotero.Attachments.resolveRelativePath(path); - if (file) { - path = file.persistentDescriptor; - } - } + if (this._parentKey) { + if (!parentItem) { + // TODO: clear caches + let msg = "Cannot set source to invalid item " + this._parentKey; + throw new Zotero.Error(msg, "MISSING_OBJECT"); } - let params = [ - itemID, - parent ? parent : null, - { int: linkMode }, - contentType ? { string: contentType } : null, - charsetID ? { int: charsetID } : null, - path ? { string: path } : null, - syncState ? { int: syncState } : 0 - ]; - yield Zotero.DB.queryAsync(sql, params); - - // Clear cached child attachments of the parent - if (!isNew && parentItem) { - reloadParentChildItems[parentItem.id] = true; - } + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); } - // Tags - if (this._changed.tags) { - let oldTags = this._previousData.tags; - let newTags = this._tags; - - // Convert to individual JSON objects, diff, and convert back - let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); - let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); - let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) - .map(function (x) JSON.parse(x)); - let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) - .map(function (x) JSON.parse(x));; - - for (let i=0; i<toAdd.length; i++) { - let tag = toAdd[i]; - let tagID = yield Zotero.Tags.getIDFromName(this.libraryID, tag.tag, true); - // "OR REPLACE" allows changing type - let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)"; - yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); - Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tag.tag); + var oldParentKey = this._previousData.parentKey; + if (oldParentKey) { + var oldParentItem = this.ObjectsClass.getByLibraryAndKey(this.libraryID, oldParentKey); + if (oldParentItem) { + let oldParentItemNotifierData = {}; + //oldParentItemNotifierData[oldParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', oldParentItem.id, oldParentItemNotifierData); } - - if (toRemove.length) { - yield Zotero.Tags.load(this.libraryID); - for (let i=0; i<toRemove.length; i++) { - let tag = toRemove[i]; - let tagID = Zotero.Tags.getID(this.libraryID, tag.tag); - let sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=? AND type=?"; - yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); - Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + tag.tag); - } - Zotero.Prefs.set('purge.tags', true); + else { + Zotero.debug("Old source item " + oldParentKey + + " didn't exist in Zotero.Item.save()", 2); } } - // Collections - if (this._changed.collections) { - let oldCollections = this._previousData.collections; - let newCollections = this._collections; - - let toAdd = Zotero.Utilities.arrayDiff(newCollections, oldCollections); - let toRemove = Zotero.Utilities.arrayDiff(oldCollections, newCollections); - - for (let i=0; i<toAdd.length; i++) { - let collectionID = toAdd[i]; - - let sql = "SELECT IFNULL(MAX(orderIndex)+1, 0) FROM collectionItems " - + "WHERE collectionID=?"; - let orderIndex = yield Zotero.DB.valueQueryAsync(sql, collectionID); - - sql = "INSERT OR IGNORE INTO collectionItems " - + "(collectionID, itemID, orderIndex) VALUES (?, ?, ?)"; - yield Zotero.DB.queryAsync(sql, [collectionID, this.id, orderIndex]); - - Zotero.Collections.refreshChildItems(collectionID); - Zotero.Notifier.trigger('add', 'collection-item', collectionID + '-' + this.id); - } - - if (toRemove.length) { - let sql = "DELETE FROM collectionItems WHERE itemID=? AND collectionID IN (" - + toRemove.join(',') - + ")"; - yield Zotero.DB.queryAsync(sql, this.id); - - for (let i=0; i<toRemove.length; i++) { - let collectionID = toRemove[i]; - Zotero.Collections.refreshChildItems(collectionID); - Zotero.Notifier.trigger('remove', 'collection-item', collectionID + '-' + this.id); + // If this was an independent item, remove from any collections + // where it existed previously and add parent instead + if (!oldParentKey) { + let sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; + let changedCollections = yield Zotero.DB.columnQueryAsync(sql, this.id); + if (changedCollections) { + for (let i=0; i<changedCollections.length; i++) { + yield parentItem.loadCollections(); + parentItem.addToCollection(changedCollections[i]); + yield this.loadCollections(); + this.removeFromCollection(changedCollections[i]); + + Zotero.Notifier.trigger( + 'remove', + 'collection-item', + changedCollections[i] + '-' + this.id + ); } + parentItem.save({ + skipDateModifiedUpdate: true + }); } } - // Related items - if (this._changed.relatedItems) { - var removed = []; - var newids = []; - var currentIDs = this._getRelatedItems(true); - - for each(var id in currentIDs) { - newids.push(id); - } - - if (newids.length) { - var sql = "REPLACE INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; - var replaceStatement = Zotero.DB.getAsyncStatement(sql); - - for each(var linkedItemID in newids) { - replaceStatement.bindInt32Parameter(0, itemID); - replaceStatement.bindInt32Parameter(1, linkedItemID); - - yield Zotero.DB.executeAsyncStatement(replaceStatement); - } - } - - Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + // Update DB, if not a note or attachment we're changing below + if (!this._changed.attachmentData && + (!this._changed.note || !this.isNote())) { + var sql = "UPDATE item" + Type + "s SET parentItemID=? " + + "WHERE itemID=?"; + var bindParams = [parentItem ? parentItem.id : null, this.id]; + yield Zotero.DB.queryAsync(sql, bindParams); } - // Related items - if (this._changed.relatedItems) { - var removed = []; - var newids = []; - var currentIDs = this._getRelatedItems(true); - - for each(var id in this._previousData.related) { - if (currentIDs.indexOf(id) == -1) { - removed.push(id); - } - } - for each(var id in currentIDs) { - if (this._previousData.related.indexOf(id) != -1) { - continue; - } - newids.push(id); - } - - if (removed.length) { - var sql = "DELETE FROM itemSeeAlso WHERE itemID=? " - + "AND linkedItemID IN (" - + removed.map(function () '?').join() - + ")"; - yield Zotero.DB.queryAsync(sql, [this.id].concat(removed)); - } - - if (newids.length) { - var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; - var insertStatement = Zotero.DB.getAsyncStatement(sql); - - for each(var linkedItemID in newids) { - insertStatement.bindInt32Parameter(0, this.id); - insertStatement.bindInt32Parameter(1, linkedItemID); - - yield Zotero.DB.executeAsyncStatement(insertStatement); - } - } - - Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + // Update the counts of the previous and new sources + if (oldParentItem) { + reloadParentChildItems[oldParentItem.id] = true; + } + if (parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + } + + // Trashed status + if (this._changed.deleted) { + if (this.deleted) { + sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; + } + else { + // If undeleting, remove any merge-tracking relations + var relations = yield Zotero.Relations.getByURIs( + Zotero.URI.getItemURI(this), + Zotero.Relations.deletedItemPredicate, + false + ); + for each(let relation in relations) { + relation.erase(); } - // Update child item counts and contents - if (reloadParentChildItems) { - for (let parentItemID in reloadParentChildItems) { - let parentItem = yield Zotero.Items.getAsync(parentItemID); - yield parentItem.reload(['primaryData', 'childItems'], true); - parentItem.clearBestAttachmentState(); + sql = "DELETE FROM deletedItems WHERE itemID=?"; + } + yield Zotero.DB.queryAsync(sql, itemID); + + // Refresh trash + Zotero.Notifier.trigger('refresh', 'trash', this.libraryID); + if (this._deleted) { + Zotero.Notifier.trigger('trash', 'item', this.id); + } + + if (parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // Note + if ((isNew && this.isNote()) || this._changed.note) { + if (!isNew) { + if (this._noteText === null || this._noteTitle === null) { + throw new Error("Cached note values not set with " + + "this._changed.note set to true"); + } + } + + let parent = this.isNote() ? this.parentID : null; + let noteText = this._noteText ? this._noteText : ''; + // Add <div> wrapper if not present + if (!noteText.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) { + // Keep consistent with getNote() + noteText = '<div class="zotero-note znv1">' + noteText + '</div>'; + } + + let params = [ + parent ? parent : null, + noteText, + this._noteTitle ? this._noteTitle : '' + ]; + let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; + if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { + sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; + params.push(itemID); + } + else { + sql = "INSERT INTO itemNotes " + + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; + params.unshift(itemID); + } + yield Zotero.DB.queryAsync(sql, params); + + if (parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // + // Attachment + // + if (!isNew) { + // If attachment title changes, update parent attachments + if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + if (this.isAttachment() || this._changed.attachmentData) { + let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " + + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; + let parent = this.parentID; + let linkMode = this.attachmentLinkMode; + let contentType = this.attachmentContentType; + let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); + let path = this.attachmentPath; + let syncState = this.attachmentSyncState; + + if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + // Save attachment within attachment base directory as relative path + if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { + path = Zotero.Attachments.getBaseDirectoryRelativePath(path); + } + // If possible, convert relative path to absolute + else { + let file = Zotero.Attachments.resolveRelativePath(path); + if (file) { + path = file.persistentDescriptor; } } - - // New items have to be reloaded via Zotero.Items.get(), so mark them as disabled - if (isNew) { - var id = this.id; - this._disabled = true; - return id; + } + + let params = [ + itemID, + parent ? parent : null, + { int: linkMode }, + contentType ? { string: contentType } : null, + charsetID ? { int: charsetID } : null, + path ? { string: path } : null, + syncState ? { int: syncState } : 0 + ]; + yield Zotero.DB.queryAsync(sql, params); + + // Clear cached child attachments of the parent + if (!isNew && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // Tags + if (this._changed.tags) { + let oldTags = this._previousData.tags; + let newTags = this._tags; + + // Convert to individual JSON objects, diff, and convert back + let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); + let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); + let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) + .map(function (x) JSON.parse(x)); + let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) + .map(function (x) JSON.parse(x));; + + for (let i=0; i<toAdd.length; i++) { + let tag = toAdd[i]; + let tagID = yield Zotero.Tags.getIDFromName(this.libraryID, tag.tag, true); + // "OR REPLACE" allows changing type + let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)"; + yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); + Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tag.tag); + } + + if (toRemove.length) { + yield Zotero.Tags.load(this.libraryID); + for (let i=0; i<toRemove.length; i++) { + let tag = toRemove[i]; + let tagID = Zotero.Tags.getID(this.libraryID, tag.tag); + let sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=? AND type=?"; + yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); + Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + tag.tag); } + Zotero.Prefs.set('purge.tags', true); + } + } + + // Collections + if (this._changed.collections) { + let oldCollections = this._previousData.collections || []; + let newCollections = this._collections; + + let toAdd = Zotero.Utilities.arrayDiff(newCollections, oldCollections); + let toRemove = Zotero.Utilities.arrayDiff(oldCollections, newCollections); + + for (let i=0; i<toAdd.length; i++) { + let collectionID = toAdd[i]; - // Always reload primary data. DataObject.reload() only reloads changed data types, so - // it won't reload, say, dateModified and firstCreator if only creator data was changed - // and not primaryData. - yield this.loadPrimaryData(true); - yield this.reload(); - this._clearChanged(); + let sql = "SELECT IFNULL(MAX(orderIndex)+1, 0) FROM collectionItems " + + "WHERE collectionID=?"; + let orderIndex = yield Zotero.DB.valueQueryAsync(sql, collectionID); - return true; - }.bind(this), transactionOptions); + sql = "INSERT OR IGNORE INTO collectionItems " + + "(collectionID, itemID, orderIndex) VALUES (?, ?, ?)"; + yield Zotero.DB.queryAsync(sql, [collectionID, this.id, orderIndex]); + + yield this.ContainerObjectsClass.refreshChildItems(collectionID); + Zotero.Notifier.trigger('add', 'collection-item', collectionID + '-' + this.id); + } + + if (toRemove.length) { + let sql = "DELETE FROM collectionItems WHERE itemID=? AND collectionID IN (" + + toRemove.join(',') + + ")"; + yield Zotero.DB.queryAsync(sql, this.id); + + for (let i=0; i<toRemove.length; i++) { + let collectionID = toRemove[i]; + yield this.ContainerObjectsClass.refreshChildItems(collectionID); + Zotero.Notifier.trigger('remove', 'collection-item', collectionID + '-' + this.id); + } + } } - catch (e) { - try { - yield this.loadPrimaryData(true); - yield this.reload(); - this._clearChanged(); + + // Related items + if (this._changed.relatedItems) { + var removed = []; + var newids = []; + var currentIDs = this._getRelatedItems(true); + + for each(var id in currentIDs) { + newids.push(id); } - catch (e2) { - Zotero.debug(e2, 1); + + if (newids.length) { + var sql = "REPLACE INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; + var replaceStatement = Zotero.DB.getAsyncStatement(sql); + + for each(var linkedItemID in newids) { + replaceStatement.bindInt32Parameter(0, itemID); + replaceStatement.bindInt32Parameter(1, linkedItemID); + + yield Zotero.DB.executeAsyncStatement(replaceStatement); + } } - Zotero.debug(e, 1); - throw e; + Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + } + + // Related items + if (this._changed.relatedItems) { + var removed = []; + var newids = []; + var currentIDs = this._getRelatedItems(true); + + for each(var id in this._previousData.related) { + if (currentIDs.indexOf(id) == -1) { + removed.push(id); + } + } + for each(var id in currentIDs) { + if (this._previousData.related.indexOf(id) != -1) { + continue; + } + newids.push(id); + } + + if (removed.length) { + var sql = "DELETE FROM itemSeeAlso WHERE itemID=? " + + "AND linkedItemID IN (" + + removed.map(function () '?').join() + + ")"; + yield Zotero.DB.queryAsync(sql, [this.id].concat(removed)); + } + + if (newids.length) { + var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; + var insertStatement = Zotero.DB.getAsyncStatement(sql); + + for each(var linkedItemID in newids) { + insertStatement.bindInt32Parameter(0, this.id); + insertStatement.bindInt32Parameter(1, linkedItemID); + + yield Zotero.DB.executeAsyncStatement(insertStatement); + } + } + + Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + } + + // Update child item counts and contents + if (reloadParentChildItems) { + for (let parentItemID in reloadParentChildItems) { + let parentItem = yield this.ObjectsClass.getAsync(parentItemID); + yield parentItem.reload(['primaryData', 'childItems'], true); + parentItem.clearBestAttachmentState(); + } } }); +Zotero.Item.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + // New items have to be reloaded via Zotero.Items.get(), so mark them as disabled + if (env.isNew) { + var id = this.id; + this._disabled = true; + return id; + } + + // Always reload primary data. DataObject.reload() only reloads changed data types, so + // it won't reload, say, dateModified and firstCreator if only creator data was changed + // and not primaryData. + yield this.loadPrimaryData(true); + yield this.reload(); + this._clearChanged(); + + return true; +}); /** * Used by sync code @@ -2002,9 +1947,9 @@ Zotero.Item.prototype.getNotes = function(includeTrashed) { // Sort by title if necessary if (!sortChronologically) { var collation = Zotero.getLocaleCollation(); - rows.sort(function (a, b) { - var aTitle = Zotero.Items.getSortTitle(a.title); - var bTitle = Zotero.Items.getSortTitle(b.title); + rows.sort((a, b) => { + var aTitle = this.ObjectsClass.getSortTitle(a.title); + var bTitle = this.ObjectsClass.getSortTitle(b.title); return collation.compareString(1, aTitle, bTitle); }); } @@ -2483,7 +2428,7 @@ Zotero.Item.prototype._updateAttachmentStates = function (exists) { } try { - var item = Zotero.Items.getByLibraryAndKey(this.libraryID, parentKey); + var item = this.ObjectsClass.getByLibraryAndKey(this.libraryID, parentKey); } catch (e) { if (e instanceof Zotero.Exception.UnloadedDataException) { @@ -2688,39 +2633,39 @@ Zotero.Item.prototype.getAttachmentLinkMode = function() { * Possible values specified as constants in Zotero.Attachments * (e.g. Zotero.Attachments.LINK_MODE_LINKED_FILE) */ -Zotero.Item.prototype.__defineGetter__('attachmentLinkMode', function () { - if (!this.isAttachment()) { - return undefined; - } - return this._attachmentLinkMode; -}); - - -Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) { - if (!this.isAttachment()) { - throw (".attachmentLinkMode can only be set for attachment items"); - } - - switch (val) { - case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: - case Zotero.Attachments.LINK_MODE_IMPORTED_URL: - case Zotero.Attachments.LINK_MODE_LINKED_FILE: - case Zotero.Attachments.LINK_MODE_LINKED_URL: - break; - - default: - throw ("Invalid attachment link mode '" + val - + "' in Zotero.Item.attachmentLinkMode setter"); - } - - if (val === this.attachmentLinkMode) { - return; - } - if (!this._changed.attachmentData) { - this._changed.attachmentData = {}; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentLinkMode', { + get: function() { + if (!this.isAttachment()) { + return undefined; + } + return this._attachmentLinkMode; + }, + set: function(val) { + if (!this.isAttachment()) { + throw (".attachmentLinkMode can only be set for attachment items"); + } + + switch (val) { + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_LINKED_FILE: + case Zotero.Attachments.LINK_MODE_LINKED_URL: + break; + + default: + throw ("Invalid attachment link mode '" + val + + "' in Zotero.Item.attachmentLinkMode setter"); + } + + if (val === this.attachmentLinkMode) { + return; + } + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.linkMode = true; + this._attachmentLinkMode = val; } - this._changed.attachmentData.linkMode = true; - this._attachmentLinkMode = val; }); @@ -2729,40 +2674,42 @@ Zotero.Item.prototype.getAttachmentMIMEType = function() { return this.attachmentContentType; }; -Zotero.Item.prototype.__defineGetter__('attachmentMIMEType', function () { - Zotero.debug(".attachmentMIMEType deprecated -- use .attachmentContentType"); - return this.attachmentContentType; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentMIMEType', { + get: function() { + Zotero.debug(".attachmentMIMEType deprecated -- use .attachmentContentType"); + return this.attachmentContentType; + } }); /** * Content type of an attachment (e.g. 'text/plain') */ -Zotero.Item.prototype.__defineGetter__('attachmentContentType', function () { - if (!this.isAttachment()) { - return undefined; - } - return this._attachmentContentType; -}); - - -Zotero.Item.prototype.__defineSetter__('attachmentContentType', function (val) { - if (!this.isAttachment()) { - throw (".attachmentContentType can only be set for attachment items"); - } - - if (!val) { - val = ''; - } - - if (val == this.attachmentContentType) { - return; - } - - if (!this._changed.attachmentData) { - this._changed.attachmentData = {}; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentContentType', { + get: function() { + if (!this.isAttachment()) { + return undefined; + } + return this._attachmentContentType; + }, + set: function(val) { + if (!this.isAttachment()) { + throw (".attachmentContentType can only be set for attachment items"); + } + + if (!val) { + val = ''; + } + + if (val == this.attachmentContentType) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.contentType = true; + this._attachmentContentType = val; } - this._changed.attachmentData.contentType = true; - this._attachmentContentType = val; }); @@ -2775,76 +2722,75 @@ Zotero.Item.prototype.getAttachmentCharset = function() { /** * Character set of an attachment */ -Zotero.Item.prototype.__defineGetter__('attachmentCharset', function () { - if (!this.isAttachment()) { - return undefined; - } - return this._attachmentCharset -}); - - -Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) { - if (!this.isAttachment()) { - throw (".attachmentCharset can only be set for attachment items"); - } - - var oldVal = this.attachmentCharset; - if (oldVal) { - oldVal = Zotero.CharacterSets.getID(oldVal); - } - if (!oldVal) { - oldVal = null; - } - - if (val) { - val = Zotero.CharacterSets.getID(val); - } - if (!val) { - val = null; - } - - if (val == oldVal) { - return; - } - - if (!this._changed.attachmentData) { - this._changed.attachmentData= {}; - } - this._changed.attachmentData.charset = true; - this._attachmentCharset = val; -}); - - -Zotero.Item.prototype.__defineGetter__('attachmentPath', function () { - if (!this.isAttachment()) { - return undefined; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentCharset', { + get: function() { + if (!this.isAttachment()) { + return undefined; + } + return this._attachmentCharset + }, + set: function(val) { + if (!this.isAttachment()) { + throw (".attachmentCharset can only be set for attachment items"); + } + + var oldVal = this.attachmentCharset; + if (oldVal) { + oldVal = Zotero.CharacterSets.getID(oldVal); + } + if (!oldVal) { + oldVal = null; + } + + if (val) { + val = Zotero.CharacterSets.getID(val); + } + if (!val) { + val = null; + } + + if (val == oldVal) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData= {}; + } + this._changed.attachmentData.charset = true; + this._attachmentCharset = val; } - return this._attachmentPath; }); - -Zotero.Item.prototype.__defineSetter__('attachmentPath', function (val) { - if (!this.isAttachment()) { - throw (".attachmentPath can only be set for attachment items"); - } - - if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { - throw ('attachmentPath cannot be set for link attachments'); - } - - if (!val) { - val = ''; - } - - if (val == this.attachmentPath) { - return; - } - - if (!this._changed.attachmentData) { - this._changed.attachmentData = {}; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentPath', { + get: function() { + if (!this.isAttachment()) { + return undefined; + } + return this._attachmentPath; + }, + set: function(val) { + if (!this.isAttachment()) { + throw (".attachmentPath can only be set for attachment items"); + } + + if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { + throw ('attachmentPath cannot be set for link attachments'); + } + + if (!val) { + val = ''; + } + + if (val == this.attachmentPath) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.path = true; + this._attachmentPath = val; } - this._changed.attachmentData.path = true; - this._attachmentPath = val; }); @@ -2865,51 +2811,51 @@ Zotero.Item.prototype.updateAttachmentPath = function () { }; -Zotero.Item.prototype.__defineGetter__('attachmentSyncState', function () { - if (!this.isAttachment()) { - return undefined; - } - return this._attachmentSyncState; -}); - - -Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) { - if (!this.isAttachment()) { - throw ("attachmentSyncState can only be set for attachment items"); - } - - switch (this.attachmentLinkMode) { - case Zotero.Attachments.LINK_MODE_IMPORTED_URL: - case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: - break; - - default: - throw ("attachmentSyncState can only be set for snapshots and " - + "imported files"); - } - - switch (val) { - case Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD: - case Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD: - case Zotero.Sync.Storage.SYNC_STATE_IN_SYNC: - case Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD: - case Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD: - break; - - default: - throw ("Invalid sync state '" + val - + "' in Zotero.Item.attachmentSyncState setter"); - } - - if (val == this.attachmentSyncState) { - return; - } - - if (!this._changed.attachmentData) { - this._changed.attachmentData = {}; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', { + get: function() { + if (!this.isAttachment()) { + return undefined; + } + return this._attachmentSyncState; + }, + set: function(val) { + if (!this.isAttachment()) { + throw ("attachmentSyncState can only be set for attachment items"); + } + + switch (this.attachmentLinkMode) { + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + break; + + default: + throw ("attachmentSyncState can only be set for snapshots and " + + "imported files"); + } + + switch (val) { + case Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD: + case Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD: + case Zotero.Sync.Storage.SYNC_STATE_IN_SYNC: + case Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD: + case Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD: + break; + + default: + throw ("Invalid sync state '" + val + + "' in Zotero.Item.attachmentSyncState setter"); + } + + if (val == this.attachmentSyncState) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.syncState = true; + this._attachmentSyncState = val; } - this._changed.attachmentData.syncState = true; - this._attachmentSyncState = val; }); @@ -2922,29 +2868,31 @@ Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) { * @return {Promise<Number|undefined>} File modification time as timestamp in milliseconds, * or undefined if no file */ -Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', Zotero.Promise.coroutine(function* () { - if (!this.isAttachment()) { - return undefined; - } - - if (!this.id) { - return undefined; - } - - var path = yield this.getFilePathAsync(); - if (!path) { - return undefined; - } - - var fmtime = OS.File.stat(path).lastModificationDate; - - if (fmtime < 1) { - Zotero.debug("File mod time " + fmtime + " is less than 1 -- interpreting as 1", 2); - fmtime = 1; - } - - return fmtime; -})); +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentModificationTime', { + get: Zotero.Promise.coroutine(function* () { + if (!this.isAttachment()) { + return undefined; + } + + if (!this.id) { + return undefined; + } + + var path = yield this.getFilePathAsync(); + if (!path) { + return undefined; + } + + var fmtime = OS.File.stat(path).lastModificationDate; + + if (fmtime < 1) { + Zotero.debug("File mod time " + fmtime + " is less than 1 -- interpreting as 1", 2); + fmtime = 1; + } + + return fmtime; + }) +}); /** @@ -2955,21 +2903,23 @@ Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', Zotero.Prom * * @return {String} MD5 hash of file as hex string */ -Zotero.Item.prototype.__defineGetter__('attachmentHash', function () { - if (!this.isAttachment()) { - return undefined; - } - - if (!this.id) { - return undefined; - } - - var file = this.getFile(); - if (!file) { - return undefined; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentHash', { + get: function () { + if (!this.isAttachment()) { + return undefined; + } + + if (!this.id) { + return undefined; + } + + var file = this.getFile(); + if (!file) { + return undefined; + } + + return Zotero.Utilities.Internal.md5(file) || undefined; } - - return Zotero.Utilities.Internal.md5(file) || undefined; }); @@ -2982,84 +2932,86 @@ Zotero.Item.prototype.__defineGetter__('attachmentHash', function () { * * @return {Promise<String>} - A promise for attachment text or empty string if unavailable */ -Zotero.Item.prototype.__defineGetter__('attachmentText', Zotero.Promise.coroutine(function* () { - if (!this.isAttachment()) { - return undefined; - } - - if (!this.id) { - return null; - } - - var file = this.getFile(); - - if (!(yield OS.File.exists(file.path))) { - file = false; - } - - var cacheFile = Zotero.Fulltext.getItemCacheFile(this); - if (!file) { - if (cacheFile.exists()) { - var str = yield Zotero.File.getContentsAsync(cacheFile); - - return str.trim(); +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentText', { + get: Zotero.Promise.coroutine(function* () { + if (!this.isAttachment()) { + return undefined; } - return ''; - } - - var contentType = this.attachmentContentType; - if (!contentType) { - contentType = yield Zotero.MIME.getMIMETypeFromFile(file); - if (contentType) { - this.attachmentContentType = contentType; - yield this.save(); + + if (!this.id) { + return null; + } + + var file = this.getFile(); + + if (!(yield OS.File.exists(file.path))) { + file = false; } - } - - var str; - if (Zotero.Fulltext.isCachedMIMEType(contentType)) { - var reindex = false; - if (!cacheFile.exists()) { - Zotero.debug("Regenerating item " + this.id + " full-text cache file"); - reindex = true; + var cacheFile = Zotero.Fulltext.getItemCacheFile(this); + if (!file) { + if (cacheFile.exists()) { + var str = yield Zotero.File.getContentsAsync(cacheFile); + + return str.trim(); + } + return ''; } - // Fully index item if it's not yet - else if (!(yield Zotero.Fulltext.isFullyIndexed(this))) { - Zotero.debug("Item " + this.id + " is not fully indexed -- caching now"); - reindex = true; + + var contentType = this.attachmentContentType; + if (!contentType) { + contentType = yield Zotero.MIME.getMIMETypeFromFile(file); + if (contentType) { + this.attachmentContentType = contentType; + yield this.save(); + } } - if (reindex) { - if (!Zotero.Fulltext.pdfConverterIsRegistered()) { - Zotero.debug("PDF converter is unavailable -- returning empty .attachmentText", 3); + var str; + if (Zotero.Fulltext.isCachedMIMEType(contentType)) { + var reindex = false; + + if (!cacheFile.exists()) { + Zotero.debug("Regenerating item " + this.id + " full-text cache file"); + reindex = true; + } + // Fully index item if it's not yet + else if (!(yield Zotero.Fulltext.isFullyIndexed(this))) { + Zotero.debug("Item " + this.id + " is not fully indexed -- caching now"); + reindex = true; + } + + if (reindex) { + if (!Zotero.Fulltext.pdfConverterIsRegistered()) { + Zotero.debug("PDF converter is unavailable -- returning empty .attachmentText", 3); + return ''; + } + yield Zotero.Fulltext.indexItems(this.id, false); + } + + if (!cacheFile.exists()) { + Zotero.debug("Cache file doesn't exist after indexing -- returning empty .attachmentText"); return ''; } - yield Zotero.Fulltext.indexItems(this.id, false); + str = yield Zotero.File.getContentsAsync(cacheFile); + } + + else if (contentType == 'text/html') { + str = yield Zotero.File.getContentsAsync(file); + str = Zotero.Utilities.unescapeHTML(str); } - if (!cacheFile.exists()) { - Zotero.debug("Cache file doesn't exist after indexing -- returning empty .attachmentText"); + else if (contentType == 'text/plain') { + str = yield Zotero.File.getContentsAsync(file); + } + + else { return ''; } - str = yield Zotero.File.getContentsAsync(cacheFile); - } - - else if (contentType == 'text/html') { - str = yield Zotero.File.getContentsAsync(file); - str = Zotero.Utilities.unescapeHTML(str); - } - - else if (contentType == 'text/plain') { - str = yield Zotero.File.getContentsAsync(file); - } - - else { - return ''; - } - - return str.trim(); -})); + + return str.trim(); + }) +}); @@ -3136,7 +3088,7 @@ Zotero.Item.prototype.getBestAttachments = Zotero.Promise.coroutine(function* () + "AND IA.itemID NOT IN (SELECT itemID FROM deletedItems) " + "ORDER BY contentType='application/pdf' DESC, value=? DESC, dateAdded ASC"; var itemIDs = yield Zotero.DB.columnQueryAsync(sql, [this.id, Zotero.Attachments.LINK_MODE_LINKED_URL, url]); - return Zotero.Items.get(itemIDs); + return this.ObjectsClass.get(itemIDs); }); @@ -3392,7 +3344,7 @@ Zotero.Item.prototype.setCollections = function (collectionIDsOrKeys) { var collectionIDs = collectionIDsOrKeys.map(function (val) { return parseInt(val) == val ? parseInt(val) - : Zotero.Collections.getIDFromLibraryAndKey(this.libraryID, val); + : this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, val); }.bind(this)); collectionIDs = Zotero.Utilities.arrayUnique(collectionIDs); @@ -3417,7 +3369,7 @@ Zotero.Item.prototype.setCollections = function (collectionIDsOrKeys) { Zotero.Item.prototype.addToCollection = function (collectionIDOrKey) { var collectionID = parseInt(collectionIDOrKey) == collectionIDOrKey ? parseInt(collectionIDOrKey) - : Zotero.Collections.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey) + : this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey) if (!collectionID) { throw new Error("Invalid collection '" + collectionIDOrKey + "'"); @@ -3442,7 +3394,7 @@ Zotero.Item.prototype.addToCollection = function (collectionIDOrKey) { Zotero.Item.prototype.removeFromCollection = function (collectionIDOrKey) { var collectionID = parseInt(collectionIDOrKey) == collectionIDOrKey ? parseInt(collectionIDOrKey) - : Zotero.Collections.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey) + : this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey) if (!collectionID) { throw new Error("Invalid collection '" + collectionIDOrKey + "'"); @@ -3591,7 +3543,7 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) { var thisData = this.serialize(); var otherData = item.serialize(); - var numDiffs = Zotero.Items.diff(thisData, otherData, diff, includeMatches); + var numDiffs = this.ObjectsClass.diff(thisData, otherData, diff, includeMatches); diff[0].creators = []; diff[1].creators = []; @@ -3727,7 +3679,7 @@ Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems let otherItem = otherItems[i]; let diff = []; let otherData = yield otherItem.toJSON(); - let numDiffs = Zotero.Items.diff(thisData, otherData, diff); + let numDiffs = this.ObjectsClass.diff(thisData, otherData, diff); if (numDiffs) { for (let field in diff[1]) { @@ -3778,6 +3730,7 @@ Zotero.Item.prototype.clone = function(libraryID, skipTags) { var sameLibrary = libraryID == this.libraryID; var newItem = new Zotero.Item; + newItem.libraryID = libraryID; newItem.setType(this.itemTypeID); var fieldIDs = this.getUsedFields(); @@ -3838,99 +3791,89 @@ Zotero.Item.prototype.copy = Zotero.Promise.coroutine(function* () { });; -/** - * Delete item from database and clear from Zotero.Items internal array - * - * Items.erase() should be used for multiple items - */ -Zotero.Item.prototype.erase = Zotero.Promise.coroutine(function* () { - if (!this.id) { - return false; - } - - Zotero.debug('Deleting item ' + this.id); +Zotero.Item.prototype._eraseInit = Zotero.Promise.coroutine(function* (env) { + var proceed = yield Zotero.Item._super.prototype._eraseInit.apply(this, arguments); + if (!proceed) return false; - var changedItems = []; - var changedItemsNotifierData = {}; - var deletedItemNotifierData = {}; + env.deletedItemNotifierData = {}; + env.deletedItemNotifierData[this.id] = { old: this.toJSON() }; - yield Zotero.DB.executeTransaction(function* () { - deletedItemNotifierData[this.id] = { old: this.toJSON() }; - - // Remove item from parent collections - var parentCollectionIDs = this.collections; - if (parentCollectionIDs) { - for (var i=0; i<parentCollectionIDs.length; i++) { - let parentCollection = yield Zotero.Collections.getAsync(parentCollectionIDs[i]); - yield parentCollection.removeItem(this.id); - } + return true; +}); + +Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) { + // Remove item from parent collections + var parentCollectionIDs = this.collections; + if (parentCollectionIDs) { + for (var i=0; i<parentCollectionIDs.length; i++) { + let parentCollection = yield Zotero.Collections.getAsync(parentCollectionIDs[i]); + yield parentCollection.removeItem(this.id); } - - var parentItem = this.parentKey; - parentItem = parentItem ? Zotero.Items.getByLibraryAndKey(this.libraryID, parentItem) : null; - - // // Delete associated attachment files - if (this.isAttachment()) { - let linkMode = this.getAttachmentLinkMode(); - // If link only, nothing to delete - if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { - try { - let file = Zotero.Attachments.getStorageDirectory(this); - yield OS.File.removeDir(file.path, { - ignoreAbsent: true, - ignorePermissions: true - }); - } - catch (e) { - Zotero.debug(e, 2); - Components.utils.reportError(e); - } + } + + var parentItem = this.parentKey; + parentItem = parentItem ? this.ObjectsClass.getByLibraryAndKey(this.libraryID, parentItem) : null; + + // // Delete associated attachment files + if (this.isAttachment()) { + let linkMode = this.getAttachmentLinkMode(); + // If link only, nothing to delete + if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { + try { + let file = Zotero.Attachments.getStorageDirectory(this); + yield OS.File.removeDir(file.path, { + ignoreAbsent: true, + ignorePermissions: true + }); } - } - // Regular item - else { - let sql = "SELECT itemID FROM itemNotes WHERE parentItemID=?1 UNION " - + "SELECT itemID FROM itemAttachments WHERE parentItemID=?1"; - let toDelete = yield Zotero.DB.columnQueryAsync(sql, [this.id]); - for (let i=0; i<toDelete.length; i++) { - let obj = yield Zotero.Items.getAsync(toDelete[i]); - yield obj.erase(); + catch (e) { + Zotero.debug(e, 2); + Components.utils.reportError(e); } } - - // Flag related items for notification - // TEMP: Do something with relations - /*var relateds = this._getRelatedItems(true); - for each(var id in relateds) { - let relatedItem = Zotero.Items.get(id); - }*/ - - // Clear fulltext cache - if (this.isAttachment()) { - yield Zotero.Fulltext.clearItemWords(this.id); - //Zotero.Fulltext.clearItemContent(this.id); - } - - // Remove relations (except for merge tracker) - var uri = Zotero.URI.getItemURI(this); - yield Zotero.Relations.eraseByURI(uri, [Zotero.Relations.deletedItemPredicate]); - - yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id); - - if (parentItem) { - yield parentItem.reload(['primaryData', 'childItems'], true); - parentItem.clearBestAttachmentState(); + } + // Regular item + else { + let sql = "SELECT itemID FROM itemNotes WHERE parentItemID=?1 UNION " + + "SELECT itemID FROM itemAttachments WHERE parentItemID=?1"; + let toDelete = yield Zotero.DB.columnQueryAsync(sql, [this.id]); + for (let i=0; i<toDelete.length; i++) { + let obj = yield this.ObjectsClass.getAsync(toDelete[i]); + yield obj.erase(); } - }.bind(this)); + } + + // Flag related items for notification + // TEMP: Do something with relations + /*var relateds = this._getRelatedItems(true); + for each(var id in relateds) { + let relatedItem = Zotero.Items.get(id); + }*/ + + // Clear fulltext cache + if (this.isAttachment()) { + yield Zotero.Fulltext.clearItemWords(this.id); + //Zotero.Fulltext.clearItemContent(this.id); + } + + // Remove relations (except for merge tracker) + var uri = Zotero.URI.getItemURI(this); + yield Zotero.Relations.eraseByURI(uri, [Zotero.Relations.deletedItemPredicate]); - Zotero.Items.unload(this.id); + env.parentItem = parentItem; +}); + +Zotero.Item.prototype._erasePreCommit = Zotero.Promise.coroutine(function* (env) { + yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id); - // Send notification of changed items - if (changedItems.length) { - Zotero.Notifier.trigger('modify', 'item', changedItems, changedItemsNotifierData); + if (env.parentItem) { + yield env.parentItem.reload(['primaryData', 'childItems'], true); + env.parentItem.clearBestAttachmentState(); } - Zotero.Notifier.trigger('delete', 'item', this.id, deletedItemNotifierData); + this.ObjectsClass.unload(this.id); + + Zotero.Notifier.trigger('delete', 'item', this.id, env.deletedItemNotifierData); Zotero.Prefs.set('purge.items', true); Zotero.Prefs.set('purge.creators', true); @@ -3962,7 +3905,7 @@ Zotero.Item.prototype.fromJSON = function (json) { case 'dateAdded': case 'dateModified': - item[field] = val; + this['_'+field] = val; break; case 'tags': @@ -4011,8 +3954,9 @@ Zotero.Item.prototype.fromJSON = function (json) { if (!changedFields[field] && // Invalid fields will already have been cleared by the type change Zotero.ItemFields.isValidForType( - Zotero.ItemFields.getID(field), data.itemTypeID - )) { + Zotero.ItemFields.getID(field), this.itemTypeID + ) + ) { this.setField(field, false); } } @@ -4021,18 +3965,17 @@ Zotero.Item.prototype.fromJSON = function (json) { this.deleted = !!json.deleted; // Creators - var numCreators = 0; + let pos = 0; if (json.creators) { - for each (let creator in json.creators) { - this.setCreator(pos, creator); - numCreators++; + while (pos<json.creators.length) { + this.setCreator(pos, json.creators[pos]); + pos++; } } // Remove item's remaining creators not in JSON - var rem = this.numCreators() - numCreators; - for (let j = 0; j < rem; j++) { + while (pos < this.numCreators()) { // Keep removing last creator - this.removeCreator(numCreators); + this.removeCreator(this.numCreators() - 1); } // Both notes and attachments might have parents and notes @@ -4125,7 +4068,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc // Collections yield this.loadCollections(); obj.collections = this.getCollections().map(function (id) { - return Zotero.Collections.getLibraryAndKeyFromID(id)[1]; + return this.ContainerObjectsClass.getLibraryAndKeyFromID(id)[1]; }); // Relations @@ -4136,9 +4079,9 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc obj.relations[rel.predicate] = rel.object; } var relatedItems = this._getRelatedItems().map(function (key) { - return Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key); + return this.ObjectsClass.getIDFromLibraryAndKey(this.libraryID, key); }.bind(this)).filter(function (val) val !== false); - relatedItems = Zotero.Items.get(relatedItems); + relatedItems = this.ObjectsClass.get(relatedItems); var pred = Zotero.Relations.relatedItemPredicate; for (let i=0; i<relatedItems.length; i++) { let item = relatedItems[i]; @@ -4269,21 +4212,22 @@ Zotero.Item.prototype.loadItemData = Zotero.Promise.coroutine(function* (reload) this._loaded.itemData = true; this._clearChanged('itemData'); - this.loadDisplayTitle(reload); + yield this.loadDisplayTitle(reload); }); Zotero.Item.prototype.loadNote = Zotero.Promise.coroutine(function* (reload) { - Zotero.debug("Loading note data for item " + this.libraryKey); - if (this._loaded.note && !reload) { return; } if (!this.isNote() && !this.isAttachment()) { - throw new Error("Can only load note for note or attachment item"); + Zotero.debug("Can only load note for note or attachment item"); + return; } + Zotero.debug("Loading note data for item " + this.libraryKey); + var sql = "SELECT note FROM itemNotes WHERE itemID=?"; var row = yield Zotero.DB.rowQueryAsync(sql, this.id); if (row) { @@ -4474,6 +4418,12 @@ Zotero.Item.prototype.loadChildItems = Zotero.Promise.coroutine(function* (reloa return; } + + if (this.isNote() || this.isAttachment()) { + Zotero.debug("Can only load child items for regular item"); + return; + } + // Attachments this._attachments = { rows: null, @@ -4676,7 +4626,7 @@ Zotero.Item.prototype._setRelatedItems = Zotero.Promise.coroutine(function* (ite continue; } - var item = yield Zotero.Items.getAsync(id); + var item = yield this.ObjectsClass.getAsync(id); if (!item) { throw ("Can't relate item to invalid item " + id + " in Zotero.Item._setRelatedItems()"); @@ -4707,7 +4657,7 @@ Zotero.Item.prototype._setRelatedItems = Zotero.Promise.coroutine(function* (ite newIDs = oldIDs.concat(newIDs); this._relatedItems = []; for each(var itemID in newIDs) { - this._relatedItems.push(yield Zotero.Items.getAsync(itemID)); + this._relatedItems.push(yield this.ObjectsClass.getAsync(itemID)); } return true; }); @@ -4734,7 +4684,7 @@ Zotero.Item.prototype._getOldCreators = function () { Zotero.Item.prototype._disabledCheck = function () { if (this._disabled) { var msg = "New Zotero.Item objects shouldn't be accessed after save -- use Zotero.Items.get()"; - Zotero.debug(msg, 2); + Zotero.debug(msg, 2, true); Components.utils.reportError(msg); } } diff --git a/chrome/content/zotero/xpcom/data/itemFields.js b/chrome/content/zotero/xpcom/data/itemFields.js @@ -103,7 +103,7 @@ Zotero.ItemFields = new function() { } if (typeof field == 'number') { - return field; + return _fields[field] ? field : false; } return _fields[field] ? _fields[field]['id'] : false; diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -27,17 +27,16 @@ /* * Primary interface for accessing Zotero items */ -Zotero.Items = new function() { - Zotero.DataObjects.apply(this, ['item']); - this.constructor.prototype = new Zotero.DataObjects(); +Zotero.Items = function() { + this.constructor = null; - // Privileged methods - this.add = add; - this.getSortTitle = getSortTitle; + this._ZDO_object = 'item'; - Object.defineProperty(this, "_primaryDataSQLParts", { + // This needs to wait until all Zotero components are loaded to initialize, + // but otherwise it can be just a simple property + Zotero.defineProperty(this, "_primaryDataSQLParts", { get: function () { - return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = { + return { itemID: "O.itemID", itemTypeID: "O.itemTypeID", dateAdded: "O.dateAdded", @@ -88,18 +87,17 @@ Zotero.Items = new function() { attachmentContentType: "IA.contentType AS attachmentContentType", attachmentPath: "IA.path AS attachmentPath", attachmentSyncState: "IA.syncState AS attachmentSyncState" - }); + }; } - }); + }, {lazy: true}); - // Private members - var _primaryDataSQLParts; - var _cachedFields = {}; - var _firstCreatorSQL = ''; - var _sortCreatorSQL = ''; - var _emptyTrashIdleObserver = null; - var _emptyTrashTimer = null; + this._primaryDataSQLFrom = "FROM items O " + + "LEFT JOIN itemAttachments IA USING (itemID) " + + "LEFT JOIN items IAP ON (IA.parentItemID=IAP.itemID) " + + "LEFT JOIN itemNotes INo ON (O.itemID=INo.itemID) " + + "LEFT JOIN items INoP ON (INo.parentItemID=INoP.itemID) " + + "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID)"; /** * Return items marked as deleted @@ -215,77 +213,7 @@ Zotero.Items = new function() { }; - /* - * Create a new item with optional metadata and pass back the primary reference - * - * Using "var item = new Zotero.Item()" and "item.save()" directly results - * in an orphaned reference to the created item. If other code retrieves the - * new item with Zotero.Items.get() and modifies it, the original reference - * will not reflect the changes. - * - * Using this method avoids the need to call Zotero.Items.get() after save() - * in order to get the primary item reference. Since it accepts metadata - * as a JavaScript object, it also offers a simpler syntax than - * item.setField() and item.setCreator(). - * - * Callers with no need for an up-to-date reference after save() (or who - * don't mind doing an extra Zotero.Items.get()) can use Zotero.Item - * directly if they prefer. - * - * Sample usage: - * - * var data = { - * title: "Shakespeare: The Invention of the Human", - * publisher: "Riverhead Hardcover", - * date: '1998-10-26', - * ISBN: 1573221201, - * pages: 745, - * creators: [ - * ['Harold', 'Bloom', 'author'] - * ] - * }; - * var item = Zotero.Items.add('book', data); - */ - function add(itemTypeOrID, data) { - var item = new Zotero.Item(itemTypeOrID); - for (var field in data) { - if (field == 'creators') { - var i = 0; - for each(var creator in data.creators) { - // TODO: accept format from toArray() - - var fields = { - firstName: creator[0], - lastName: creator[1], - fieldMode: creator[3] ? creator[3] : 0 - }; - - var creatorDataID = Zotero.Creators.getDataID(fields); - if (creatorDataID) { - var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID); - // TODO: identical creators? - var creatorID = linkedCreators[0]; - } - else { - var creatorObj = new Zotero.Creator; - creatorObj.setFields(fields); - var creatorID = creatorObj.save(); - } - - item.setCreator(i, Zotero.Creators.get(creatorID), creator[2]); - i++; - } - } - else { - item.setField(field, data[field]); - } - } - var id = item.save(); - - return this.getAsync(id); - } - - + this._cachedFields = {}; this.cacheFields = Zotero.Promise.coroutine(function* (libraryID, fields, items) { if (items && items.length == 0) { return; @@ -315,14 +243,14 @@ Zotero.Items = new function() { var fieldIDs = []; for each(var field in fields) { // Check if field already cached - if (_cachedFields[libraryID] && _cachedFields[libraryID].indexOf(field) != -1) { + if (this._cachedFields[libraryID] && this._cachedFields[libraryID].indexOf(field) != -1) { continue; } - if (!_cachedFields[libraryID]) { - _cachedFields[libraryID] = []; + if (!this._cachedFields[libraryID]) { + this._cachedFields[libraryID] = []; } - _cachedFields[libraryID].push(field); + this._cachedFields[libraryID].push(field); if (this.isPrimaryField(field)) { primaryFields.push(field); @@ -472,7 +400,7 @@ Zotero.Items = new function() { for (let i=0; i<allItemIDs.length; i++) { let itemID = allItemIDs[i]; let item = this._objectCache[itemID]; - yield this._objectCache[itemID].loadDisplayTitle() + yield item.loadDisplayTitle() } } @@ -497,7 +425,7 @@ Zotero.Items = new function() { // Move child items to master var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true)); for each(var id in ids) { - var attachment = yield Zotero.Items.getAsync(id); + var attachment = yield this.getAsync(id); // TODO: Skip identical children? @@ -549,7 +477,7 @@ Zotero.Items = new function() { } yield item.save(); - }); + }.bind(this)); }; @@ -604,9 +532,11 @@ Zotero.Items = new function() { /** * Start idle observer to delete trashed items older than a certain number of days */ + this._emptyTrashIdleObserver = null; + this._emptyTrashTimer = null; this.startEmptyTrashTimer = function () { - _emptyTrashIdleObserver = { - observe: function (subject, topic, data) { + this._emptyTrashIdleObserver = { + observe: (subject, topic, data) => { if (topic == 'idle' || topic == 'timer-callback') { var days = Zotero.Prefs.get('trashAutoEmptyDays'); if (!days) { @@ -620,20 +550,20 @@ Zotero.Items = new function() { // TODO: increase number after dealing with slow // tag.getLinkedItems() call during deletes var num = 10; - Zotero.Items.emptyTrash(null, days, num) - .then(function (deleted) { + this.emptyTrash(null, days, num) + .then(deleted => { if (!deleted) { - _emptyTrashTimer = null; + this._emptyTrashTimer = null; return; } // Set a timer to do more every few seconds - if (!_emptyTrashTimer) { - _emptyTrashTimer = Components.classes["@mozilla.org/timer;1"] + if (!this._emptyTrashTimer) { + this._emptyTrashTimer = Components.classes["@mozilla.org/timer;1"] .createInstance(Components.interfaces.nsITimer); } - _emptyTrashTimer.init( - _emptyTrashIdleObserver.observe, + this._emptyTrashTimer.init( + this._emptyTrashIdleObserver.observe, 5 * 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT ); @@ -641,8 +571,8 @@ Zotero.Items = new function() { } // When no longer idle, cancel timer else if (topic == 'back') { - if (_emptyTrashTimer) { - _emptyTrashTimer.cancel(); + if (this._emptyTrashTimer) { + this._emptyTrashTimer.cancel(); } } } @@ -650,7 +580,7 @@ Zotero.Items = new function() { var idleService = Components.classes["@mozilla.org/widget/idleservice;1"]. getService(Components.interfaces.nsIIdleService); - idleService.addIdleObserver(_emptyTrashIdleObserver, 305); + idleService.addIdleObserver(this._emptyTrashIdleObserver, 305); } @@ -693,28 +623,12 @@ Zotero.Items = new function() { }); - this.getPrimaryDataSQL = function () { - return "SELECT " - + Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ') - + this.primaryDataSQLFrom; - }; - - - this.primaryDataSQLFrom = " FROM items O " - + "LEFT JOIN itemAttachments IA USING (itemID) " - + "LEFT JOIN items IAP ON (IA.parentItemID=IAP.itemID) " - + "LEFT JOIN itemNotes INo ON (O.itemID=INo.itemID) " - + "LEFT JOIN items INoP ON (INo.parentItemID=INoP.itemID) " - + "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID) " - + "WHERE 1"; - - this._postLoad = function (libraryID, ids) { if (!ids) { - if (!_cachedFields[libraryID]) { - _cachedFields[libraryID] = []; + if (!this._cachedFields[libraryID]) { + this._cachedFields[libraryID] = []; } - _cachedFields[libraryID] = this.primaryFields.concat(); + this._cachedFields[libraryID] = this.primaryFields.concat(); } } @@ -724,6 +638,7 @@ Zotero.Items = new function() { * * Why do we do this entirely in SQL? Because we're crazy. Crazy like foxes. */ + var _firstCreatorSQL = ''; function _getFirstCreatorSQL() { if (_firstCreatorSQL) { return _firstCreatorSQL; @@ -828,6 +743,7 @@ Zotero.Items = new function() { /* * Generate SQL to retrieve sortCreator field */ + var _sortCreatorSQL = ''; function _getSortCreatorSQL() { if (_sortCreatorSQL) { return _sortCreatorSQL; @@ -947,7 +863,7 @@ Zotero.Items = new function() { } - function getSortTitle(title) { + this.getSortTitle = function(title) { if (title === false || title === undefined) { return ''; } @@ -956,4 +872,8 @@ Zotero.Items = new function() { } return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1') } -} + + Zotero.DataObjects.call(this); + + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); diff --git a/chrome/content/zotero/xpcom/data/libraries.js b/chrome/content/zotero/xpcom/data/libraries.js @@ -28,7 +28,7 @@ Zotero.Libraries = new function () { _userLibraryID, _libraryDataLoaded = false; - Zotero.Utilities.Internal.defineProperty(this, 'userLibraryID', { + Zotero.defineProperty(this, 'userLibraryID', { get: function() { if (!_libraryDataLoaded) { throw new Error("Library data not yet loaded"); @@ -177,4 +177,12 @@ Zotero.Libraries = new function () { throw new Error("Unsupported library type '" + type + "' in Zotero.Libraries.getName()"); } } -} + + this.isGroupLibrary = function (libraryID) { + if (!_libraryDataLoaded) { + throw new Error("Library data not yet loaded"); + } + + return this.getType(libraryID) == 'group'; + } +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js @@ -23,18 +23,15 @@ ***** END LICENSE BLOCK ***** */ -Zotero.Relations = new function () { - Zotero.DataObjects.apply(this, ['relation']); - this.constructor.prototype = new Zotero.DataObjects(); +Zotero.Relations = function () { + this.constructor = null; - this.__defineGetter__('relatedItemPredicate', function () "dc:relation"); - this.__defineGetter__('linkedObjectPredicate', function () "owl:sameAs"); - this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy'); + this._ZDO_object = 'relation'; + this._ZDO_idOnly = true; - var _namespaces = { - dc: 'http://purl.org/dc/elements/1.1/', - owl: 'http://www.w3.org/2002/07/owl#' - }; + Zotero.defineProperty(this, 'relatedItemPredicate', {value: 'dc:relation'}); + Zotero.defineProperty(this, 'linkedObjectPredicate', {value: 'owl:sameAs'}); + Zotero.defineProperty(this, 'deletedItemPredicate', {value: 'dc:isReplacedBy'}); this.get = function (id) { if (typeof id != 'number') { @@ -52,7 +49,7 @@ Zotero.Relations = new function () { */ this.getByURIs = Zotero.Promise.coroutine(function* (subject, predicate, object) { if (predicate) { - predicate = _getPrefixAndValue(predicate).join(':'); + predicate = this._getPrefixAndValue(predicate).join(':'); } if (!subject && !predicate && !object) { @@ -141,7 +138,7 @@ Zotero.Relations = new function () { this.add = Zotero.Promise.coroutine(function* (libraryID, subject, predicate, object) { - predicate = _getPrefixAndValue(predicate).join(':'); + predicate = this._getPrefixAndValue(predicate).join(':'); var relation = new Zotero.Relation; if (!libraryID) { @@ -272,11 +269,15 @@ Zotero.Relations = new function () { return relation; } + this._namespaces = { + dc: 'http://purl.org/dc/elements/1.1/', + owl: 'http://www.w3.org/2002/07/owl#' + }; - function _getPrefixAndValue(uri) { + this._getPrefixAndValue = function(uri) { var [prefix, value] = uri.split(':'); if (prefix && value) { - if (!_namespaces[prefix]) { + if (!this._namespaces[prefix]) { throw ("Invalid prefix '" + prefix + "' in Zotero.Relations._getPrefixAndValue()"); } return [prefix, value]; @@ -290,4 +291,8 @@ Zotero.Relations = new function () { } throw ("Invalid namespace in URI '" + uri + "' in Zotero.Relations._getPrefixAndValue()"); } -} + + Zotero.DataObjects.call(this); + + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -37,13 +37,10 @@ Zotero.Search = function() { this._hasPrimaryConditions = false; } -Zotero.Search._super = Zotero.DataObject; -Zotero.Search.prototype = Object.create(Zotero.Search._super.prototype); -Zotero.Search.constructor = Zotero.Search; +Zotero.extendClass(Zotero.DataObject, Zotero.Search); Zotero.Search.prototype._objectType = 'search'; Zotero.Search.prototype._dataTypes = Zotero.Search._super.prototype._dataTypes.concat([ - 'primaryData', 'conditions' ]); @@ -62,21 +59,33 @@ Zotero.Search.prototype.setName = function(val) { this.name = val; } - -Zotero.Search.prototype.__defineGetter__('id', function () { return this._get('id'); }); -Zotero.Search.prototype.__defineSetter__('id', function (val) { this._set('id', val); }); -Zotero.Search.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); }); -Zotero.Search.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); }); -Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); }); -Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val) }); -Zotero.Search.prototype.__defineGetter__('name', function () { return this._get('name'); }); -Zotero.Search.prototype.__defineSetter__('name', function (val) { this._set('name', val); }); -Zotero.Search.prototype.__defineGetter__('version', function () { return this._get('version'); }); -Zotero.Search.prototype.__defineSetter__('version', function (val) { this._set('version', val); }); -Zotero.Search.prototype.__defineGetter__('synced', function () { return this._get('synced'); }); -Zotero.Search.prototype.__defineSetter__('synced', function (val) { this._set('synced', val); }); - -Zotero.Search.prototype.__defineGetter__('conditions', function (arr) { this.getSearchConditions(); }); +Zotero.defineProperty(Zotero.Search.prototype, 'id', { + get: function() this._get('id'), + set: function(val) this._set('id', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'libraryID', { + get: function() this._get('libraryID'), + set: function(val) this._set('libraryID', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'key', { + get: function() this._get('key'), + set: function(val) this._set('key', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'name', { + get: function() this._get('name'), + set: function(val) this._set('name', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'version', { + get: function() this._get('version'), + set: function(val) this._set('version', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'synced', { + get: function() this._get('synced'), + set: function(val) this._set('synced', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'conditions', { + get: function() this.getSearchConditions() +}); Zotero.Search.prototype._set = function (field, value) { if (field == 'id' || field == 'libraryID' || field == 'key') { @@ -161,152 +170,115 @@ Zotero.Search.prototype.loadFromRow = function (row) { this._identified = true; } +Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + if (!this.name) { + throw('Name not provided for saved search'); + } + + return Zotero.Search._super.prototype._initSave.apply(this, arguments); +}); -/* - * Save the search to the DB and return a savedSearchID - * - * If there are gaps in the searchConditionIDs, |fixGaps| must be true - * and the caller must dispose of the search or reload the condition ids, - * which may change after the save. - * - * For new searches, name must be set called before saving - */ -Zotero.Search.prototype.save = Zotero.Promise.coroutine(function* (fixGaps) { - try { - Zotero.Searches.editCheck(this); - - if (!this.name) { - throw('Name not provided for saved search'); +Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var fixGaps = env.options.fixGaps; + var isNew = env.isNew; + + var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches'); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + var columns = [ + 'savedSearchID', + 'savedSearchName', + 'clientDateModified', + 'libraryID', + 'key', + 'version', + 'synced' + ]; + var placeholders = columns.map(function () '?').join(); + var sqlValues = [ + searchID ? { int: searchID } : null, + { string: this.name }, + Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ]; + + var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!searchID) { + searchID = env.id = insertID; + } + + if (!isNew) { + var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; + yield Zotero.DB.queryAsync(sql, this.id); + } + + // Close gaps in savedSearchIDs + var saveConditions = {}; + var i = 1; + for (var id in this._conditions) { + if (!fixGaps && id != i) { + Zotero.DB.rollbackTransaction(); + throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id); } + saveConditions[i] = this._conditions[id]; + i++; + } + + this._conditions = saveConditions; + + for (var i in this._conditions){ + var sql = "INSERT INTO savedSearchConditions (savedSearchID, " + + "searchConditionID, condition, operator, value, required) " + + "VALUES (?,?,?,?,?,?)"; - var isNew = !this.id; - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var searchID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Searches.registerIdentifiers(searchID, libraryID, key); - } - }; - } - else { - var transactionOptions = null; - } + // Convert condition and mode to "condition[/mode]" + var condition = this._conditions[i].mode ? + this._conditions[i].condition + '/' + this._conditions[i].mode : + this._conditions[i].condition - return Zotero.DB.executeTransaction(function* () { - searchID = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches'); - libraryID = this.libraryID; - key = this._key = this.key ? this.key : this._generateKey(); - - Zotero.debug("Saving " + (isNew ? 'new ' : '') + "search " + this.id); - - var columns = [ - 'savedSearchID', - 'savedSearchName', - 'clientDateModified', - 'libraryID', - 'key', - 'version', - 'synced' - ]; - var placeholders = columns.map(function () '?').join(); - var sqlValues = [ - searchID ? { int: searchID } : null, - { string: this.name }, - Zotero.DB.transactionDateTime, - this.libraryID ? this.libraryID : 0, - key, - this.version ? this.version : 0, - this.synced ? 1 : 0 - ]; - - var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") " - + "VALUES (" + placeholders + ")"; - var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); - if (!searchID) { - searchID = insertID; - } - - if (!isNew) { - var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; - yield Zotero.DB.queryAsync(sql, this.id); - } - - // Close gaps in savedSearchIDs - var saveConditions = {}; - var i = 1; - for (var id in this._conditions) { - if (!fixGaps && id != i) { - Zotero.DB.rollbackTransaction(); - throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id); - } - saveConditions[i] = this._conditions[id]; - i++; - } - - this._conditions = saveConditions; - - for (var i in this._conditions){ - var sql = "INSERT INTO savedSearchConditions (savedSearchID, " - + "searchConditionID, condition, operator, value, required) " - + "VALUES (?,?,?,?,?,?)"; - - // Convert condition and mode to "condition[/mode]" - var condition = this._conditions[i].mode ? - this._conditions[i].condition + '/' + this._conditions[i].mode : - this._conditions[i].condition - - var sqlParams = [ - searchID, - i, - condition, - this._conditions[i].operator ? this._conditions[i].operator : null, - this._conditions[i].value ? this._conditions[i].value : null, - this._conditions[i].required ? 1 : null - ]; - yield Zotero.DB.queryAsync(sql, sqlParams); - } - - - if (isNew) { - Zotero.Notifier.trigger('add', 'search', this.id); - } - else { - Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData); - } - - if (isNew && this.libraryID) { - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); - var group = yield Zotero.Groups.get(groupID); - group.clearSearchCache(); - } - - if (isNew) { - var id = this.id; - this._disabled = true; - return id; - } - - yield this.reload(); - this._clearChanged(); - - return isNew ? this.id : true; - }.bind(this), transactionOptions); + var sqlParams = [ + searchID, + i, + condition, + this._conditions[i].operator ? this._conditions[i].operator : null, + this._conditions[i].value ? this._conditions[i].value : null, + this._conditions[i].required ? 1 : null + ]; + yield Zotero.DB.queryAsync(sql, sqlParams); } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); - } - catch (e2) { - Zotero.debug(e2, 1); - } - - Zotero.debug(e, 1); - throw e; +}); + +Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + if (isNew) { + Zotero.Notifier.trigger('add', 'search', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData); } + + if (isNew && Zotero.Libraries.isGroupLibrary(this.libraryID)) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); + var group = yield Zotero.Groups.get(groupID); + group.clearSearchCache(); + } + + if (isNew) { + var id = this.id; + this._disabled = true; + return id; + } + + yield this.reload(); + this._clearChanged(); + + return isNew ? this.id : true; }); @@ -1189,7 +1161,7 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { let objLibraryID; let objKey = condition.value; let objectType = condition.name == 'collection' ? 'collection' : 'search'; - let objectTypeClass = Zotero.DataObjectUtilities.getClassForObjectType(objectType); + let objectTypeClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); // Old-style library-key hash if (objKey.contains('_')) { @@ -1665,29 +1637,25 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { this._sqlParams = sqlParams.length ? sqlParams : false; }); -Zotero.Searches = new function(){ - Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']); - this.constructor.prototype = new Zotero.DataObjects(); - - Object.defineProperty(this, "_primaryDataSQLParts", { - get: function () { - return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = { - savedSearchID: "O.savedSearchID", - name: "O.savedSearchName", - libraryID: "O.libraryID", - key: "O.key", - version: "O.version", - synced: "O.synced" - }); - } - }); - - - var _primaryDataSQLParts; +Zotero.Searches = function() { + this.constructor = null; + + this._ZDO_object = 'search'; + this._ZDO_id = 'savedSearch'; + this._ZDO_table = 'savedSearches'; + + this._primaryDataSQLParts = { + savedSearchID: "O.savedSearchID", + name: "O.savedSearchName", + libraryID: "O.libraryID", + key: "O.key", + version: "O.version", + synced: "O.synced" + } this.init = Zotero.Promise.coroutine(function* () { - yield this.constructor.prototype.init.apply(this); + yield Zotero.DataObjects.prototype.init.apply(this); yield Zotero.SearchConditions.init(); }); @@ -1735,6 +1703,8 @@ Zotero.Searches = new function(){ let id = ids[i]; var search = new Zotero.Search; search.id = id; + yield search.loadPrimaryData(); + yield search.loadConditions(); notifierData[id] = { old: search.serialize() }; var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; @@ -1756,7 +1726,11 @@ Zotero.Searches = new function(){ + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " + "FROM savedSearches O WHERE 1"; } -} + + Zotero.DataObjects.call(this); + + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -493,24 +493,6 @@ Zotero.Utilities.Internal = { }, 0, 0, null); return pipe.inputStream; - }, - - /** - * Defines property on the object - * More compact way to do Object.defineProperty - * - * @param {Object} obj Target object - * @param {String} prop Property to be defined - * @param {Object} desc Propery descriptor. If not overriden, "enumerable" is true - */ - "defineProperty": function(obj, prop, desc) { - if (typeof prop != 'string') throw new Error("Property must be a string"); - var d = { __proto__: null, enumerable: true }; // Enumerable by default - for (let p in desc) { - if (!desc.hasOwnProperty(p)) continue; - d[p] = desc[p]; - } - Object.defineProperty(obj, prop, d); } } diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -1400,6 +1400,40 @@ Components.utils.import("resource://gre/modules/osfile.jsm"); } + /** + * Defines property on the object + * More compact way to do Object.defineProperty + * + * @param {Object} obj Target object + * @param {String} prop Property to be defined + * @param {Object} desc Propery descriptor. If not overriden, "enumerable" is true + * @param {Object} opts Options: + * lazy {Boolean} If true, the _getter_ is intended for late + * initialization of the property. The getter is replaced with a simple + * property once initialized. + */ + this.defineProperty = function(obj, prop, desc, opts) { + if (typeof prop != 'string') throw new Error("Property must be a string"); + var d = { __proto__: null, enumerable: true, configurable: true }; // Enumerable by default + for (let p in desc) { + if (!desc.hasOwnProperty(p)) continue; + d[p] = desc[p]; + } + + if (opts) { + if (opts.lazy && d.get) { + let getter = d.get; + d.get = function() { + var val = getter.call(this); + this[prop] = val; // Replace getter with value + return val; + } + } + } + + Object.defineProperty(obj, prop, d); + } + /* * This function should be removed * @@ -1497,6 +1531,12 @@ Components.utils.import("resource://gre/modules/osfile.jsm"); }; } + this.defineProperty(this, "localeCompare", { + get: function() { + var collation = this.getLocaleCollation(); + return collation.compareString.bind(collation, 1); + } + }, {lazy: true}); /* * Sets font size based on prefs -- intended for use on root element @@ -1580,6 +1620,46 @@ Components.utils.import("resource://gre/modules/osfile.jsm"); /** + * Defines property on the object + * More compact way to do Object.defineProperty + * + * @param {Object} obj Target object + * @param {String} prop Property to be defined + * @param {Object} desc Propery descriptor. If not overriden, "enumerable" is true + * @param {Object} opts Options: + * lateInit {Boolean} If true, the _getter_ is intended for late + * initialization of the property. The getter is replaced with a simple + * property once initialized. + */ + this.defineProperty = function(obj, prop, desc, opts) { + if (typeof prop != 'string') throw new Error("Property must be a string"); + var d = { __proto__: null, enumerable: true, configurable: true }; // Enumerable by default + for (let p in desc) { + if (!desc.hasOwnProperty(p)) continue; + d[p] = desc[p]; + } + + if (opts) { + if (opts.lateInit && d.get) { + let getter = d.get; + d.get = function() { + var val = getter.call(this); + this[prop] = val; // Replace getter with value + return val; + } + } + } + + Object.defineProperty(obj, prop, d); + } + + this.extendClass = function(superClass, newClass) { + newClass._super = superClass; + newClass.prototype = Object.create(superClass.prototype); + newClass.prototype.constructor = newClass; + } + + /** * Allow other events (e.g., UI updates) on main thread to be processed if necessary * * @param {Integer} [timeout=50] Maximum number of milliseconds to wait diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js @@ -1971,6 +1971,7 @@ var ZoteroPane = new function() } var self = this; + var deferred = Zotero.Promise.defer(); this.collectionsView.addEventListener('load', function () { Zotero.spawn(function* () { var currentLibraryID = self.getSelectedLibraryID(); @@ -1993,15 +1994,22 @@ var ZoteroPane = new function() yield self.collectionsView.selectLibrary(item.libraryID); yield self.itemsView.selectItem(itemID, expand); } + deferred.resolve(true); + }) + .catch(function(e) { + deferred.reject(e); }); }); + }) + .catch(function(e) { + deferred.reject(e); }); }); // open Zotero pane this.show(); - return true; + return deferred.promise; });