www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit c7b2c84869ddf8c61aa5923c84517b0053637567
parent 104d39bfa61cd45e0a5de715483eb16c9e45ff7d
Author: Dan Stillman <dstillman@zotero.org>
Date:   Sat, 27 Dec 2008 05:42:52 +0000

- Add automatic merging of collection and tag metadata and associated items, with warning alerts (eventually to be converted to logged notifications)
- Switch to using only keys for deleted items
- Fix various tag-related problems
- Probably other things


Diffstat:
Mchrome/content/zotero/xpcom/data/collection.js | 14++++++++------
Mchrome/content/zotero/xpcom/data/tag.js | 57++++++++++++++++++++++++++++++++++++++++-----------------
Mchrome/content/zotero/xpcom/schema.js | 10+++++++++-
Mchrome/content/zotero/xpcom/sync.js | 476+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mchrome/content/zotero/xpcom/utilities.js | 42++++++++++++++++++++++++------------------
Muserdata.sql | 3+--
6 files changed, 389 insertions(+), 213 deletions(-)

diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -398,7 +398,7 @@ Zotero.Collection.prototype.save = function () { currentIDs = []; } - if (this._previousData.childCollections) { + if (this._previousData) { for each(var id in this._previousData.childCollections) { if (currentIDs.indexOf(id) == -1) { removed.push(id); @@ -406,7 +406,7 @@ Zotero.Collection.prototype.save = function () { } } for each(var id in currentIDs) { - if (this._previousData.childCollections && + if (this._previousData && this._previousData.childCollections.indexOf(id) != -1) { continue; } @@ -442,7 +442,7 @@ Zotero.Collection.prototype.save = function () { currentIDs = []; } - if (this._previousData.childItems) { + if (this._previousData) { for each(var id in this._previousData.childItems) { if (currentIDs.indexOf(id) == -1) { removed.push(id); @@ -450,7 +450,7 @@ Zotero.Collection.prototype.save = function () { } } for each(var id in currentIDs) { - if (this._previousData.childItems && + if (this._previousData && this._previousData.childItems.indexOf(id) != -1) { continue; } @@ -801,6 +801,8 @@ Zotero.Collection.prototype.toArray = function() { Zotero.Collection.prototype.serialize = function(nested) { + var childCollections = this.getChildCollections(true); + var childItems = this.getChildItems(true); var obj = { primary: { collectionID: this.id, @@ -811,8 +813,8 @@ Zotero.Collection.prototype.serialize = function(nested) { name: this.name, parent: this.parent, }, - childCollections: this.getChildCollections(true), - childItems: this.getChildItems(true), + childCollections: childCollections ? childCollections : [], + childItems: childItems ? childItems : [], descendents: this.id ? this.getDescendents(nested) : [] }; return obj; diff --git a/chrome/content/zotero/xpcom/data/tag.js b/chrome/content/zotero/xpcom/data/tag.js @@ -143,8 +143,8 @@ Zotero.Tag.prototype.loadFromRow = function (row) { /** * Returns items linked to this tag * - * @param bool asIDs Return as itemIDs - * @return array Array of Zotero.Item instances or itemIDs, + * @param {Boolean} asIDs Return as itemIDs + * @return {Array} Array of Zotero.Item instances or itemIDs, * or FALSE if none */ Zotero.Tag.prototype.getLinkedItems = function (asIDs) { @@ -211,7 +211,7 @@ Zotero.Tag.prototype.removeItem = function (itemID) { } -Zotero.Tag.prototype.save = function () { +Zotero.Tag.prototype.save = function (full) { // Default to manual tag if (!this.type) { this.type = 0; @@ -307,7 +307,7 @@ Zotero.Tag.prototype.save = function () { // Linked items - if (this._changed.linkedItems) { + if (full || this._changed.linkedItems) { var removed = []; var newids = []; var currentIDs = this.getLinkedItems(true); @@ -315,19 +315,31 @@ Zotero.Tag.prototype.save = function () { currentIDs = []; } - if (this._previousData.linkedItems) { - for each(var id in this._previousData.linkedItems) { - if (currentIDs.indexOf(id) == -1) { - removed.push(id); - } + // Use the database for comparison instead of relying on the cache + // This is necessary for a syncing edge case (described in sync.js). + if (full) { + var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; + var dbItemIDs = Zotero.DB.columnQuery(sql, tagID); + if (dbItemIDs) { + removed = Zotero.Utilities.prototype.arrayDiff(currentIDs, dbItemIDs); + newids = Zotero.Utilities.prototype.arrayDiff(dbItemIDs, currentIDs); + } + else { + newids = currentIDs; } } - for each(var id in currentIDs) { - if (this._previousData.linkedItems && - this._previousData.linkedItems.indexOf(id) != -1) { - continue; + else { + if (this._previousData.linkedItems) { + removed = Zotero.Utilities.prototype.arrayDiff( + currentIDs, this._previousData.linkedItems + ); + newids = Zotero.Utilities.prototype.arrayDiff( + this._previousData.linkedItems, currentIDs + ); + } + else { + newids = currentIDs; } - newids.push(id); } if (removed.length) { @@ -414,8 +426,17 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie var d2 = Zotero.Utilities.prototype.arrayDiff( otherData.linkedItems, thisData.linkedItems ); - numDiffs += d1.length; - numDiffs += d2.length; + numDiffs += d1.length + d2.length; + + if (d1.length || d2.length) { + numDiffs += d1.length + d2.length; + diff[0].linkedItems = d1; + diff[1].linkedItems = d2; + } + else { + diff[0].linkedItems = []; + diff[1].linkedItems = []; + } // DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set? if (numDiffs == 0 || @@ -429,6 +450,8 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie Zotero.Tag.prototype.serialize = function () { + var linkedItems = this.getLinkedItems(true); + var obj = { primary: { tagID: this.id, @@ -439,7 +462,7 @@ Zotero.Tag.prototype.serialize = function () { name: this.name, type: this.type, }, - linkedItems: this.getLinkedItems(true), + linkedItems: linkedItems ? linkedItems : [] }; return obj; } diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -2144,7 +2144,15 @@ Zotero.Schema = new function(){ } } - // + // // 1.5 Sync Preview 3.6 + if (i==47) { + Zotero.DB.query("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld"); + Zotero.DB.query("DROP INDEX syncDeleteLog_timestamp"); + Zotero.DB.query("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n key TEXT NOT NULL UNIQUE,\n timestamp INT NOT NULL,\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n);"); + Zotero.DB.query("CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);"); + Zotero.DB.query("INSERT INTO syncDeleteLog SELECT syncObjectTypeID, key, timestamp FROM syncDeleteLogOld"); + Zotero.DB.query("DROP TABLE syncDeleteLogOld"); + } } _updateDBVersion('userdata', toVersion); diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js @@ -136,10 +136,10 @@ Zotero.Sync = new function() { /** * @param object lastSyncDate JS Date object - * @return mixed Returns object with deleted ids + * @return mixed * { - * items: [ { id: 123, key: ABCD1234 }, ... ] - * creators: [ { id: 123, key: ABCD1234 }, ... ], + * items: [ 'ABCD1234', 'BCDE2345', ... ] + * creators: [ 'ABCD1234', 'BCDE2345', ... ], * ... * } * or FALSE if none or -1 if last sync time is before start of log @@ -162,7 +162,7 @@ Zotero.Sync = new function() { } var param = false; - var sql = "SELECT syncObjectTypeID, objectID, key FROM syncDeleteLog"; + var sql = "SELECT syncObjectTypeID, key FROM syncDeleteLog"; if (lastSyncDate) { param = Zotero.Date.toUnixTimestamp(lastSyncDate); sql += " WHERE timestamp>?"; @@ -174,20 +174,17 @@ Zotero.Sync = new function() { return false; } - var deletedIDs = {}; + var deletedKeys = {}; for each(var syncObject in this.syncObjects) { - deletedIDs[syncObject.plural.toLowerCase()] = []; + deletedKeys[syncObject.plural.toLowerCase()] = []; } for each(var row in rows) { var type = this.getObjectTypeName(row.syncObjectTypeID); type = this.syncObjects[type].plural.toLowerCase() - deletedIDs[type].push({ - id: row.objectID, - key: row.key - }); + deletedKeys[type].push(row.key); } - return deletedIDs; + return deletedKeys; } @@ -297,7 +294,7 @@ Zotero.Sync.EventListener = new function () { Zotero.DB.beginTransaction(); if (event == 'delete') { - var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)"; + var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?)"; var syncStatement = Zotero.DB.getStatement(sql); if (isItem && Zotero.Sync.Storage.active) { @@ -326,9 +323,8 @@ Zotero.Sync.EventListener = new function () { } syncStatement.bindInt32Parameter(0, objectTypeID); - syncStatement.bindInt32Parameter(1, ids[i]); - syncStatement.bindStringParameter(2, key); - syncStatement.bindInt32Parameter(3, ts); + syncStatement.bindStringParameter(1, key); + syncStatement.bindInt32Parameter(2, ts); if (storageEnabled && oldItem.primary.itemType == 'attachment' && @@ -652,7 +648,7 @@ Zotero.Sync.Server = new function () { }); this.nextLocalSyncDate = false; - this.apiVersion = 2; + this.apiVersion = 3; default xml namespace = ''; @@ -715,7 +711,7 @@ Zotero.Sync.Server = new function () { } - Zotero.debug('Got session ID ' + _sessionID + ' from server'); + //Zotero.debug('Got session ID ' + _sessionID + ' from server'); if (callback) { callback(); @@ -1318,10 +1314,10 @@ Zotero.BufferedInputListener.prototype = { * }, * deleted: { * items: [ - * { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ... + * 'ABCD1234', 'BCDE2345', ... * ], * creators: [ - * { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ... + * 'ABCD1234', 'BCDE2345', ... * ] * } * }; @@ -1370,26 +1366,21 @@ Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (syncObjectTyp } -Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, id, key) { +Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, key) { var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase(); var deleted = this.uploadIDs.deleted[pluralType]; - - // DEBUG: inefficient - for each(var pair in deleted) { - if (pair.id == id) { - return; - } + if (deleted.indexOf(key) != -1) { + return; } - deleted.push({ id: id, key: key}); + deleted.push(key); } -Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, id, key) { +Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, key) { var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase(); var deleted = this.uploadIDs.deleted[pluralType]; - for (var i=0; i<deleted.length; i++) { - if (deleted[i].id == id && deleted[i].key == key) { + if (deleted[i] == key) { deleted.splice(i, 1); i--; } @@ -1488,16 +1479,16 @@ Zotero.Sync.Server.Data = new function() { * Pull out collections from delete queue in XML * * @param {XML} xml - * @return {Integer[]} Array of collection ids + * @return {String[]} Array of collection keys */ - function _getDeletedCollections(xml) { - var ids = []; + function _getDeletedCollectionKeys(xml) { + var keys = []; if (xml.deleted && xml.deleted.collections) { for each(var xmlNode in xml.deleted.collections.collection) { - ids.push(parseInt(xmlNode.@id)); + keys.push(xmlNode.@key.toString()); } } - return ids; + return keys; } @@ -1510,7 +1501,7 @@ Zotero.Sync.Server.Data = new function() { Zotero.DB.beginTransaction(); xml = _preprocessUpdatedXML(xml); - var deletedCollections = _getDeletedCollections(xml); + var deletedCollectionKeys = _getDeletedCollectionKeys(xml); var remoteCreatorStore = {}; var relatedItemsStore = {}; @@ -1644,85 +1635,18 @@ Zotero.Sync.Server.Data = new function() { break; case 'collection': - var diff = obj.diff(remoteObj, false, true); - if (diff) { - var fieldsChanged = false; - for (var field in diff[0].primary) { - if (field != 'dateModified') { - fieldsChanged = true; - break; - } - } - for (var field in diff[0].fields) { - fieldsChanged = true; - break; - } - - if (fieldsChanged) { - // Check for collection hierarchy change - if (diff[0].childCollections.length) { - // TODO - } - if (diff[1].childCollections.length) { - // TODO - } - // Check for item membership change - if (diff[0].childItems.length) { - var childItems = remoteObj.getChildItems(true); - remoteObj.childItems = childItems.concat(diff[0].childItems); - } - if (diff[1].childItems.length) { - var childItems = obj.getChildItems(true); - obj.childItems = childItems.concat(diff[1].childItems); - } - - // TODO: log - - // TEMP: uncomment once supported - //reconcile = true; - } - // No CR necessary - else { - var save = false; - // Check for child collections in the remote object - // that aren't in the local one - if (diff[1].childCollections.length) { - // TODO: log - // TODO: add - save = true; - } - // Check for items in the remote object - // that aren't in the local one - if (diff[1].childItems.length) { - var childItems = obj.getChildItems(true); - obj.childItems = childItems.concat(diff[1].childItems); - - var msg = _logCollectionItemMerge(obj.name, diff[1].childItems); - // TODO: log rather than alert - alert(msg); - - save = true; - } - - if (save) { - obj.save(); - } - continue; - } - } - else { + var changed = _mergeCollection(obj, remoteObj); + if (!changed) { syncSession.removeFromUpdated(type, obj.id); - continue; } - break; - + continue; + case 'tag': - var diff = obj.diff(remoteObj, false, true); - if (!diff) { + var changed = _mergeTag(obj, remoteObj); + if (!changed) { syncSession.removeFromUpdated(type, obj.id); - continue; } - break; + continue; } if (!reconcile) { @@ -1775,13 +1699,6 @@ Zotero.Sync.Server.Data = new function() { syncSession.uploadIDs.updated[types][index] = newID; } - // Update id in local deletions array - for (var i in syncSession.uploadIDs.deleted[types]) { - if (syncSession.uploadIDs.deleted[types][i].id == oldID) { - syncSession.uploadIDs.deleted[types][i] = newID; - } - } - // Add items linked to creators to updated array, // since their timestamps will be set to the // transaction timestamp @@ -1808,21 +1725,35 @@ Zotero.Sync.Server.Data = new function() { else { isNewObject = true; + Zotero.debug(syncSession.uploadIDs.deleted); + // Check if object has been deleted locally - for each(var pair in syncSession.uploadIDs.deleted[types]) { - if (pair.id != parseInt(xmlNode.@id) || - pair.key != xmlNode.@key.toString()) { + for each(var key in syncSession.uploadIDs.deleted[types]) { + if (key != xmlNode.@key.toString()) { continue; } // TODO: non-merged items - if (type != 'item') { - alert('Delete reconciliation unimplemented for ' + types); - throw ('Delete reconciliation unimplemented for ' + types); + switch (type) { + case 'item': + localDelete = true; + break; + + // Auto-restore locally deleted tags that have + // changed remotely + case 'tag': + syncSession.removeFromDeleted(type, key); + var msg = _generateAutoChangeMessage( + type, null, xmlNode.@name.toString() + ); + alert(msg); + continue; + + default: + alert('Delete reconciliation unimplemented for ' + types); + throw ('Delete reconciliation unimplemented for ' + types); } - - localDelete = true; } // If key already exists on a different item, change local key @@ -1858,11 +1789,17 @@ Zotero.Sync.Server.Data = new function() { ? parseInt(xmlNode.@type) : 0; var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType); if (linkedItems) { - obj.dateModified = Zotero.DB.transactionDateTime; + var mod = false; for each(var id in linkedItems) { - obj.addItem(id); + var added = obj.addItem(id); + if (added) { + mod = true; + } + } + if (mod) { + obj.dateModified = Zotero.DB.transactionDateTime; + syncSession.addToUpdated('tag', parseInt(xmlNode.@id)); } - syncSession.addToUpdated('tag', parseInt(xmlNode.@id)); } } @@ -1889,11 +1826,7 @@ Zotero.Sync.Server.Data = new function() { if (obj.isRegularItem()) { var creators = obj.getCreators(); for each(var creator in creators) { - syncSession.removeFromDeleted( - 'creator', - creator.ref.id, - creator.ref.key - ); + syncSession.removeFromDeleted('creator', creator.ref.key); } } else if (obj.isAttachment() && @@ -1925,10 +1858,14 @@ Zotero.Sync.Server.Data = new function() { Zotero.debug("Processing remotely deleted " + types); for each(var xmlNode in xml.deleted[types][type]) { - var id = parseInt(xmlNode.@id); - var obj = Zotero[Types].get(id); + var key = xmlNode.@key.toString(); + var obj = Zotero[Types].getByKey(key); // Object can't be found - if (!obj || obj.key != xmlNode.@key) { + if (!obj) { + // Since it's already deleted remotely, don't include + // the object in the deleted array if something else + // caused its deletion during the sync + syncSession.removeFromDeleted(type, xmlNode.@key.toString()); continue; } @@ -1940,7 +1877,7 @@ Zotero.Sync.Server.Data = new function() { } // Local object hasn't been modified -- delete else { - toDelete.push(id); + toDelete.push(obj.id); } } } @@ -1995,7 +1932,20 @@ Zotero.Sync.Server.Data = new function() { } } for each(var obj in toSave) { - obj.save(); + // Use a special saving mode for tags to avoid an issue that + // occurs if a tag has changed names remotely but another tag + // conflicts with the local version after the first tag has been + // updated in memory, causing a deletion of the local tag. + // Using the normal save mode, when the first remote tag then + // goes to save, the linked items aren't saved, since as far + // as the in-memory object is concerned, they haven't changed, + // even though they've been deleted from the DB. + // + // To replicate, add an item, add a tag, sync both sides, + // rename the tag, add a new one with the old name, and sync. + var full = type == 'tag'; + + obj.save(full); } // Add back related items (which now exist) @@ -2012,7 +1962,7 @@ Zotero.Sync.Server.Data = new function() { // Add back subcollections else if (type == 'collection') { for each(var collection in collections) { - if (collection.childCollections) { + if (collection.childCollections.length) { collection.obj.childCollections = collection.childCollections; collection.obj.save(); } @@ -2042,8 +1992,8 @@ Zotero.Sync.Server.Data = new function() { // collections so that any deleted items within them don't // update them, which would trigger erroneous conflicts var collections = []; - for each(var colID in deletedCollections) { - var col = Zotero.Collections.get(colID); + for each(var colKey in deletedCollectionKeys) { + var col = Zotero.Collections.getByKey(colKey); col.lockDateModified(); collections.push(col); } @@ -2154,10 +2104,9 @@ Zotero.Sync.Server.Data = new function() { Zotero.debug('Processing locally deleted ' + types); - for each(var obj in ids.deleted[types]) { + for each(var key in ids.deleted[types]) { var deletexml = new XML('<' + type + '/>'); - deletexml.@id = obj.id; - deletexml.@key = obj.key; + deletexml.@key = key; xml.deleted[types][type] += deletexml; } } @@ -2171,6 +2120,215 @@ Zotero.Sync.Server.Data = new function() { } + function _mergeCollection(localObj, remoteObj) { + var diff = localObj.diff(remoteObj, false, true); + if (!diff) { + return false; + } + + Zotero.debug("COLLECTION HAS CHANGED"); + Zotero.debug(diff); + + // Local is newer + if (diff[0].primary.dateModified > + diff[1].primary.dateModified) { + var remoteIsTarget = false; + var targetObj = localObj; + var targetDiff = diff[0]; + var otherDiff = diff[1]; + } + // Remote is newer + else { + var remoteIsTarget = true; + var targetObj = remoteObj; + var targetDiff = diff[1]; + var otherDiff = diff[0]; + } + + if (targetDiff.fields.name) { + var msg = _generateAutoChangeMessage( + 'collection', diff[0].fields.name, diff[1].fields.name, remoteIsTarget + ); + // TODO: log rather than alert + alert(msg); + } + + // Check for child collections in the other object + // that aren't in the target one + if (otherDiff.childCollections.length) { + // TODO: log + // TODO: add + throw ("Collection hierarchy conflict resolution is unimplemented"); + } + + // Add items in other object to target one + if (otherDiff.childItems.length) { + var childItems = targetObj.getChildItems(true); + targetObj.childItems = childItems.concat(otherDiff.childItems); + + var msg = _generateCollectionItemMergeMessage( + targetObj.name, + otherDiff.childItems, + remoteIsTarget + ); + // TODO: log rather than alert + alert(msg); + } + + targetObj.save(); + return true; + } + + + function _mergeTag(localObj, remoteObj) { + var diff = localObj.diff(remoteObj, false, true); + if (!diff) { + return false; + } + + Zotero.debug("TAG HAS CHANGED"); + Zotero.debug(diff); + + // Local is newer + if (diff[0].primary.dateModified > + diff[1].primary.dateModified) { + var remoteIsTarget = false; + var targetObj = localObj; + var targetDiff = diff[0]; + var otherDiff = diff[1]; + } + // Remote is newer + else { + var remoteIsTarget = true; + var targetObj = remoteObj; + var targetDiff = diff[1]; + var otherDiff = diff[0]; + } + + // TODO: log old name + if (targetDiff.fields.name) { + var msg = _generateAutoChangeMessage( + 'tag', diff[0].fields.name, diff[1].fields.name, remoteIsTarget + ); + alert(msg); + } + + // Add linked items in the other object to the target one + if (otherDiff.linkedItems.length) { + // need to handle changed items + + var linkedItems = targetObj.getLinkedItems(true); + targetObj.linkedItems = linkedItems.concat(otherDiff.linkedItems); + + var msg = _generateTagItemMergeMessage( + targetObj.name, + otherDiff.linkedItems, + remoteIsTarget + ); + // TODO: log rather than alert + alert(msg); + } + + targetObj.save(); + return true; + } + + + /** + * @param {String} itemType + * @param {String} localName + * @param {String} remoteName + * @param {Boolean} [remoteMoreRecent=false] + */ + function _generateAutoChangeMessage(itemType, localName, remoteName, remoteMoreRecent) { + if (localName === null) { + // TODO: localize + localName = "[deleted]"; + var localDelete = true; + } + + // TODO: localize + var msg = "A " + itemType + " has changed both locally and " + + "remotely since the last sync:"; + msg += "\n\n"; + msg += "Local version: " + localName + "\n"; + msg += "Remote version: " + remoteName + "\n"; + msg += "\n"; + if (localDelete) { + msg += "The remote version has been kept."; + } + else { + var moreRecent = remoteMoreRecent ? remoteName : localName; + msg += "The most recent version, '" + moreRecent + "', has been kept."; + } + return msg; + } + + + /** + * @param {String} collectionName + * @param {Integer[]} addedItemIDs + * @param {Boolean} remoteIsTarget + */ + function _generateCollectionItemMergeMessage(collectionName, addedItemIDs, remoteIsTarget) { + // TODO: localize + var introMsg = "Items in the collection '" + collectionName + "' have been " + + "added and/or removed in multiple locations." + + introMsg += " "; + if (remoteIsTarget) { + introMsg += "The following items have been added to the remote collection:"; + } + else { + introMsg += "The following items have been added to the local collection:"; + } + var itemText = []; + for each(var id in addedItemIDs) { + var item = Zotero.Items.get(id); + var title = item.getField('title'); + var text = " - " + title; + var firstCreator = item.getField('firstCreator'); + if (firstCreator) { + text += " (" + firstCreator + ")"; + } + itemText.push(text); + } + return introMsg + "\n\n" + itemText.join("\n"); + } + + + /** + * @param {String} tagName + * @param {Integer[]} addedItemIDs + * @param {Boolean} remoteIsTarget + */ + function _generateTagItemMergeMessage(tagName, addedItemIDs, remoteIsTarget) { + // TODO: localize + var introMsg = "The tag '" + tagName + "' has been " + + "added to and/or removed from items in multiple locations." + + introMsg += " "; + if (remoteIsTarget) { + introMsg += "It has been added to the following remote items:"; + } + else { + introMsg += "It has been added to the following local items:"; + } + var itemText = []; + for each(var id in addedItemIDs) { + var item = Zotero.Items.get(id); + var title = item.getField('title'); + var text = " - " + title; + var firstCreator = item.getField('firstCreator'); + if (firstCreator) { + text += " (" + firstCreator + ")"; + } + itemText.push(text); + } + return introMsg + "\n\n" + itemText.join("\n"); + } + + /** * Open a conflict resolution window and return the results * @@ -2223,7 +2381,7 @@ Zotero.Sync.Server.Data = new function() { delete relatedItems[obj.id]; } - syncSession.addToDeleted(type, obj.id, obj.left.key); + syncSession.addToDeleted(type, obj.left.key); } continue; } @@ -2236,7 +2394,7 @@ Zotero.Sync.Server.Data = new function() { // Item had been deleted locally, so remove from // deleted array if (obj.left == 'deleted') { - syncSession.removeFromDeleted(type, obj.id, obj.ref.key); + syncSession.removeFromDeleted(type, obj.ref.key); } // TODO: only upload if the local item was chosen @@ -2583,26 +2741,6 @@ Zotero.Sync.Server.Data = new function() { } - function _logCollectionItemMerge(collectionName, remoteItemIDs) { - // TODO: localize - var introMsg = "Items in the collection '" + collectionName + "' have been " - + "added and/or removed in multiple locations. The following remote " - + "items have been added to the local collection:"; - var itemText = []; - for each(var id in remoteItemIDs) { - var item = Zotero.Items.get(id); - var title = item.getField('title'); - var text = " - " + title; - var firstCreator = item.getField('firstCreator'); - if (firstCreator) { - text += " (" + firstCreator + ")"; - } - itemText.push(text); - } - return introMsg + "\n\n" + itemText.join("\n"); - } - - /** * Converts a Zotero.Creator object to an E4X <creator> object */ @@ -2871,16 +3009,16 @@ Zotero.Sync.Server.Data = new function() { function _deleteConflictingTag(syncSession, name, type) { var tagID = Zotero.Tags.getID(name, type); if (tagID) { + Zotero.debug("Deleting conflicting local tag " + tagID); var tag = Zotero.Tags.get(tagID); var linkedItems = tag.getLinkedItems(true); Zotero.Tags.erase(tagID); - // DEBUG: should purge() be called by Tags.erase() Zotero.Tags.purge(); syncSession.removeFromUpdated('tag', tagID); - syncSession.addToDeleted('tag', tagID, tag.key); + //syncSession.addToDeleted('tag', tag.key); - return linkedItems; + return linkedItems ? linkedItems : []; } return false; diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -292,27 +292,33 @@ Zotero.Utilities.prototype.isInt = function(x) { /** - * Compares an array with another (comparator) and returns an array with - * the values from comparator that don't exist in vector + * Compares an array with another and returns an array with + * the values from array2 that don't exist in array1 * - * Code by Carlos R. L. Rodrigues - * From http://jsfromhell.com/array/diff [rev. #1] - * - * @param {Array} v Array that will be checked - * @param {Array} c Array that will be compared - * @param {Boolean} useIndex If true, returns an array containing just + * @param {Array} array1 Array that will be checked + * @param {Array} array2 Array that will be compared + * @param {Boolean} useIndex If true, return an array containing just * the index of the comparator's elements; - * otherwise returns the values + * otherwise return the values */ -Zotero.Utilities.prototype.arrayDiff = function(v, c, m) { - var d = [], e = -1, h, i, j, k; - for(i = c.length, k = v.length; i--;){ - for(j = k; j && (h = c[i] !== v[--j]);); - h && (d[++e] = m ? i : c[i]); - } - return d; -}; - +Zotero.Utilities.prototype.arrayDiff = function(array1, array2, useIndex) { + if (array1.constructor.name != 'Array') { + throw ("array1 is not an array in Zotero.Utilities.arrayDiff() (" + array1 + ")"); + } + if (array2.constructor.name != 'Array') { + throw ("array2 is not an array in Zotero.Utilities.arrayDiff() (" + array2 + ")"); + } + + var val, pos, vals = []; + for (var i=0; i<array2.length; i++) { + val = array2[i]; + pos = array1.indexOf(val); + if (pos == -1) { + vals.push(useIndex ? pos : val); + } + } + return vals; +} /** diff --git a/userdata.sql b/userdata.sql @@ -1,4 +1,4 @@ --- 46 +-- 47 -- This file creates tables containing user-specific data -- any changes made -- here must be mirrored in transition steps in schema.js::_migrateSchema() @@ -198,7 +198,6 @@ CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID); CREATE TABLE syncDeleteLog ( syncObjectTypeID INT NOT NULL, - objectID INT NOT NULL, key TEXT NOT NULL UNIQUE, timestamp INT NOT NULL, FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)