Merge pull request #576 from aurimasv/async_db-av2

[Async DB] Modularize Zotero.DataObject.save()
This commit is contained in:
Dan Stillman 2015-01-28 15:07:32 -05:00
commit 66a04a39db
16 changed files with 2430 additions and 2341 deletions

View file

@ -232,7 +232,7 @@
<body> <body>
<![CDATA[ <![CDATA[
this.updateSearch(); this.updateSearch();
return this.search.save(true); return this.search.save({fixGaps: true});
]]> ]]>
</body> </body>
</method> </method>

View file

@ -201,7 +201,7 @@ Zotero.API.Data = {
var params = this.parsePath(path); var params = this.parsePath(path);
//Zotero.debug(params); //Zotero.debug(params);
return Zotero.DataObjectUtilities.getClassForObjectType(params.objectType) return Zotero.DataObjectUtilities.getObjectsClassForObjectType(params.objectType)
.apiDataGenerator(params); .apiDataGenerator(params);
} }
}; };

View file

@ -37,38 +37,51 @@ Zotero.Collection = function() {
this._childItems = []; this._childItems = [];
} }
Zotero.Collection._super = Zotero.DataObject; Zotero.extendClass(Zotero.DataObject, Zotero.Collection);
Zotero.Collection.prototype = Object.create(Zotero.Collection._super.prototype);
Zotero.Collection.constructor = Zotero.Collection;
Zotero.Collection.prototype._objectType = 'collection'; Zotero.Collection.prototype._objectType = 'collection';
Zotero.Collection.prototype._dataTypes = Zotero.Collection._super.prototype._dataTypes.concat([ Zotero.Collection.prototype._dataTypes = Zotero.Collection._super.prototype._dataTypes.concat([
'primaryData',
'childCollections', 'childCollections',
'childItems' 'childItems'
]); ]);
Zotero.Collection.prototype.__defineGetter__('id', function () { return this._get('id'); }); Zotero.defineProperty(Zotero.Collection.prototype, 'ChildObjects', {
Zotero.Collection.prototype.__defineSetter__('id', function (val) { this._set('id', val); }); get: function() Zotero.Items
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.Collection.prototype.__defineSetter__('parent', function (val) {
Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2); Zotero.defineProperty(Zotero.Collection.prototype, 'id', {
this.parentID = val; 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) { 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 * Populate collection data from a database row
*/ */
Zotero.Collection.prototype.loadFromRow = function(row) { Zotero.Collection.prototype.loadFromRow = function(row) {
Zotero.debug("Loading collection from 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) { if (row[col] === undefined) {
Zotero.debug('Skipping missing collection field ' + col); Zotero.debug('Skipping missing collection field ' + col);
} }
@ -267,168 +248,139 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) {
return objs; 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* () { Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
try { var isNew = env.isNew;
Zotero.Collections.editCheck(this);
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) { var sql = "REPLACE INTO collections (" + columns.join(', ') + ") "
throw new Error('Collection name is empty'); + "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) { else {
try { columns.shift();
yield this.reload(); sqlValues.push(sqlValues.shift());
this._clearChanged(); let sql = 'UPDATE collections SET '
} + columns.map(function (x) x + '=?').join(', ')
catch (e2) { + ' WHERE collectionID=?';
Zotero.debug(e2, 1); yield Zotero.DB.queryAsync(sql, sqlValues);
}
Zotero.debug(e, 1);
throw e;
} }
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; continue;
} }
let item = yield Zotero.Items.getAsync(itemID); let item = yield this.ChildObjects.getAsync(itemID);
yield item.loadCollections(); yield item.loadCollections();
item.addToCollection(this.id); item.addToCollection(this.id);
yield item.save({ yield item.save({
@ -513,7 +465,7 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it
continue; continue;
} }
let item = yield Zotero.Items.getAsync(itemID); let item = yield this.ChildObjects.getAsync(itemID);
yield item.loadCollections(); yield item.loadCollections();
item.removeFromCollection(this.id); item.removeFromCollection(this.id);
yield item.save({ yield item.save({
@ -565,7 +517,7 @@ Zotero.Collection.prototype.diff = function (collection, includeMatches) {
var diff = []; var diff = [];
var thisData = this.serialize(); var thisData = this.serialize();
var otherData = collection.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 // For the moment, just compare children and increase numDiffs if any differences
var d1 = Zotero.Utilities.arrayDiff( var d1 = Zotero.Utilities.arrayDiff(
@ -625,7 +577,7 @@ Zotero.Collection.prototype.clone = function (includePrimary, newCollection) {
var sameLibrary = newCollection.libraryID == this.libraryID; var sameLibrary = newCollection.libraryID == this.libraryID;
} }
else { else {
var newCollection = new Zotero.Collection; var newCollection = new this.constructor;
var sameLibrary = true; var sameLibrary = true;
if (includePrimary) { if (includePrimary) {
@ -661,7 +613,7 @@ Zotero.Collection.prototype.erase = function(deleteItems) {
// Descendent collections // Descendent collections
if (descendents[i].type == 'collection') { if (descendents[i].type == 'collection') {
collections.push(descendents[i].id); 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) { if (c) {
notifierData[c.id] = { old: c.toJSON() }; notifierData[c.id] = { old: c.toJSON() };
} }
@ -675,7 +627,7 @@ Zotero.Collection.prototype.erase = function(deleteItems) {
} }
} }
if (del.length) { if (del.length) {
yield Zotero.Items.trash(del); yield this.ChildObjects.trash(del);
} }
// Remove relations // Remove relations
@ -698,9 +650,9 @@ Zotero.Collection.prototype.erase = function(deleteItems) {
// TODO: Update member items // TODO: Update member items
}.bind(this)) }.bind(this))
.then(function () { .then(() => {
// Clear deleted collection from internal memory // Clear deleted collection from internal memory
Zotero.Collections.unload(collections); this.ObjectsClass.unload(collections);
//return Zotero.Collections.reloadAll(); //return Zotero.Collections.reloadAll();
}) })
.then(function () { .then(function () {
@ -815,7 +767,7 @@ Zotero.Collection.prototype.getChildren = Zotero.Promise.coroutine(function* (re
} }
if (recursive) { 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( let descendents = yield child.getChildren(
true, nested, type, includeDeletedItems, level+1 true, nested, type, includeDeletedItems, level+1
); );
@ -871,7 +823,7 @@ Zotero.Collection.prototype.addLinkedCollection = Zotero.Promise.coroutine(funct
var predicate = Zotero.Relations.linkedObjectPredicate; var predicate = Zotero.Relations.linkedObjectPredicate;
if ((yield Zotero.Relations.getByURIs(url1, predicate, url2)).length if ((yield Zotero.Relations.getByURIs(url1, predicate, url2)).length
|| (yield Zotero.Relations.getByURIs(url2, predicate, url1)).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; return false;
} }
@ -901,9 +853,9 @@ Zotero.Collection.prototype.loadChildCollections = Zotero.Promise.coroutine(func
this._childCollections = []; this._childCollections = [];
if (ids) { if (ids.length) {
for each(var id in ids) { for each(var id in ids) {
var col = yield Zotero.Collections.getAsync(id); var col = yield this.ObjectsClass.getAsync(id);
if (!col) { if (!col) {
throw new Error('Collection ' + id + ' not found'); throw new Error('Collection ' + id + ' not found');
} }
@ -943,7 +895,7 @@ Zotero.Collection.prototype.loadChildItems = Zotero.Promise.coroutine(function*
this._childItems = []; this._childItems = [];
if (ids) { if (ids) {
var items = yield Zotero.Items.getAsync(ids) var items = yield this.ChildObjects.getAsync(ids)
if (items) { if (items) {
this._childItems = items; this._childItems = items;
} }

View file

@ -27,9 +27,10 @@
/* /*
* Primary interface for accessing Zotero collection * Primary interface for accessing Zotero collection
*/ */
Zotero.Collections = new function() { Zotero.Collections = function() {
Zotero.DataObjects.apply(this, ['collection']); this.constructor = null;
this.constructor.prototype = new Zotero.DataObjects();
this._ZDO_object = 'collection';
this._primaryDataSQLParts = { this._primaryDataSQLParts = {
collectionID: "O.collectionID", collectionID: "O.collectionID",
@ -45,9 +46,13 @@ Zotero.Collections = new function() {
hasChildCollections: "(SELECT COUNT(*) FROM collections WHERE " hasChildCollections: "(SELECT COUNT(*) FROM collections WHERE "
+ "parentCollectionID=O.collectionID) != 0 AS hasChildCollections", + "parentCollectionID=O.collectionID) != 0 AS hasChildCollections",
hasChildItems: "(SELECT COUNT(*) FROM collectionItems WHERE " 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 * Add new collection to DB and return Collection object
* *
@ -74,55 +79,51 @@ Zotero.Collections = new function() {
* Takes parent collectionID as optional parameter; * Takes parent collectionID as optional parameter;
* by default, returns root collections * by default, returns root collections
*/ */
this.getByParent = Zotero.Promise.coroutine(function* (libraryID, parent, recursive) { this.getByParent = Zotero.Promise.coroutine(function* (libraryID, parentID, recursive) {
var toReturn = []; let children;
if (!parent) { if (parentID) {
parent = null; 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 " if (!children.length) {
+ "WHERE libraryID=? AND parentCollectionID " + (parent ? '= ' + parent : 'IS NULL'); return children;
var children = yield Zotero.DB.queryAsync(sql, [libraryID]);
if (!children) {
Zotero.debug('No child collections of collection ' + parent, 5);
return toReturn;
} }
// Do proper collation sort // Do proper collation sort
var collation = Zotero.getLocaleCollation(); children.sort(function (a, b) Zotero.localeCompare(a.name, b.name));
children.sort(function (a, b) {
return collation.compareString(1, a.name, b.name);
});
if (!recursive) return children;
let toReturn = [];
for (var i=0, len=children.length; i<len; i++) { for (var i=0, len=children.length; i<len; i++) {
var obj = yield this.getAsync(children[i].id); var obj = children[i];
if (!obj) {
throw ('Collection ' + children[i].id + ' not found');
}
toReturn.push(obj); toReturn.push(obj);
// If recursive, get descendents var desc = obj.getDescendents(false, 'collection');
if (recursive) { for (var j in desc) {
var desc = obj.getDescendents(false, 'collection'); var obj2 = yield this.getAsync(desc[j]['id']);
for (var j in desc) { if (!obj2) {
var obj2 = yield this.getAsync(desc[j]['id']); throw new Error('Collection ' + desc[j] + ' not found');
if (!obj2) {
throw new Error('Collection ' + desc[j] + ' not found');
}
// TODO: This is a quick hack so that we can indent subcollections
// in the search dialog -- ideally collections would have a
// getLevel() method, but there's no particularly quick way
// of calculating that without either storing it in the DB or
// changing the schema to Modified Preorder Tree Traversal,
// and I don't know if we'll actually need it anywhere else.
obj2.level = desc[j].level;
toReturn.push(obj2);
} }
// TODO: This is a quick hack so that we can indent subcollections
// in the search dialog -- ideally collections would have a
// getLevel() method, but there's no particularly quick way
// of calculating that without either storing it in the DB or
// changing the schema to Modified Preorder Tree Traversal,
// and I don't know if we'll actually need it anywhere else.
obj2.level = desc[j].level;
toReturn.push(obj2);
} }
} }
@ -130,6 +131,17 @@ Zotero.Collections = new function() {
}); });
this.getCollectionsInLibrary = Zotero.Promise.coroutine(function* (libraryID) {
let sql = "SELECT collectionID AS id FROM collections C "
+ "WHERE libraryID=? AND parentCollectionId IS NULL";
let ids = yield Zotero.DB.queryAsync(sql, [libraryID]);
let collections = yield this.getAsync(ids.map(function(row) row.id));
if (!collections.length) return collections;
return collections.sort(function (a, b) Zotero.localeCompare(a.name, b.name));
});
this.getCollectionsContainingItems = function (itemIDs, asIDs) { this.getCollectionsContainingItems = function (itemIDs, asIDs) {
// If an unreasonable number of items, don't try // If an unreasonable number of items, don't try
if (itemIDs.length > 100) { if (itemIDs.length > 100) {
@ -145,8 +157,8 @@ Zotero.Collections = new function() {
} }
sql = sql.substring(0, sql.length - 5); sql = sql.substring(0, sql.length - 5);
return Zotero.DB.columnQueryAsync(sql, sqlParams) return Zotero.DB.columnQueryAsync(sql, sqlParams)
.then(function (collectionIDs) { .then(collectionIDs => {
return asIDs ? collectionIDs : Zotero.Collections.get(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); ids = Zotero.flattenArguments(ids);
Zotero.DB.beginTransaction(); return Zotero.DB.executeTransaction(function* () {
for each(var id in ids) { for each(var id in ids) {
var collection = this.getAsync(id); var collection = yield this.getAsync(id);
if (collection) { if (collection) {
collection.erase(); yield collection.erase();
}
collection = undefined;
} }
collection = undefined;
} this.unload(ids);
});
this.unload(ids); };
Zotero.DB.commitTransaction();
}
Zotero.DataObjects.call(this);
this.getPrimaryDataSQL = function () { return this;
// This should be the same as the query in Zotero.Collection.load(), }.bind(Object.create(Zotero.DataObjects.prototype))();
// 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";
}
}

View file

@ -34,6 +34,8 @@ Zotero.DataObject = function () {
let objectType = this._objectType; let objectType = this._objectType;
this._ObjectType = objectType[0].toUpperCase() + objectType.substr(1); this._ObjectType = objectType[0].toUpperCase() + objectType.substr(1);
this._objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 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._id = null;
this._libraryID = null; this._libraryID = null;
@ -53,23 +55,36 @@ Zotero.DataObject = function () {
}; };
Zotero.DataObject.prototype._objectType = 'dataObject'; 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 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 get: function() this._libraryID + "/" + this._key
}); });
Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'parentKey', { Zotero.defineProperty(Zotero.DataObject.prototype, 'parentKey', {
get: function() this._parentKey, get: function() this._parentKey,
set: function(v) this._setParentKey(v) set: function(v) this._setParentKey(v)
}); });
Zotero.Utilities.Internal.defineProperty(Zotero.DataObject.prototype, 'parentID', { Zotero.defineProperty(Zotero.DataObject.prototype, 'parentID', {
get: function() this._getParentID(), get: function() this._getParentID(),
set: function(v) this._setParentID(v) set: function(v) this._setParentID(v)
}); });
Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', {
get: function() this._ObjectsClass
});
Zotero.DataObject.prototype._get = function (field) { Zotero.DataObject.prototype._get = function (field) {
if (this['_' + field] !== null) { if (this['_' + field] !== null) {
@ -135,7 +150,7 @@ Zotero.DataObject.prototype._getParentID = function () {
if (!this._parentKey) { if (!this._parentKey) {
return false; 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) { Zotero.DataObject.prototype._setParentID = function (id) {
return this._setParentKey( return this._setParentKey(
id id
? this._getClass().getLibraryAndKeyFromID(Zotero.DataObjectUtilities.checkDataID(id))[1] ? this.ObjectsClass.getLibraryAndKeyFromID(Zotero.DataObjectUtilities.checkDataID(id))[1]
: null : null
); );
} }
@ -309,6 +324,60 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function
return false; return false;
}); });
/*
* Build object from database
*/
Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) {
if (this._loaded.primaryData && !reload) return;
var id = this.id;
var key = this.key;
var libraryID = this.libraryID;
if (!id && !key) {
throw new Error('ID or key not set in Zotero.' + this._ObjectType + '.loadPrimaryData()');
}
var columns = [], join = [], where = [];
var primaryFields = this.ObjectsClass.primaryFields;
var idField = this.ObjectsClass.idColumn;
for (let i=0; i<primaryFields.length; i++) {
let field = primaryFields[i];
// If field not already set
if (field == idField || this['_' + field] === null || reload) {
columns.push(this.ObjectsClass.getPrimaryDataSQLPart(field));
}
}
if (!columns.length) {
return;
}
// This should match Zotero.*.primaryDataSQL, but without
// necessarily including all columns
var sql = "SELECT " + columns.join(", ") + this.ObjectsClass.primaryDataSQLFrom;
if (id) {
sql += " AND O." + idField + "=? ";
var params = id;
}
else {
sql += " AND O.key=? AND O.libraryID=? ";
var params = [key, libraryID];
}
sql += (where.length ? ' AND ' + where.join(' AND ') : '');
var row = yield Zotero.DB.rowQueryAsync(sql, params);
if (!row) {
if (failOnMissing) {
throw new Error(this._ObjectType + " " + (id ? id : libraryID + "/" + key)
+ " not found in Zotero." + this._ObjectType + ".loadPrimaryData()");
}
this._loaded.primaryData = true;
this._clearChanged('primaryData');
return;
}
this.loadFromRow(row, reload);
});
/** /**
* Reloads loaded, changed data * Reloads loaded, changed data
@ -368,13 +437,6 @@ Zotero.DataObject.prototype._requireData = function (dataType) {
} }
} }
/**
* Returns a global Zotero class object given a data object. (e.g. Zotero.Items)
* @return {obj} One of Zotero data classes
*/
Zotero.DataObject.prototype._getClass = function () {
return Zotero.DataObjectUtilities.getClassForObjectType(this._objectType);
}
/** /**
* Loads data for a given data type * Loads data for a given data type
@ -385,6 +447,14 @@ Zotero.DataObject.prototype._loadDataType = function (dataType, reload) {
return this["load" + dataType[0].toUpperCase() + dataType.substr(1)](reload); return this["load" + dataType[0].toUpperCase() + dataType.substr(1)](reload);
} }
Zotero.DataObject.prototype.loadAllData = function (reload) {
let loadPromises = new Array(this._dataTypes.length);
for (let i=0; i<this._dataTypes.length; i++) {
loadPromises[i] = this._loadDataType(this._dataTypes[i], reload);
}
return Zotero.Promise.all(loadPromises);
}
/** /**
* Save old version of data that's being changed, to pass to the notifier * Save old version of data that's being changed, to pass to the notifier
@ -422,6 +492,141 @@ Zotero.DataObject.prototype._clearFieldChange = function (field) {
delete this._previousData[field]; delete this._previousData[field];
} }
Zotero.DataObject.prototype.isEditable = function () {
return Zotero.Libraries.isEditable(this.libraryID);
}
Zotero.DataObject.prototype.editCheck = function () {
if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable()) {
throw ("Cannot edit " + this._objectType + " in read-only Zotero library");
}
}
/**
* Save changes to database
*
* @return {Promise<Integer|Boolean>} Promise for itemID of new item,
* TRUE on item update, or FALSE if item was unchanged
*/
Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) {
var env = {
transactionOptions: null,
options: options || {}
};
var proceed = yield this._initSave(env);
if (!proceed) return false;
if (env.isNew) {
Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4);
}
else {
Zotero.debug('Updating database with new ' + this._objectType + ' data', 4);
}
return Zotero.DB.executeTransaction(function* () {
yield this._saveData(env);
return yield this._finalizeSave(env);
}.bind(this), env.transactionOptions)
.catch(e => {
return this._recoverFromSaveError(env, e)
.catch(function(e2) {
Zotero.debug(e2, 1);
})
.then(function() {
Zotero.debug(e, 1);
throw e;
})
});
});
Zotero.DataObject.prototype.hasChanged = function() {
Zotero.debug(this._changed);
return !!Object.keys(this._changed).filter(dataType => this._changed[dataType]).length
}
Zotero.DataObject.prototype._saveData = function() {
throw new Error("Zotero.DataObject.prototype._saveData is an abstract method");
}
Zotero.DataObject.prototype._finalizeSave = function() {
throw new Error("Zotero.DataObject.prototype._finalizeSave is an abstract method");
}
Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* () {
yield this.reload(null, true);
this._clearChanged();
});
Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
env.isNew = !this.id;
if (!env.options.skipEditCheck) this.editCheck();
if (!this.hasChanged()) {
Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4);
return false;
}
// Register this object's identifiers in Zotero.DataObjects on transaction commit,
// before other callbacks run
if (env.isNew) {
env.transactionOptions = {
onCommit: () => {
this.ObjectsClass.registerIdentifiers(env.id, env.libraryID, env.key);
}
};
}
return true;
});
/**
* Delete object from database
*/
Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* () {
var env = {};
var proceed = yield this._eraseInit(env);
if (!proceed) return false;
Zotero.debug('Deleting ' + this.objectType + ' ' + this.id);
yield Zotero.DB.executeTransaction(function* () {
yield this._eraseData(env);
yield this._erasePreCommit(env);
}.bind(this))
.catch(e => {
return this._eraseRecoverFromFailure(env);
});
return this._erasePostCommit(env);
});
Zotero.DataObject.prototype._eraseInit = function(env) {
if (!this.id) return Zotero.Promise.resolve(false);
return Zotero.Promise.resolve(true);
};
Zotero.DataObject.prototype._eraseData = function(env) {
throw new Error("Zotero.DataObject.prototype._eraseData is an abstract method");
};
Zotero.DataObject.prototype._erasePreCommit = function(env) {
return Zotero.Promise.resolve();
};
Zotero.DataObject.prototype._erasePostCommit = function(env) {
return Zotero.Promise.resolve();
};
Zotero.DataObject.prototype._eraseRecoverFromFailure = function(env) {
throw new Error("Zotero.DataObject.prototype._eraseRecoverFromFailure is an abstract method");
};
/** /**
* Generates data object key * Generates data object key
* @return {String} key * @return {String} key

View file

@ -59,12 +59,12 @@ Zotero.DataObjectUtilities = {
}, },
"getObjectTypePlural": function getObjectTypePlural(objectType) { "getObjectTypePlural": function(objectType) {
return objectType == 'search' ? 'searches' : objectType + 's'; return objectType == 'search' ? 'searches' : objectType + 's';
}, },
"getClassForObjectType": function getClassForObjectType(objectType) { "getObjectsClassForObjectType": function(objectType) {
var objectTypePlural = this.getObjectTypePlural(objectType); var objectTypePlural = this.getObjectTypePlural(objectType);
var className = objectTypePlural[0].toUpperCase() + objectTypePlural.substr(1); var className = objectTypePlural[0].toUpperCase() + objectTypePlural.substr(1);
return Zotero[className] return Zotero[className]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -103,7 +103,7 @@ Zotero.ItemFields = new function() {
} }
if (typeof field == 'number') { if (typeof field == 'number') {
return field; return _fields[field] ? field : false;
} }
return _fields[field] ? _fields[field]['id'] : false; return _fields[field] ? _fields[field]['id'] : false;

View file

@ -27,17 +27,16 @@
/* /*
* Primary interface for accessing Zotero items * Primary interface for accessing Zotero items
*/ */
Zotero.Items = new function() { Zotero.Items = function() {
Zotero.DataObjects.apply(this, ['item']); this.constructor = null;
this.constructor.prototype = new Zotero.DataObjects();
// Privileged methods this._ZDO_object = 'item';
this.add = add;
this.getSortTitle = getSortTitle;
Object.defineProperty(this, "_primaryDataSQLParts", { // This needs to wait until all Zotero components are loaded to initialize,
// but otherwise it can be just a simple property
Zotero.defineProperty(this, "_primaryDataSQLParts", {
get: function () { get: function () {
return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = { return {
itemID: "O.itemID", itemID: "O.itemID",
itemTypeID: "O.itemTypeID", itemTypeID: "O.itemTypeID",
dateAdded: "O.dateAdded", dateAdded: "O.dateAdded",
@ -88,18 +87,17 @@ Zotero.Items = new function() {
attachmentContentType: "IA.contentType AS attachmentContentType", attachmentContentType: "IA.contentType AS attachmentContentType",
attachmentPath: "IA.path AS attachmentPath", attachmentPath: "IA.path AS attachmentPath",
attachmentSyncState: "IA.syncState AS attachmentSyncState" attachmentSyncState: "IA.syncState AS attachmentSyncState"
}); };
} }
}); }, {lazy: true});
// Private members
var _primaryDataSQLParts;
var _cachedFields = {};
var _firstCreatorSQL = '';
var _sortCreatorSQL = '';
var _emptyTrashIdleObserver = null;
var _emptyTrashTimer = null;
this._primaryDataSQLFrom = "FROM items O "
+ "LEFT JOIN itemAttachments IA USING (itemID) "
+ "LEFT JOIN items IAP ON (IA.parentItemID=IAP.itemID) "
+ "LEFT JOIN itemNotes INo ON (O.itemID=INo.itemID) "
+ "LEFT JOIN items INoP ON (INo.parentItemID=INoP.itemID) "
+ "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID)";
/** /**
* Return items marked as deleted * Return items marked as deleted
@ -215,77 +213,7 @@ Zotero.Items = new function() {
}; };
/* this._cachedFields = {};
* Create a new item with optional metadata and pass back the primary reference
*
* Using "var item = new Zotero.Item()" and "item.save()" directly results
* in an orphaned reference to the created item. If other code retrieves the
* new item with Zotero.Items.get() and modifies it, the original reference
* will not reflect the changes.
*
* Using this method avoids the need to call Zotero.Items.get() after save()
* in order to get the primary item reference. Since it accepts metadata
* as a JavaScript object, it also offers a simpler syntax than
* item.setField() and item.setCreator().
*
* Callers with no need for an up-to-date reference after save() (or who
* don't mind doing an extra Zotero.Items.get()) can use Zotero.Item
* directly if they prefer.
*
* Sample usage:
*
* var data = {
* title: "Shakespeare: The Invention of the Human",
* publisher: "Riverhead Hardcover",
* date: '1998-10-26',
* ISBN: 1573221201,
* pages: 745,
* creators: [
* ['Harold', 'Bloom', 'author']
* ]
* };
* var item = Zotero.Items.add('book', data);
*/
function add(itemTypeOrID, data) {
var item = new Zotero.Item(itemTypeOrID);
for (var field in data) {
if (field == 'creators') {
var i = 0;
for each(var creator in data.creators) {
// TODO: accept format from toArray()
var fields = {
firstName: creator[0],
lastName: creator[1],
fieldMode: creator[3] ? creator[3] : 0
};
var creatorDataID = Zotero.Creators.getDataID(fields);
if (creatorDataID) {
var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID);
// TODO: identical creators?
var creatorID = linkedCreators[0];
}
else {
var creatorObj = new Zotero.Creator;
creatorObj.setFields(fields);
var creatorID = creatorObj.save();
}
item.setCreator(i, Zotero.Creators.get(creatorID), creator[2]);
i++;
}
}
else {
item.setField(field, data[field]);
}
}
var id = item.save();
return this.getAsync(id);
}
this.cacheFields = Zotero.Promise.coroutine(function* (libraryID, fields, items) { this.cacheFields = Zotero.Promise.coroutine(function* (libraryID, fields, items) {
if (items && items.length == 0) { if (items && items.length == 0) {
return; return;
@ -315,14 +243,14 @@ Zotero.Items = new function() {
var fieldIDs = []; var fieldIDs = [];
for each(var field in fields) { for each(var field in fields) {
// Check if field already cached // Check if field already cached
if (_cachedFields[libraryID] && _cachedFields[libraryID].indexOf(field) != -1) { if (this._cachedFields[libraryID] && this._cachedFields[libraryID].indexOf(field) != -1) {
continue; continue;
} }
if (!_cachedFields[libraryID]) { if (!this._cachedFields[libraryID]) {
_cachedFields[libraryID] = []; this._cachedFields[libraryID] = [];
} }
_cachedFields[libraryID].push(field); this._cachedFields[libraryID].push(field);
if (this.isPrimaryField(field)) { if (this.isPrimaryField(field)) {
primaryFields.push(field); primaryFields.push(field);
@ -472,7 +400,7 @@ Zotero.Items = new function() {
for (let i=0; i<allItemIDs.length; i++) { for (let i=0; i<allItemIDs.length; i++) {
let itemID = allItemIDs[i]; let itemID = allItemIDs[i];
let item = this._objectCache[itemID]; let item = this._objectCache[itemID];
yield this._objectCache[itemID].loadDisplayTitle() yield item.loadDisplayTitle()
} }
} }
@ -497,7 +425,7 @@ Zotero.Items = new function() {
// Move child items to master // Move child items to master
var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true)); var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
for each(var id in ids) { for each(var id in ids) {
var attachment = yield Zotero.Items.getAsync(id); var attachment = yield this.getAsync(id);
// TODO: Skip identical children? // TODO: Skip identical children?
@ -549,7 +477,7 @@ Zotero.Items = new function() {
} }
yield item.save(); yield item.save();
}); }.bind(this));
}; };
@ -604,9 +532,11 @@ Zotero.Items = new function() {
/** /**
* Start idle observer to delete trashed items older than a certain number of days * Start idle observer to delete trashed items older than a certain number of days
*/ */
this._emptyTrashIdleObserver = null;
this._emptyTrashTimer = null;
this.startEmptyTrashTimer = function () { this.startEmptyTrashTimer = function () {
_emptyTrashIdleObserver = { this._emptyTrashIdleObserver = {
observe: function (subject, topic, data) { observe: (subject, topic, data) => {
if (topic == 'idle' || topic == 'timer-callback') { if (topic == 'idle' || topic == 'timer-callback') {
var days = Zotero.Prefs.get('trashAutoEmptyDays'); var days = Zotero.Prefs.get('trashAutoEmptyDays');
if (!days) { if (!days) {
@ -620,20 +550,20 @@ Zotero.Items = new function() {
// TODO: increase number after dealing with slow // TODO: increase number after dealing with slow
// tag.getLinkedItems() call during deletes // tag.getLinkedItems() call during deletes
var num = 10; var num = 10;
Zotero.Items.emptyTrash(null, days, num) this.emptyTrash(null, days, num)
.then(function (deleted) { .then(deleted => {
if (!deleted) { if (!deleted) {
_emptyTrashTimer = null; this._emptyTrashTimer = null;
return; return;
} }
// Set a timer to do more every few seconds // Set a timer to do more every few seconds
if (!_emptyTrashTimer) { if (!this._emptyTrashTimer) {
_emptyTrashTimer = Components.classes["@mozilla.org/timer;1"] this._emptyTrashTimer = Components.classes["@mozilla.org/timer;1"]
.createInstance(Components.interfaces.nsITimer); .createInstance(Components.interfaces.nsITimer);
} }
_emptyTrashTimer.init( this._emptyTrashTimer.init(
_emptyTrashIdleObserver.observe, this._emptyTrashIdleObserver.observe,
5 * 1000, 5 * 1000,
Components.interfaces.nsITimer.TYPE_ONE_SHOT Components.interfaces.nsITimer.TYPE_ONE_SHOT
); );
@ -641,8 +571,8 @@ Zotero.Items = new function() {
} }
// When no longer idle, cancel timer // When no longer idle, cancel timer
else if (topic == 'back') { else if (topic == 'back') {
if (_emptyTrashTimer) { if (this._emptyTrashTimer) {
_emptyTrashTimer.cancel(); this._emptyTrashTimer.cancel();
} }
} }
} }
@ -650,7 +580,7 @@ Zotero.Items = new function() {
var idleService = Components.classes["@mozilla.org/widget/idleservice;1"]. var idleService = Components.classes["@mozilla.org/widget/idleservice;1"].
getService(Components.interfaces.nsIIdleService); 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) { this._postLoad = function (libraryID, ids) {
if (!ids) { if (!ids) {
if (!_cachedFields[libraryID]) { if (!this._cachedFields[libraryID]) {
_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. * Why do we do this entirely in SQL? Because we're crazy. Crazy like foxes.
*/ */
var _firstCreatorSQL = '';
function _getFirstCreatorSQL() { function _getFirstCreatorSQL() {
if (_firstCreatorSQL) { if (_firstCreatorSQL) {
return _firstCreatorSQL; return _firstCreatorSQL;
@ -828,6 +743,7 @@ Zotero.Items = new function() {
/* /*
* Generate SQL to retrieve sortCreator field * Generate SQL to retrieve sortCreator field
*/ */
var _sortCreatorSQL = '';
function _getSortCreatorSQL() { function _getSortCreatorSQL() {
if (_sortCreatorSQL) { if (_sortCreatorSQL) {
return _sortCreatorSQL; return _sortCreatorSQL;
@ -947,7 +863,7 @@ Zotero.Items = new function() {
} }
function getSortTitle(title) { this.getSortTitle = function(title) {
if (title === false || title === undefined) { if (title === false || title === undefined) {
return ''; return '';
} }
@ -956,4 +872,8 @@ Zotero.Items = new function() {
} }
return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1') return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1')
} }
}
Zotero.DataObjects.call(this);
return this;
}.bind(Object.create(Zotero.DataObjects.prototype))();

View file

@ -28,7 +28,7 @@ Zotero.Libraries = new function () {
_userLibraryID, _userLibraryID,
_libraryDataLoaded = false; _libraryDataLoaded = false;
Zotero.Utilities.Internal.defineProperty(this, 'userLibraryID', { Zotero.defineProperty(this, 'userLibraryID', {
get: function() { get: function() {
if (!_libraryDataLoaded) { if (!_libraryDataLoaded) {
throw new Error("Library data not yet loaded"); 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()"); 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';
}
}

View file

@ -23,18 +23,15 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
*/ */
Zotero.Relations = new function () { Zotero.Relations = function () {
Zotero.DataObjects.apply(this, ['relation']); this.constructor = null;
this.constructor.prototype = new Zotero.DataObjects();
this.__defineGetter__('relatedItemPredicate', function () "dc:relation"); this._ZDO_object = 'relation';
this.__defineGetter__('linkedObjectPredicate', function () "owl:sameAs"); this._ZDO_idOnly = true;
this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy');
var _namespaces = { Zotero.defineProperty(this, 'relatedItemPredicate', {value: 'dc:relation'});
dc: 'http://purl.org/dc/elements/1.1/', Zotero.defineProperty(this, 'linkedObjectPredicate', {value: 'owl:sameAs'});
owl: 'http://www.w3.org/2002/07/owl#' Zotero.defineProperty(this, 'deletedItemPredicate', {value: 'dc:isReplacedBy'});
};
this.get = function (id) { this.get = function (id) {
if (typeof id != 'number') { if (typeof id != 'number') {
@ -52,7 +49,7 @@ Zotero.Relations = new function () {
*/ */
this.getByURIs = Zotero.Promise.coroutine(function* (subject, predicate, object) { this.getByURIs = Zotero.Promise.coroutine(function* (subject, predicate, object) {
if (predicate) { if (predicate) {
predicate = _getPrefixAndValue(predicate).join(':'); predicate = this._getPrefixAndValue(predicate).join(':');
} }
if (!subject && !predicate && !object) { if (!subject && !predicate && !object) {
@ -141,7 +138,7 @@ Zotero.Relations = new function () {
this.add = Zotero.Promise.coroutine(function* (libraryID, subject, predicate, object) { this.add = Zotero.Promise.coroutine(function* (libraryID, subject, predicate, object) {
predicate = _getPrefixAndValue(predicate).join(':'); predicate = this._getPrefixAndValue(predicate).join(':');
var relation = new Zotero.Relation; var relation = new Zotero.Relation;
if (!libraryID) { if (!libraryID) {
@ -272,11 +269,15 @@ Zotero.Relations = new function () {
return relation; 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(':'); var [prefix, value] = uri.split(':');
if (prefix && value) { if (prefix && value) {
if (!_namespaces[prefix]) { if (!this._namespaces[prefix]) {
throw ("Invalid prefix '" + prefix + "' in Zotero.Relations._getPrefixAndValue()"); throw ("Invalid prefix '" + prefix + "' in Zotero.Relations._getPrefixAndValue()");
} }
return [prefix, value]; return [prefix, value];
@ -290,4 +291,8 @@ Zotero.Relations = new function () {
} }
throw ("Invalid namespace in URI '" + uri + "' in Zotero.Relations._getPrefixAndValue()"); throw ("Invalid namespace in URI '" + uri + "' in Zotero.Relations._getPrefixAndValue()");
} }
}
Zotero.DataObjects.call(this);
return this;
}.bind(Object.create(Zotero.DataObjects.prototype))();

View file

@ -37,13 +37,10 @@ Zotero.Search = function() {
this._hasPrimaryConditions = false; this._hasPrimaryConditions = false;
} }
Zotero.Search._super = Zotero.DataObject; Zotero.extendClass(Zotero.DataObject, Zotero.Search);
Zotero.Search.prototype = Object.create(Zotero.Search._super.prototype);
Zotero.Search.constructor = Zotero.Search;
Zotero.Search.prototype._objectType = 'search'; Zotero.Search.prototype._objectType = 'search';
Zotero.Search.prototype._dataTypes = Zotero.Search._super.prototype._dataTypes.concat([ Zotero.Search.prototype._dataTypes = Zotero.Search._super.prototype._dataTypes.concat([
'primaryData',
'conditions' 'conditions'
]); ]);
@ -62,21 +59,33 @@ Zotero.Search.prototype.setName = function(val) {
this.name = val; this.name = val;
} }
Zotero.defineProperty(Zotero.Search.prototype, 'id', {
Zotero.Search.prototype.__defineGetter__('id', function () { return this._get('id'); }); get: function() this._get('id'),
Zotero.Search.prototype.__defineSetter__('id', function (val) { this._set('id', val); }); set: 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.defineProperty(Zotero.Search.prototype, 'libraryID', {
Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); }); get: function() this._get('libraryID'),
Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val) }); set: function(val) this._set('libraryID', val)
Zotero.Search.prototype.__defineGetter__('name', function () { return this._get('name'); }); });
Zotero.Search.prototype.__defineSetter__('name', function (val) { this._set('name', val); }); Zotero.defineProperty(Zotero.Search.prototype, 'key', {
Zotero.Search.prototype.__defineGetter__('version', function () { return this._get('version'); }); get: function() this._get('key'),
Zotero.Search.prototype.__defineSetter__('version', function (val) { this._set('version', val); }); set: function(val) this._set('key', val)
Zotero.Search.prototype.__defineGetter__('synced', function () { return this._get('synced'); }); });
Zotero.Search.prototype.__defineSetter__('synced', function (val) { this._set('synced', val); }); Zotero.defineProperty(Zotero.Search.prototype, 'name', {
get: function() this._get('name'),
Zotero.Search.prototype.__defineGetter__('conditions', function (arr) { this.getSearchConditions(); }); 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) { Zotero.Search.prototype._set = function (field, value) {
if (field == 'id' || field == 'libraryID' || field == 'key') { if (field == 'id' || field == 'libraryID' || field == 'key') {
@ -161,152 +170,115 @@ Zotero.Search.prototype.loadFromRow = function (row) {
this._identified = true; 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);
});
/* Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
* Save the search to the DB and return a savedSearchID var fixGaps = env.options.fixGaps;
* var isNew = env.isNew;
* If there are gaps in the searchConditionIDs, |fixGaps| must be true
* and the caller must dispose of the search or reload the condition ids, var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches');
* which may change after the save. var libraryID = env.libraryID = this.libraryID;
* var key = env.key = this._key = this.key ? this.key : this._generateKey();
* For new searches, name must be set called before saving
*/ var columns = [
Zotero.Search.prototype.save = Zotero.Promise.coroutine(function* (fixGaps) { 'savedSearchID',
try { 'savedSearchName',
Zotero.Searches.editCheck(this); 'clientDateModified',
'libraryID',
if (!this.name) { 'key',
throw('Name not provided for saved search'); 'version',
} 'synced'
];
var isNew = !this.id; var placeholders = columns.map(function () '?').join();
var sqlValues = [
// Register this item's identifiers in Zotero.DataObjects on transaction commit, searchID ? { int: searchID } : null,
// before other callbacks run { string: this.name },
var searchID, libraryID, key; Zotero.DB.transactionDateTime,
if (isNew) { this.libraryID ? this.libraryID : 0,
var transactionOptions = { key,
onCommit: function () { this.version ? this.version : 0,
Zotero.Searches.registerIdentifiers(searchID, libraryID, key); this.synced ? 1 : 0
} ];
};
} var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") "
else { + "VALUES (" + placeholders + ")";
var transactionOptions = null; var insertID = yield Zotero.DB.queryAsync(sql, sqlValues);
} if (!searchID) {
searchID = env.id = insertID;
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);
} }
catch (e) {
try { if (!isNew) {
yield this.reload(); var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
this._clearChanged(); yield Zotero.DB.queryAsync(sql, this.id);
}
catch (e2) {
Zotero.debug(e2, 1);
}
Zotero.debug(e, 1);
throw e;
} }
// 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 objLibraryID;
let objKey = condition.value; let objKey = condition.value;
let objectType = condition.name == 'collection' ? 'collection' : 'search'; let objectType = condition.name == 'collection' ? 'collection' : 'search';
let objectTypeClass = Zotero.DataObjectUtilities.getClassForObjectType(objectType); let objectTypeClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
// Old-style library-key hash // Old-style library-key hash
if (objKey.contains('_')) { if (objKey.contains('_')) {
@ -1665,29 +1637,25 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
this._sqlParams = sqlParams.length ? sqlParams : false; this._sqlParams = sqlParams.length ? sqlParams : false;
}); });
Zotero.Searches = new function(){ Zotero.Searches = function() {
Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']); this.constructor = null;
this.constructor.prototype = new Zotero.DataObjects();
Object.defineProperty(this, "_primaryDataSQLParts", { this._ZDO_object = 'search';
get: function () { this._ZDO_id = 'savedSearch';
return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = { this._ZDO_table = 'savedSearches';
savedSearchID: "O.savedSearchID",
name: "O.savedSearchName",
libraryID: "O.libraryID",
key: "O.key",
version: "O.version",
synced: "O.synced"
});
}
});
this._primaryDataSQLParts = {
var _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* () { this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this); yield Zotero.DataObjects.prototype.init.apply(this);
yield Zotero.SearchConditions.init(); yield Zotero.SearchConditions.init();
}); });
@ -1735,6 +1703,8 @@ Zotero.Searches = new function(){
let id = ids[i]; let id = ids[i];
var search = new Zotero.Search; var search = new Zotero.Search;
search.id = id; search.id = id;
yield search.loadPrimaryData();
yield search.loadConditions();
notifierData[id] = { old: search.serialize() }; notifierData[id] = { old: search.serialize() };
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; 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(", ") + " " + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " "
+ "FROM savedSearches O WHERE 1"; + "FROM savedSearches O WHERE 1";
} }
}
Zotero.DataObjects.call(this);
return this;
}.bind(Object.create(Zotero.DataObjects.prototype))();

View file

@ -493,24 +493,6 @@ Zotero.Utilities.Internal = {
}, 0, 0, null); }, 0, 0, null);
return pipe.inputStream; 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);
} }
} }

View file

@ -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 * 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 * 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 * Allow other events (e.g., UI updates) on main thread to be processed if necessary
* *

View file

@ -1971,6 +1971,7 @@ var ZoteroPane = new function()
} }
var self = this; var self = this;
var deferred = Zotero.Promise.defer();
this.collectionsView.addEventListener('load', function () { this.collectionsView.addEventListener('load', function () {
Zotero.spawn(function* () { Zotero.spawn(function* () {
var currentLibraryID = self.getSelectedLibraryID(); var currentLibraryID = self.getSelectedLibraryID();
@ -1993,15 +1994,22 @@ var ZoteroPane = new function()
yield self.collectionsView.selectLibrary(item.libraryID); yield self.collectionsView.selectLibrary(item.libraryID);
yield self.itemsView.selectItem(itemID, expand); yield self.itemsView.selectItem(itemID, expand);
} }
deferred.resolve(true);
})
.catch(function(e) {
deferred.reject(e);
}); });
}); });
})
.catch(function(e) {
deferred.reject(e);
}); });
}); });
// open Zotero pane // open Zotero pane
this.show(); this.show();
return true; return deferred.promise;
}); });