From 33de40ad9527efaf712c609dc9c6a7e89acb5bb4 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Wed, 25 Jun 2008 00:26:55 +0000 Subject: [PATCH] Adds sync support for related items Might fix (or break) other stuff, but who remembers? --- chrome/content/zotero/bindings/noteeditor.xml | 8 +- chrome/content/zotero/bindings/relatedbox.xml | 72 ++- chrome/content/zotero/xpcom/data/item.js | 510 +++++++++++++----- chrome/content/zotero/xpcom/data/items.js | 9 + chrome/content/zotero/xpcom/data/tag.js | 116 ++-- chrome/content/zotero/xpcom/sync.js | 205 ++++--- chrome/content/zotero/xpcom/translate.js | 6 +- 7 files changed, 630 insertions(+), 296 deletions(-) diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml index e5b93a923..cbdff9ae2 100644 --- a/chrome/content/zotero/bindings/noteeditor.xml +++ b/chrome/content/zotero/bindings/noteeditor.xml @@ -407,11 +407,13 @@ 0) + var relatedList = this.item.relatedItemsBidirectional; + if (relatedList.length > 0) { this.id('seeAlsoPopup').showPopup(this.id('seeAlsoLabel'),-1,-1,'popup',0,0); - else + } + else { this.id('seeAlso').add(); + } ]]> diff --git a/chrome/content/zotero/bindings/relatedbox.xml b/chrome/content/zotero/bindings/relatedbox.xml index b2a5d9c10..fe7c6ade4 100644 --- a/chrome/content/zotero/bindings/relatedbox.xml +++ b/chrome/content/zotero/bindings/relatedbox.xml @@ -42,15 +42,12 @@ @@ -156,12 +152,15 @@ @@ -206,13 +205,8 @@ 0 || oldIDs.length != currentIDs.length) { + this._prepFieldChange('relatedItems'); + } + else { + Zotero.debug('Related items not changed in Zotero.Item._setRelatedItems()', 4); + return false; + } + + newIDs = oldIDs.concat(newIDs); + this._relatedItems = []; + for each(var itemID in newIDs) { + this._relatedItems.push(Zotero.Items.get(itemID)); + } + return true; +} + + +// TODO: use for stuff other than related items +Zotero.Item.prototype._prepFieldChange = function (field) { + if (!this._changed) { + this._changed = {}; + } + this._changed[field] = true; + + // Save a copy of the data before changing + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } +} + + Zotero.Item.prototype._generateKey = function () { return Zotero.ID.getKey(); } diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 59bddec49..6066c23bd 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -27,6 +27,7 @@ Zotero.Items = new function() { // Privileged methods this.get = get; + this.exist = exist; this.getAll = getAll; this.getUpdated = getUpdated; this.add = add; @@ -100,6 +101,14 @@ Zotero.Items = new function() { } + function exist(itemIDs) { + var sql = "SELECT itemID FROM items WHERE itemID IN (" + + itemIDs.map(function () '?').join() + ")"; + var exist = Zotero.DB.columnQuery(sql, itemIDs); + return exist ? exist : []; + } + + /* * Returns all items in the database * diff --git a/chrome/content/zotero/xpcom/data/tag.js b/chrome/content/zotero/xpcom/data/tag.js index cd156bec0..aac987508 100644 --- a/chrome/content/zotero/xpcom/data/tag.js +++ b/chrome/content/zotero/xpcom/data/tag.js @@ -167,64 +167,6 @@ Zotero.Tag.prototype.getLinkedItems = function (asIDs) { } -Zotero.Tag.prototype._setLinkedItems = function (itemIDs) { - if (!this._linkedItemsLoaded) { - this._loadLinkedItems(); - } - - if (itemIDs.constructor.name != 'Array') { - throw ('ids must be an array in Zotero.Tag._setLinkedItems()'); - } - - var currentIDs = this.getLinkedItems(true); - if (!currentIDs) { - currentIDs = []; - } - var oldIDs = []; // children being kept - var newIDs = []; // new children - - if (itemIDs.length == 0) { - if (currentIDs.length == 0) { - Zotero.debug('No linked items added', 4); - return false; - } - } - else { - for (var i in itemIDs) { - var id = parseInt(itemIDs[i]); - if (isNaN(id)) { - throw ("Invalid itemID '" + itemIDs[i] - + "' in Zotero.Tag._setLinkedItems()"); - } - - if (currentIDs.indexOf(id) != -1) { - Zotero.debug("Item " + itemIDs[i] - + " is already linked to tag " + this.id); - oldIDs.push(id); - continue; - } - - newIDs.push(id); - } - } - - // Mark as changed if new or removed ids - if (newIDs.length > 0 || oldIDs.length != currentIDs.length) { - this._prepFieldChange('linkedItems'); - } - else { - Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4); - return false; - } - - newIDs = oldIDs.concat(newIDs); - - var items = Zotero.Items.get(itemIDs); - this._linkedItems = items ? items : []; - return true; -} - - Zotero.Tag.prototype.addItem = function (itemID) { var current = this.getLinkedItems(true); if (current && current.indexOf(itemID) != -1) { @@ -524,6 +466,64 @@ Zotero.Tag.prototype._loadLinkedItems = function() { } +Zotero.Tag.prototype._setLinkedItems = function (itemIDs) { + if (!this._linkedItemsLoaded) { + this._loadLinkedItems(); + } + + if (itemIDs.constructor.name != 'Array') { + throw ('ids must be an array in Zotero.Tag._setLinkedItems()'); + } + + var currentIDs = this.getLinkedItems(true); + if (!currentIDs) { + currentIDs = []; + } + var oldIDs = []; // children being kept + var newIDs = []; // new children + + if (itemIDs.length == 0) { + if (currentIDs.length == 0) { + Zotero.debug('No linked items added', 4); + return false; + } + } + else { + for (var i in itemIDs) { + var id = parseInt(itemIDs[i]); + if (isNaN(id)) { + throw ("Invalid itemID '" + itemIDs[i] + + "' in Zotero.Tag._setLinkedItems()"); + } + + if (currentIDs.indexOf(id) != -1) { + Zotero.debug("Item " + itemIDs[i] + + " is already linked to tag " + this.id); + oldIDs.push(id); + continue; + } + + newIDs.push(id); + } + } + + // Mark as changed if new or removed ids + if (newIDs.length > 0 || oldIDs.length != currentIDs.length) { + this._prepFieldChange('linkedItems'); + } + else { + Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4); + return false; + } + + newIDs = oldIDs.concat(newIDs); + + var items = Zotero.Items.get(itemIDs); + this._linkedItems = items ? items : []; + return true; +} + + Zotero.Tag.prototype._prepFieldChange = function (field) { if (!this._changed) { this._changed = {}; diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js index b60a61544..2b78d996f 100644 --- a/chrome/content/zotero/xpcom/sync.js +++ b/chrome/content/zotero/xpcom/sync.js @@ -1067,6 +1067,7 @@ Zotero.Sync.Server.Data = new function() { this.buildUploadXML = buildUploadXML; this.itemToXML = itemToXML; this.xmlToItem = xmlToItem; + this.removeMissingRelatedItems = removeMissingRelatedItems; this.collectionToXML = collectionToXML; this.xmlToCollection = xmlToCollection; this.creatorToXML = creatorToXML; @@ -1087,6 +1088,7 @@ Zotero.Sync.Server.Data = new function() { } var remoteCreatorStore = {}; + var relatedItemsStore = {}; Zotero.DB.beginTransaction(); @@ -1100,21 +1102,23 @@ Zotero.Sync.Server.Data = new function() { continue; } - Zotero.debug("Processing remotely changed " + types); - var toSaveParents = []; var toSaveChildren = []; var toDeleteParents = []; var toDeleteChildren = []; var toReconcile = []; + // + // Handle modified objects + // + Zotero.debug("Processing remotely changed " + types); + typeloop: for each(var xmlNode in xml[types][type]) { + var localDelete = false; + // Get local object with same id var obj = Zotero[Types].get(parseInt(xmlNode.@id)); - - // TODO: check local deleted items for possible conflict - if (obj) { // Key match -- same item if (obj.key == xmlNode.@key.toString()) { @@ -1130,6 +1134,24 @@ Zotero.Sync.Server.Data = new function() { // linked to a creator whose id changed) || uploadIDs.updated[types].indexOf(obj.id) != -1) { + // Merge and store related items, since CR doesn't + // affect related items + if (type == 'item') { + // TODO: skip conflict if only related items changed + + var related = xmlNode.related.toString(); + related = related ? related.split(' ') : []; + for each(var relID in obj.relatedItems) { + if (related.indexOf(relID) == -1) { + related.push(relID); + } + } + if (related.length) { + relatedItemsStore[obj.id] = related; + } + Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode); + } + var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); // Some types we don't bother to reconcile @@ -1138,24 +1160,31 @@ Zotero.Sync.Server.Data = new function() { Zotero.Sync.addToUpdated(uploadIDs.updated.items, obj.id); continue; } - else { - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); - } + + // Overwrite local below } // Mark other types for conflict resolution else { - /* - // For now, show item conflicts even if only - // dateModified changed, since we need to handle - // creator conflicts there - if (type != 'item') { - // Skip if only dateModified changed + // Skip item if dateModified is the only modified + // field (and no linked creators changed) + if (type == 'item') { var diff = obj.diff(remoteObj, false, true); if (!diff) { - continue; + // Check if creators changed + var creatorsChanged = false; + var creators = obj.getCreators(); + creators = creators.concat(remoteObj.getCreators()); + for each(var creator in creators) { + if (remoteCreatorStore[obj.id]) { + creatorsChanged = true; + break; + } + } + if (!creatorsChanged) { + continue; + } } } - */ // Will be handled by item CR for now if (type == 'creator') { @@ -1178,17 +1207,14 @@ Zotero.Sync.Server.Data = new function() { continue; } } - // Local object hasn't been modified -- overwrite - else { - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); - } + + // Overwrite local below } // Key mismatch -- different objects with same id, // so change id of local object else { var oldID = parseInt(xmlNode.@id); - var newID = Zotero.ID.get(types, true); Zotero.debug("Changing " + type + " " + oldID + " id to " + newID); @@ -1222,6 +1248,9 @@ Zotero.Sync.Server.Data = new function() { // Add items linked to creators to updated array, // since their timestamps will be set to the // transaction timestamp + // + // Note: Don't need to change collection children or + // related items, since they're stored as objects if (type == 'creator') { var linkedItems = obj.getLinkedItems(); if (linkedItems) { @@ -1229,22 +1258,18 @@ Zotero.Sync.Server.Data = new function() { } } - - // Note: Don't need to change collection children - // since they're stored as objects - uploadIDs.changed[types][oldID] = { oldID: oldID, newID: newID }; - // Process new item - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + obj = null; } } - // Object doesn't exist + + // Object doesn't exist locally else { - // Reconcile locally deleted objects + // Check if object has been deleted locally for each(var pair in uploadIDs.deleted[types]) { if (pair.id != parseInt(xmlNode.@id) || pair.key != xmlNode.@key.toString()) { @@ -1258,24 +1283,31 @@ Zotero.Sync.Server.Data = new function() { throw ('Delete reconciliation unimplemented for ' + types); } - var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); - - // TODO: order reconcile by parent/child? - - toReconcile.push([ - 'deleted', - remoteObj - ]); - - continue typeloop; + localDelete = true; } - - // Create locally - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); } + // Temporarily remove and store related items that don't yet exist + if (type == 'item') { + var missing = Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode); + if (missing.length) { + relatedItemsStore[xmlNode.@id] = missing; + } + } + + // Create or overwrite locally + obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); + + if (localDelete) { + // TODO: order reconcile by parent/child? + + toReconcile.push([ + 'deleted', + obj + ]); + } // Child items have to be saved after parent items - if (type == 'item' && obj.getSource()) { + else if (type == 'item' && obj.getSource()) { toSaveChildren.push(obj); } else { @@ -1348,6 +1380,7 @@ Zotero.Sync.Server.Data = new function() { for each(var obj in io.dataOut) { // TODO: do we need to make sure item isn't already being saved? + // Handle items deleted during merge if (obj.ref == 'deleted') { // Deleted item was remote if (obj.left != 'deleted') { @@ -1358,6 +1391,10 @@ Zotero.Sync.Server.Data = new function() { toDeleteChildren.push(obj.id); } + if (relatedItemsStore[obj.id]) { + delete relatedItemsStore[obj.id]; + } + uploadIDs.deleted[types].push({ id: obj.id, key: obj.left.key @@ -1394,11 +1431,9 @@ Zotero.Sync.Server.Data = new function() { } } - // Sort collections in order of parent collections, - // so referenced parent collections always exist when saving if (type == 'collection') { - var collections = []; - + // 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; @@ -1408,35 +1443,45 @@ Zotero.Sync.Server.Data = new function() { return (pA < pB) ? -1 : 1; }; toSaveParents.sort(cmp); - } - - Zotero.debug('Saving merged ' + types); - for each(var obj in toSaveParents) { - // If collection, temporarily clear subcollections before - // saving since referenced collections may not exist yet - if (type == 'collection') { - var childCollections = obj.getChildCollections(true); - if (childCollections) { - obj.childCollections = []; + + // 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; } - } - - var id = obj.save(); - - // Store subcollections - if (type == 'collection') { + // TODO: use exist(), like related items above + obj.childCollections = []; collections.push({ obj: obj, - childCollections: childCollections + childCollections: colIDs }); } } + + // Save objects + Zotero.debug('Saving merged ' + types); + for each(var obj in toSaveParents) { + obj.save(); + } for each(var obj in toSaveChildren) { obj.save(); } - // Set subcollections - if (type == 'collection') { + // Add back related items (which now exist) + if (type == 'item') { + for (var itemID in relatedItemsStore) { + item = Zotero.Items.get(itemID); + for each(var id in relatedItemsStore[itemID]) { + item.addRelatedItem(id); + } + item.save(); + } + } + // Add back subcollections + else if (type == 'collection') { for each(var collection in collections) { if (collection.collections) { collection.obj.childCollections = collection.collections; @@ -1631,6 +1676,11 @@ Zotero.Sync.Server.Data = new function() { xml.creator += newCreator; } + // Related items + if (item.related.length) { + xml.related = item.related.join(' '); + } + return xml; } @@ -1645,7 +1695,7 @@ Zotero.Sync.Server.Data = new function() { function xmlToItem(xmlItem, item, skipPrimary) { if (!item) { if (skipPrimary) { - item = new Zotero.Item(null); + item = new Zotero.Item; } else { item = new Zotero.Item(parseInt(xmlItem.@id)); @@ -1740,10 +1790,31 @@ Zotero.Sync.Server.Data = new function() { } } + // Related items + var related = xmlItem.related.toString(); + item.relatedItems = related ? related.split(' ') : []; + return item; } + function removeMissingRelatedItems(xmlNode) { + var missing = []; + var related = xmlNode.related.toString(); + var relIDs = related ? related.split(' ') : []; + if (relIDs.length) { + var exist = Zotero.Items.exist(relIDs); + for each(var id in relIDs) { + if (exist.indexOf(id) == -1) { + missing.push(id); + } + } + xmlNode.related = exist.join(' '); + } + return missing; + } + + function collectionToXML(collection) { var xml = ; diff --git a/chrome/content/zotero/xpcom/translate.js b/chrome/content/zotero/xpcom/translate.js index a33f1a819..fdeed015a 100644 --- a/chrome/content/zotero/xpcom/translate.js +++ b/chrome/content/zotero/xpcom/translate.js @@ -1017,9 +1017,10 @@ Zotero.Translate.prototype._itemTagsAndSeeAlso = function(item, newItem) { if(item.seeAlso) { for each(var seeAlso in item.seeAlso) { if(this._IDMap[seeAlso]) { - newItem.addSeeAlso(this._IDMap[seeAlso]); + newItem.addRelatedItem(this._IDMap[seeAlso]); } } + newItem.save(); } if(item.tags) { var tagsToAdd = {}; @@ -1407,9 +1408,10 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { if(item.seeAlso) { for each(var seeAlso in item.seeAlso) { if(this._IDMap[seeAlso]) { - newItem.addSeeAlso(this._IDMap[seeAlso]); + newItem.addRelatedItem(this._IDMap[seeAlso]); } } + newItem.save(); } // handle tags, if this is an import translation or automatic tagging is