diff --git a/chrome/content/zotero/bindings/zoterosearch.xml b/chrome/content/zotero/bindings/zoterosearch.xml index f93b8076c..f84f724c1 100644 --- a/chrome/content/zotero/bindings/zoterosearch.xml +++ b/chrome/content/zotero/bindings/zoterosearch.xml @@ -232,7 +232,7 @@ diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js index 2a38a569b..a732c7800 100644 --- 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 index dc75fe18f..0244b578d 100644 --- 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) { @@ -114,45 +127,13 @@ 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._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 (!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'); + } + + env.parent = newParent.id; + } + else { + env.parent = null; + } + + return true; +}); -Zotero.Collection.prototype.save = Zotero.Promise.coroutine(function* () { - try { - Zotero.Collections.editCheck(this); +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(); - if (!this.name) { - throw new Error('Collection name is empty'); + var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!collectionID) { + collectionID = env.id = insertID; } - - if (Zotero.Utilities.isEmpty(this._changed)) { - Zotero.debug("Collection " + this.id + " has not changed"); - return false; - } - - 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; - } - - 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); } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); - } - catch (e2) { - Zotero.debug(e2, 1); - } - - Zotero.debug(e, 1); - throw e; + 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 + )); + } + if (this.parentKey) { + parentIDs.push(this.ObjectsClass.getIDFromLibraryAndKey( + this.libraryID, this.parentKey + )); + } + 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 index 2377355c5..3cb752086 100644 --- 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 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 index a7a9b7ce5..4704e5e8c 100644 --- 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} 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 index 854419f43..fa70c0b9c 100644 --- 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 index 9a6aded3d..2c41a458b 100644 --- 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); } - // 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; - - // 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._ZDO_id) { + this._ZDO_id = this._ZDO_object + 'ID'; } + if (!this._ZDO_table) { + this._ZDO_table = this._ZDO_objects; + } + + 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; - - // Public properties - this.table = this._ZDO_table; - - - this.init = function () { - return this._loadIDsAndKeys(); +} + +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} 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]; } + var 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; - }); - - - this.isPrimaryField = function (field) { - return this.primaryFields.indexOf(field) != -1; + for (let i=0; i} 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} 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 = []; + + 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} 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} - 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); +} + + +/** + * @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; + } + 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 + */ +Zotero.DataObjects.prototype.getByLibraryAndKey = function (libraryID, key, options) { + var id = this.getIDFromLibraryAndKey(libraryID, key); + if (!id) { + return 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} - 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()") } - - /** - * @return {Array} Array with libraryID and key - */ - this.getLibraryAndKeyFromID = function (id) { - return this._objectKeys[id] ? this._objectKeys[id] : false; + 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); - this.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?"; - 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 - */ - 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); - - for (let i=0; i 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 = {}; + + // 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.primaryDataSQL; + 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 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,71 +294,16 @@ 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 this._changed[dataType]).length -} - - /* * Set or change the item's type */ @@ -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; +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; } - 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; - } - 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} 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 = {}; - } +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; - var isNew = !this.id; - - Zotero.Items.editCheck(this); - - if (!this.hasChanged()) { - Zotero.debug('Item ' + this.id + ' has not changed', 4); - return false; - } - - // 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; + } + } + } + + 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; + } + + 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); + + 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); + + // 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)); + } + } + + // + // 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 = !isNew && 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 + ] + ); + } + } + + // 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"); + } + + 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 { - var transactionOptions = null; + 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 = this.ObjectsClass.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 wrapper if not present - if (!noteText.match(/^
[\s\S]*<\/div>$/)) { - // Keep consistent with getNote() - noteText = '
' + noteText + '
'; - } - - 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; - } - } - } - - 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 wrapper if not present + if (!noteText.match(/^
[\s\S]*<\/div>$/)) { + // Keep consistent with getNote() + noteText = '
' + noteText + '
'; + } + + 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; + } + } + } + + 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 { + 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; +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; } - 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 = {}; - } - 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; +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; } - 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 = {}; - } - 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; +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._attachmentCharset }); - -Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) { - if (!this.isAttachment()) { - throw (".attachmentCharset can only be set for attachment items"); +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; } - - 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; - } - 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 = {}; - } - 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; +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; } - 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 = {}; - } - this._changed.attachmentData.syncState = true; - this._attachmentSyncState = val; }); @@ -2922,29 +2868,31 @@ Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) { * @return {Promise} 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; +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; } - - if (!this.id) { - return undefined; - } - - var file = this.getFile(); - if (!file) { - return undefined; - } - - return Zotero.Utilities.Internal.md5(file) || undefined; }); @@ -2982,84 +2932,86 @@ Zotero.Item.prototype.__defineGetter__('attachmentHash', function () { * * @return {Promise} - 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(); - } - return ''; - } - - var contentType = this.attachmentContentType; - if (!contentType) { - contentType = yield Zotero.MIME.getMIMETypeFromFile(file); - if (contentType) { - this.attachmentContentType = contentType; - yield this.save(); - } - } - - 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; +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentText', { + get: Zotero.Promise.coroutine(function* () { + if (!this.isAttachment()) { + return undefined; } - if (reindex) { - if (!Zotero.Fulltext.pdfConverterIsRegistered()) { - Zotero.debug("PDF converter is unavailable -- returning empty .attachmentText", 3); - return ''; + 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(); } - yield Zotero.Fulltext.indexItems(this.id, false); - } - - if (!cacheFile.exists()) { - Zotero.debug("Cache file doesn't exist after indexing -- returning empty .attachmentText"); 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(); -})); + + var contentType = this.attachmentContentType; + if (!contentType) { + contentType = yield Zotero.MIME.getMIMETypeFromFile(file); + if (contentType) { + this.attachmentContentType = contentType; + yield this.save(); + } + } + + 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 ''; + } + 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(); + }) +}); @@ -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.Item.prototype._eraseInit = Zotero.Promise.coroutine(function* (env) { + var proceed = yield Zotero.Item._super.prototype._eraseInit.apply(this, arguments); + if (!proceed) return false; + + env.deletedItemNotifierData = {}; + env.deletedItemNotifierData[this.id] = { old: this.toJSON() }; + + 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 { 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 index 8234f2083..9fdc5a827 100644 --- 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 index 310f39e97..de60de528 100644 --- 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 index 0166666ad..cee68a6aa 100644 --- 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'); - } - - 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; - } - - 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); +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; } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); - } - catch (e2) { - Zotero.debug(e2, 1); - } - - Zotero.debug(e, 1); - throw e; + + 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); + } +}); + +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(); +Zotero.Searches = function() { + this.constructor = null; - 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" - }); - } - }); + this._ZDO_object = 'search'; + this._ZDO_id = 'savedSearch'; + this._ZDO_table = 'savedSearches'; - - var _primaryDataSQLParts; + 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 index f88c08f75..bbe1767c0 100644 --- 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 index cdd4bd080..3501acb3b 100644 --- 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 @@ -1579,6 +1619,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 * diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 01c6918c8..09c7c28e3 100644 --- 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; });