commit 595f775c398d42904399e0a0c1156862873a83d4
parent 2f858bd39fb1c6f85c3a609b071adab8b40ccb36
Author: Dan Stillman <dstillman@zotero.org>
Date: Tue, 12 Aug 2008 07:10:50 +0000
Improved subcollection support -- fixes "Cannot set parent of collection [x] to invalid parent [x]" error, among other things
Removed child collections from XML -- now uses parent attribute exclusively -- and increased API version to 2
Diffstat:
3 files changed, 163 insertions(+), 56 deletions(-)
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -169,16 +169,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
var madeChanges = false;
- if (action == 'refresh') {
- switch (type) {
- case 'share':
- this.reload();
- this.rememberSelection(savedSelection);
- break;
- }
- }
-
- else if(action == 'delete') {
+ if (action == 'delete') {
//Since a delete involves shifting of rows, we have to do it in order
//sort the ids by row
@@ -222,8 +213,9 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
for (var i=0; i<ids.length; i++) {
// Open the parent collection if closed
var collection = Zotero.Collections.get(ids[i]);
- var parentID = collection.getParent();
- if (parentID && !this.isContainerOpen(this._collectionRowMap[parentID])) {
+ var parentID = collection.parent;
+ if (parentID && this._collectionRowMap[parentID] &&
+ !this.isContainerOpen(this._collectionRowMap[parentID])) {
this.toggleOpenState(this._collectionRowMap[parentID]);
}
}
@@ -231,8 +223,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
this.reload();
this.rememberSelection(savedSelection);
}
- else if(action == 'modify')
- {
+ else if (action == 'modify' || action == 'refresh') {
this.reload();
this.rememberSelection(savedSelection);
}
@@ -550,6 +541,10 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
*/
Zotero.CollectionTreeView.prototype.rememberSelection = function(selection)
{
+ if (!selection) {
+ return;
+ }
+
var id = selection.substr(1);
switch (selection.substr(0, 1)) {
// Library
diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js
@@ -37,7 +37,7 @@ Zotero.Collection.prototype._init = function () {
this._changed = false;
this._previousData = false;
- this._hasChildCollections = false;
+ this._hasChildCollections;
this._childCollections = [];
this._childCollectionsLoaded = false;
@@ -59,7 +59,7 @@ Zotero.Collection.prototype.__defineSetter__('dateModified', function (val) { th
Zotero.Collection.prototype.__defineGetter__('key', function () { return this._get('key'); });
Zotero.Collection.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
-Zotero.Collection.prototype.__defineSetter__('childCollections', function (arr) { this._setChildCollections(arr); });
+//Zotero.Collection.prototype.__defineSetter__('childCollections', function (arr) { this._setChildCollections(arr); });
Zotero.Collection.prototype.__defineSetter__('childItems', function (arr) { this._setChildItems(arr); });
@@ -150,8 +150,8 @@ Zotero.Collection.prototype.loadFromRow = function(row) {
this._parent = row.parentCollectionID;
this._dateModified = row.dateModified;
this._key = row.key;
- this._hasChildCollections = row.hasChildCollections;
- this._hasChildItems = row.hasChildItems;
+ this._hasChildCollections = !!row.hasChildCollections;
+ this._hasChildItems = !!row.hasChildItems;
this._loadChildItems();
}
@@ -161,13 +161,29 @@ Zotero.Collection.prototype.isEmpty = function() {
}
Zotero.Collection.prototype.hasChildCollections = function() {
- return !!(parseInt(this._hasChildCollections));
+ if (!this.id) {
+ throw ("Zotero.Collection.hasChildCollections cannot be called "
+ + "on an unsaved collection");
+ }
+
+ if (this._hasChildCollections == undefined) {
+ var sql = "SELECT COUNT(*) FROM collections WHERE "
+ + "parentCollectionID=?";
+ this._hasChildCollections = !!Zotero.DB.valueQuery(sql, this.id);
+ }
+
+ return this._hasChildCollections;
}
Zotero.Collection.prototype.hasChildItems = function() {
return !!(parseInt(this._hasChildItems));
}
+Zotero.Collection.prototype.refreshChildCollections = function () {
+ this._hasChildCollections = undefined;
+ this._childCollectionsLoaded = false;
+}
+
/**
* Check if collection exists in the database
@@ -272,7 +288,7 @@ Zotero.Collection.prototype.save = function () {
throw ('Cannot move collection into itself!');
}
- if (this.hasDescendent('collection', this.parent)) {
+ if (this.id && this.hasDescendent('collection', this.parent)) {
throw ('Cannot move collection into one of its own descendents!', 2);
}
}
@@ -339,6 +355,21 @@ Zotero.Collection.prototype.save = function () {
collectionID = insertID;
}
+
+ if (this._changed.parent) {
+ var parentIDs = [];
+ if (this._previousData.parent) {
+ parentIDs.push(this._previousData.parent);
+ }
+ if (this.parent) {
+ parentIDs.push(this.parent);
+ }
+
+ Zotero.Notifier.trigger('move', 'collection', this.id);
+ }
+
+
+ /*
// Subcollections
if (this._changed.childCollections) {
var removed = [];
@@ -381,6 +412,7 @@ Zotero.Collection.prototype.save = function () {
// TODO: notifier
}
+ */
// Child items
if (this._changed.childItems) {
@@ -469,7 +501,7 @@ Zotero.Collection.prototype.save = function () {
this._key = key;
}
- Zotero.Collections.reloadAll();
+ Zotero.Collections.reload(this.id);
if (isNew) {
Zotero.Notifier.trigger('add', 'collection', this.id);
@@ -478,15 +510,12 @@ Zotero.Collection.prototype.save = function () {
Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData);
}
- if (this._changed.parent) {
- var notifyIDs = [this.id];
- if (this._previousData.parent) {
- notifyIDs.push(this._previousData.parent);
- }
- if (this.parent) {
- notifyIDs.push(this.parent);
+ // Refresh child collection counts
+ if (parentIDs) {
+ for each(var id in parentIDs) {
+ var col = Zotero.Collections.get(id);
+ col.refreshChildCollections();
}
- //Zotero.Notifier.trigger('move', 'collection', notifyIDs, notifierData);
}
return this.id;
@@ -701,7 +730,7 @@ Zotero.Collection.prototype.serialize = function(nested) {
parent: this.parent,
childCollections: this.getChildCollections(true),
childItems: this.getChildItems(true),
- descendents: this.getDescendents(nested)
+ descendents: this.id ? this.getDescendents(nested) : []
};
return obj;
}
@@ -709,15 +738,20 @@ Zotero.Collection.prototype.serialize = function(nested) {
/**
* Returns an array of descendent collections and items
- * (rows of 'id', 'type' ('item' or 'collection'), 'parent', and,
- * if collection, 'name' and the nesting 'level')
*
* @param bool recursive Descend into subcollections
* @param bool nested Return multidimensional array with 'children'
* nodes instead of flat array
* @param string type 'item', 'collection', or FALSE for both
+ * @return {Object[]} Array of objects with 'id',
+ * 'type' ('item' or 'collection'), 'parent',
+ * and, if collection, 'name' and the nesting 'level'
*/
Zotero.Collection.prototype.getChildren = function(recursive, nested, type, level) {
+ if (!this.id) {
+ throw ('Zotero.Collection.getChildren() cannot be called on an unsaved item');
+ }
+
var toReturn = [];
if (!level) {
@@ -813,9 +847,11 @@ Zotero.Collection.prototype._prepFieldChange = function (field) {
}
+/*
Zotero.Collection.prototype._setChildCollections = function (collectionIDs) {
this._setChildren('collection', collectionIDs);
}
+*/
Zotero.Collection.prototype._setChildItems = function (itemIDs) {
@@ -889,15 +925,18 @@ Zotero.Collection.prototype._setChildren = function (type, ids) {
this._childItems = Zotero.Items.get(newIDs);
}
else {
- for (var id in newIDs) {
- this['_child' + Types].push(Zotero[Types].get(id));
+ for each(var id in newIDs) {
+ var obj = Zotero[Types].get(id);
+ if (!obj) {
+ throw (type + ' ' + id + ' not found in Zotero.Collection._setChildren()');
+ }
+ this['_child' + Types].push(obj);
}
}
return true;
}
-
Zotero.Collection.prototype._loadChildCollections = function () {
var sql = "SELECT collectionID FROM collections WHERE parentCollectionID=?";
var ids = Zotero.DB.columnQuery(sql, this.id);
@@ -906,8 +945,16 @@ Zotero.Collection.prototype._loadChildCollections = function () {
if (ids) {
for each(var id in ids) {
- this._childCollections.push(Zotero.Collections.get(id));
+ var col = Zotero.Collections.get(id);
+ if (!col) {
+ throw ('Collection ' + id + ' not found in Zotero.Collection._loadChildCollections()');
+ }
+ this._childCollections.push(col);
}
+ this._hasChildCollections = true;
+ }
+ else {
+ this._hasChildCollections = false;
}
this._childCollectionsLoaded = true;
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -480,7 +480,7 @@ Zotero.Sync.Server = new function () {
});
this.nextLocalSyncDate = false;
- this.apiVersion = 1;
+ this.apiVersion = 2;
default xml namespace = '';
@@ -1169,6 +1169,7 @@ Zotero.BufferedInputListener.prototype = {
}
+// TODO: use prototype
Zotero.Sync.Server.EventListener = {
init: function () {
Zotero.Notifier.registerObserver(this);
@@ -1207,12 +1208,82 @@ Zotero.Sync.Server.Data = new function() {
default xml namespace = '';
+
+ /**
+ * Reorder XML nodes for parent/child relationships, etc.
+ *
+ * @param {E4X} xml
+ */
+ function _preprocessUpdatedXML(xml) {
+ if (xml.collections.length()) {
+ var collections = xml.collections.children();
+ var orderedCollections = <collections/>;
+ var collectionIDHash = {};
+
+ for (var i=0; i<collections.length(); i++) {
+ // Build a hash of all collection ids
+ collectionIDHash[collections[i].@id.toString()] = true;
+
+ // Pull out top-level collections
+ if (!collections[i].@parent.toString()) {
+ orderedCollections.collection += collections[i];
+ delete collections[i];
+ i--;
+ }
+ }
+
+ // Pull out all collections pointing to parents that
+ // aren't present, which we assume already exist
+ for (var i=0; i<collections.length(); i++) {
+ if (!collectionIDHash[collections[i].@parent]) {
+ orderedCollections.collection += collections[i]
+ delete collections[i];
+ i--;
+ }
+ }
+
+ // Insert children directly under parents
+ for (var i=0; i<orderedCollections.children().length(); i++) {
+ for (var j=0; j<collections.length(); j++) {
+ if (collections[j].@parent.toString() ==
+ orderedCollections.children()[i].@id.toString()) {
+ // Make a clone of object, since otherwise
+ // delete below erases inserted item as well
+ // (which only seems to happen with
+ // insertChildBefore(), not += above)
+ var newChild = new XML(collections[j].toXMLString())
+
+ // If last top-level, just append
+ if (i == orderedCollections.children().length() - 1) {
+ orderedCollections.appendChild(newChild);
+ }
+ else {
+ orderedCollections.insertChildBefore(
+ orderedCollections.children()[i+1],
+ newChild
+ );
+ }
+ delete collections[j];
+ j--;
+ }
+ }
+ }
+
+ xml.collections = orderedCollections;
+ }
+
+ return xml;
+ }
+
+
function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) {
if (xml.children().length() == 0) {
Zotero.debug('No changes received from server');
return Zotero.Sync.Server.Data.buildUploadXML(uploadIDs);
}
+ xml = _preprocessUpdatedXML(xml);
+
var remoteCreatorStore = {};
var relatedItemsStore = {};
@@ -1564,27 +1635,13 @@ Zotero.Sync.Server.Data = new function() {
}
}
+ /*
if (type == 'collection') {
- // Sort collections in order of parent collections,
- // so referenced parent collections always exist when saving
- var cmp = function (a, b) {
- var pA = a.parent;
- var pB = b.parent;
- if (pA == pB) {
- return 0;
- }
- return (pA < pB) ? -1 : 1;
- };
- toSaveParents.sort(cmp);
-
// Temporarily remove and store subcollections before saving
// since referenced collections may not exist yet
var collections = [];
for each(var obj in toSaveParents) {
var colIDs = obj.getChildCollections(true);
- if (!colIDs.length) {
- continue;
- }
// TODO: use exist(), like related items above
obj.childCollections = [];
collections.push({
@@ -1593,6 +1650,7 @@ Zotero.Sync.Server.Data = new function() {
});
}
}
+ */
// Save objects
Zotero.debug('Saving merged ' + types);
@@ -1613,15 +1671,17 @@ Zotero.Sync.Server.Data = new function() {
item.save();
}
}
+ /*
// Add back subcollections
else if (type == 'collection') {
for each(var collection in collections) {
- if (collection.collections) {
- collection.obj.childCollections = collection.collections;
+ if (collection.childCollections) {
+ collection.obj.childCollections = collection.childCollections;
collection.obj.save();
}
}
}
+ */
// Delete
@@ -1961,21 +2021,24 @@ Zotero.Sync.Server.Data = new function() {
var children = collection.getChildren();
if (children) {
- xml.collections = '';
+ //xml.collections = '';
xml.items = '';
for each(var child in children) {
+ /*
if (child.type == 'collection') {
xml.collections = xml.collections ?
xml.collections + ' ' + child.id : child.id;
}
- else if (child.type == 'item') {
+ else */if (child.type == 'item') {
xml.items = xml.items ?
xml.items + ' ' + child.id : child.id;
}
}
+ /*
if (xml.collections == '') {
delete xml.collections;
}
+ */
if (xml.items == '') {
delete xml.items;
}
@@ -2021,9 +2084,11 @@ Zotero.Sync.Server.Data = new function() {
collection.key = xmlCollection.@key.toString();
}
+ /*
// Subcollections
var str = xmlCollection.collections.toString();
collection.childCollections = str == '' ? [] : str.split(' ');
+ */
// Child items
var str = xmlCollection.items.toString();