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 @@
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