- 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
This commit is contained in:
Dan Stillman 2008-12-27 05:42:52 +00:00
parent 104d39bfa6
commit c7b2c84869
6 changed files with 389 additions and 213 deletions

View file

@ -398,7 +398,7 @@ Zotero.Collection.prototype.save = function () {
currentIDs = []; currentIDs = [];
} }
if (this._previousData.childCollections) { if (this._previousData) {
for each(var id in this._previousData.childCollections) { for each(var id in this._previousData.childCollections) {
if (currentIDs.indexOf(id) == -1) { if (currentIDs.indexOf(id) == -1) {
removed.push(id); removed.push(id);
@ -406,7 +406,7 @@ Zotero.Collection.prototype.save = function () {
} }
} }
for each(var id in currentIDs) { for each(var id in currentIDs) {
if (this._previousData.childCollections && if (this._previousData &&
this._previousData.childCollections.indexOf(id) != -1) { this._previousData.childCollections.indexOf(id) != -1) {
continue; continue;
} }
@ -442,7 +442,7 @@ Zotero.Collection.prototype.save = function () {
currentIDs = []; currentIDs = [];
} }
if (this._previousData.childItems) { if (this._previousData) {
for each(var id in this._previousData.childItems) { for each(var id in this._previousData.childItems) {
if (currentIDs.indexOf(id) == -1) { if (currentIDs.indexOf(id) == -1) {
removed.push(id); removed.push(id);
@ -450,7 +450,7 @@ Zotero.Collection.prototype.save = function () {
} }
} }
for each(var id in currentIDs) { for each(var id in currentIDs) {
if (this._previousData.childItems && if (this._previousData &&
this._previousData.childItems.indexOf(id) != -1) { this._previousData.childItems.indexOf(id) != -1) {
continue; continue;
} }
@ -801,6 +801,8 @@ Zotero.Collection.prototype.toArray = function() {
Zotero.Collection.prototype.serialize = function(nested) { Zotero.Collection.prototype.serialize = function(nested) {
var childCollections = this.getChildCollections(true);
var childItems = this.getChildItems(true);
var obj = { var obj = {
primary: { primary: {
collectionID: this.id, collectionID: this.id,
@ -811,8 +813,8 @@ Zotero.Collection.prototype.serialize = function(nested) {
name: this.name, name: this.name,
parent: this.parent, parent: this.parent,
}, },
childCollections: this.getChildCollections(true), childCollections: childCollections ? childCollections : [],
childItems: this.getChildItems(true), childItems: childItems ? childItems : [],
descendents: this.id ? this.getDescendents(nested) : [] descendents: this.id ? this.getDescendents(nested) : []
}; };
return obj; return obj;

View file

@ -143,8 +143,8 @@ Zotero.Tag.prototype.loadFromRow = function (row) {
/** /**
* Returns items linked to this tag * Returns items linked to this tag
* *
* @param bool asIDs Return as itemIDs * @param {Boolean} asIDs Return as itemIDs
* @return array Array of Zotero.Item instances or itemIDs, * @return {Array} Array of Zotero.Item instances or itemIDs,
* or FALSE if none * or FALSE if none
*/ */
Zotero.Tag.prototype.getLinkedItems = function (asIDs) { 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 // Default to manual tag
if (!this.type) { if (!this.type) {
this.type = 0; this.type = 0;
@ -307,7 +307,7 @@ Zotero.Tag.prototype.save = function () {
// Linked items // Linked items
if (this._changed.linkedItems) { if (full || this._changed.linkedItems) {
var removed = []; var removed = [];
var newids = []; var newids = [];
var currentIDs = this.getLinkedItems(true); var currentIDs = this.getLinkedItems(true);
@ -315,20 +315,32 @@ Zotero.Tag.prototype.save = function () {
currentIDs = []; currentIDs = [];
} }
// 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;
}
}
else {
if (this._previousData.linkedItems) { if (this._previousData.linkedItems) {
for each(var id in this._previousData.linkedItems) { removed = Zotero.Utilities.prototype.arrayDiff(
if (currentIDs.indexOf(id) == -1) { currentIDs, this._previousData.linkedItems
removed.push(id); );
newids = Zotero.Utilities.prototype.arrayDiff(
this._previousData.linkedItems, currentIDs
);
} }
else {
newids = currentIDs;
} }
} }
for each(var id in currentIDs) {
if (this._previousData.linkedItems &&
this._previousData.linkedItems.indexOf(id) != -1) {
continue;
}
newids.push(id);
}
if (removed.length) { if (removed.length) {
var sql = "DELETE FROM itemTags WHERE tagID=? " var sql = "DELETE FROM itemTags WHERE tagID=? "
@ -414,8 +426,17 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie
var d2 = Zotero.Utilities.prototype.arrayDiff( var d2 = Zotero.Utilities.prototype.arrayDiff(
otherData.linkedItems, thisData.linkedItems otherData.linkedItems, thisData.linkedItems
); );
numDiffs += d1.length; numDiffs += d1.length + d2.length;
numDiffs += 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? // DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
if (numDiffs == 0 || if (numDiffs == 0 ||
@ -429,6 +450,8 @@ Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModifie
Zotero.Tag.prototype.serialize = function () { Zotero.Tag.prototype.serialize = function () {
var linkedItems = this.getLinkedItems(true);
var obj = { var obj = {
primary: { primary: {
tagID: this.id, tagID: this.id,
@ -439,7 +462,7 @@ Zotero.Tag.prototype.serialize = function () {
name: this.name, name: this.name,
type: this.type, type: this.type,
}, },
linkedItems: this.getLinkedItems(true), linkedItems: linkedItems ? linkedItems : []
}; };
return obj; return obj;
} }

View file

@ -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); _updateDBVersion('userdata', toVersion);

View file

@ -136,10 +136,10 @@ Zotero.Sync = new function() {
/** /**
* @param object lastSyncDate JS Date object * @param object lastSyncDate JS Date object
* @return mixed Returns object with deleted ids * @return mixed
* { * {
* items: [ { id: 123, key: ABCD1234 }, ... ] * items: [ 'ABCD1234', 'BCDE2345', ... ]
* creators: [ { id: 123, key: ABCD1234 }, ... ], * creators: [ 'ABCD1234', 'BCDE2345', ... ],
* ... * ...
* } * }
* or FALSE if none or -1 if last sync time is before start of log * 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 param = false;
var sql = "SELECT syncObjectTypeID, objectID, key FROM syncDeleteLog"; var sql = "SELECT syncObjectTypeID, key FROM syncDeleteLog";
if (lastSyncDate) { if (lastSyncDate) {
param = Zotero.Date.toUnixTimestamp(lastSyncDate); param = Zotero.Date.toUnixTimestamp(lastSyncDate);
sql += " WHERE timestamp>?"; sql += " WHERE timestamp>?";
@ -174,20 +174,17 @@ Zotero.Sync = new function() {
return false; return false;
} }
var deletedIDs = {}; var deletedKeys = {};
for each(var syncObject in this.syncObjects) { for each(var syncObject in this.syncObjects) {
deletedIDs[syncObject.plural.toLowerCase()] = []; deletedKeys[syncObject.plural.toLowerCase()] = [];
} }
for each(var row in rows) { for each(var row in rows) {
var type = this.getObjectTypeName(row.syncObjectTypeID); var type = this.getObjectTypeName(row.syncObjectTypeID);
type = this.syncObjects[type].plural.toLowerCase() type = this.syncObjects[type].plural.toLowerCase()
deletedIDs[type].push({ deletedKeys[type].push(row.key);
id: row.objectID,
key: row.key
});
} }
return deletedIDs; return deletedKeys;
} }
@ -297,7 +294,7 @@ Zotero.Sync.EventListener = new function () {
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
if (event == 'delete') { if (event == 'delete') {
var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)"; var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?)";
var syncStatement = Zotero.DB.getStatement(sql); var syncStatement = Zotero.DB.getStatement(sql);
if (isItem && Zotero.Sync.Storage.active) { if (isItem && Zotero.Sync.Storage.active) {
@ -326,9 +323,8 @@ Zotero.Sync.EventListener = new function () {
} }
syncStatement.bindInt32Parameter(0, objectTypeID); syncStatement.bindInt32Parameter(0, objectTypeID);
syncStatement.bindInt32Parameter(1, ids[i]); syncStatement.bindStringParameter(1, key);
syncStatement.bindStringParameter(2, key); syncStatement.bindInt32Parameter(2, ts);
syncStatement.bindInt32Parameter(3, ts);
if (storageEnabled && if (storageEnabled &&
oldItem.primary.itemType == 'attachment' && oldItem.primary.itemType == 'attachment' &&
@ -652,7 +648,7 @@ Zotero.Sync.Server = new function () {
}); });
this.nextLocalSyncDate = false; this.nextLocalSyncDate = false;
this.apiVersion = 2; this.apiVersion = 3;
default xml namespace = ''; 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) { if (callback) {
callback(); callback();
@ -1318,10 +1314,10 @@ Zotero.BufferedInputListener.prototype = {
* }, * },
* deleted: { * deleted: {
* items: [ * items: [
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ... * 'ABCD1234', 'BCDE2345', ...
* ], * ],
* creators: [ * 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 pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
var deleted = this.uploadIDs.deleted[pluralType]; var deleted = this.uploadIDs.deleted[pluralType];
if (deleted.indexOf(key) != -1) {
// DEBUG: inefficient
for each(var pair in deleted) {
if (pair.id == id) {
return; return;
} }
} deleted.push(key);
deleted.push({ id: id, key: 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 pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
var deleted = this.uploadIDs.deleted[pluralType]; var deleted = this.uploadIDs.deleted[pluralType];
for (var i=0; i<deleted.length; i++) { 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); deleted.splice(i, 1);
i--; i--;
} }
@ -1488,16 +1479,16 @@ Zotero.Sync.Server.Data = new function() {
* Pull out collections from delete queue in XML * Pull out collections from delete queue in XML
* *
* @param {XML} xml * @param {XML} xml
* @return {Integer[]} Array of collection ids * @return {String[]} Array of collection keys
*/ */
function _getDeletedCollections(xml) { function _getDeletedCollectionKeys(xml) {
var ids = []; var keys = [];
if (xml.deleted && xml.deleted.collections) { if (xml.deleted && xml.deleted.collections) {
for each(var xmlNode in xml.deleted.collections.collection) { 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(); Zotero.DB.beginTransaction();
xml = _preprocessUpdatedXML(xml); xml = _preprocessUpdatedXML(xml);
var deletedCollections = _getDeletedCollections(xml); var deletedCollectionKeys = _getDeletedCollectionKeys(xml);
var remoteCreatorStore = {}; var remoteCreatorStore = {};
var relatedItemsStore = {}; var relatedItemsStore = {};
@ -1644,85 +1635,18 @@ Zotero.Sync.Server.Data = new function() {
break; break;
case 'collection': case 'collection':
var diff = obj.diff(remoteObj, false, true); var changed = _mergeCollection(obj, remoteObj);
if (diff) { if (!changed) {
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 {
syncSession.removeFromUpdated(type, obj.id); syncSession.removeFromUpdated(type, obj.id);
continue;
} }
break; continue;
case 'tag': case 'tag':
var diff = obj.diff(remoteObj, false, true); var changed = _mergeTag(obj, remoteObj);
if (!diff) { if (!changed) {
syncSession.removeFromUpdated(type, obj.id); syncSession.removeFromUpdated(type, obj.id);
continue;
} }
break; continue;
} }
if (!reconcile) { if (!reconcile) {
@ -1775,13 +1699,6 @@ Zotero.Sync.Server.Data = new function() {
syncSession.uploadIDs.updated[types][index] = newID; 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, // Add items linked to creators to updated array,
// since their timestamps will be set to the // since their timestamps will be set to the
// transaction timestamp // transaction timestamp
@ -1808,21 +1725,35 @@ Zotero.Sync.Server.Data = new function() {
else { else {
isNewObject = true; isNewObject = true;
Zotero.debug(syncSession.uploadIDs.deleted);
// Check if object has been deleted locally // Check if object has been deleted locally
for each(var pair in syncSession.uploadIDs.deleted[types]) { for each(var key in syncSession.uploadIDs.deleted[types]) {
if (pair.id != parseInt(xmlNode.@id) || if (key != xmlNode.@key.toString()) {
pair.key != xmlNode.@key.toString()) {
continue; continue;
} }
// TODO: non-merged items // TODO: non-merged items
if (type != 'item') { 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); alert('Delete reconciliation unimplemented for ' + types);
throw ('Delete reconciliation unimplemented for ' + types); throw ('Delete reconciliation unimplemented for ' + types);
} }
localDelete = true;
} }
// If key already exists on a different item, change local key // If key already exists on a different item, change local key
@ -1858,13 +1789,19 @@ Zotero.Sync.Server.Data = new function() {
? parseInt(xmlNode.@type) : 0; ? parseInt(xmlNode.@type) : 0;
var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType); var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType);
if (linkedItems) { if (linkedItems) {
obj.dateModified = Zotero.DB.transactionDateTime; var mod = false;
for each(var id in linkedItems) { 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));
} }
} }
}
if (localDelete) { if (localDelete) {
// TODO: order reconcile by parent/child? // TODO: order reconcile by parent/child?
@ -1889,11 +1826,7 @@ Zotero.Sync.Server.Data = new function() {
if (obj.isRegularItem()) { if (obj.isRegularItem()) {
var creators = obj.getCreators(); var creators = obj.getCreators();
for each(var creator in creators) { for each(var creator in creators) {
syncSession.removeFromDeleted( syncSession.removeFromDeleted('creator', creator.ref.key);
'creator',
creator.ref.id,
creator.ref.key
);
} }
} }
else if (obj.isAttachment() && else if (obj.isAttachment() &&
@ -1925,10 +1858,14 @@ Zotero.Sync.Server.Data = new function() {
Zotero.debug("Processing remotely deleted " + types); Zotero.debug("Processing remotely deleted " + types);
for each(var xmlNode in xml.deleted[types][type]) { for each(var xmlNode in xml.deleted[types][type]) {
var id = parseInt(xmlNode.@id); var key = xmlNode.@key.toString();
var obj = Zotero[Types].get(id); var obj = Zotero[Types].getByKey(key);
// Object can't be found // 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; continue;
} }
@ -1940,7 +1877,7 @@ Zotero.Sync.Server.Data = new function() {
} }
// Local object hasn't been modified -- delete // Local object hasn't been modified -- delete
else { else {
toDelete.push(id); toDelete.push(obj.id);
} }
} }
} }
@ -1995,7 +1932,20 @@ Zotero.Sync.Server.Data = new function() {
} }
} }
for each(var obj in toSave) { 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) // Add back related items (which now exist)
@ -2012,7 +1962,7 @@ Zotero.Sync.Server.Data = new function() {
// Add back subcollections // Add back subcollections
else if (type == 'collection') { else if (type == 'collection') {
for each(var collection in collections) { for each(var collection in collections) {
if (collection.childCollections) { if (collection.childCollections.length) {
collection.obj.childCollections = collection.childCollections; collection.obj.childCollections = collection.childCollections;
collection.obj.save(); collection.obj.save();
} }
@ -2042,8 +1992,8 @@ Zotero.Sync.Server.Data = new function() {
// collections so that any deleted items within them don't // collections so that any deleted items within them don't
// update them, which would trigger erroneous conflicts // update them, which would trigger erroneous conflicts
var collections = []; var collections = [];
for each(var colID in deletedCollections) { for each(var colKey in deletedCollectionKeys) {
var col = Zotero.Collections.get(colID); var col = Zotero.Collections.getByKey(colKey);
col.lockDateModified(); col.lockDateModified();
collections.push(col); collections.push(col);
} }
@ -2154,10 +2104,9 @@ Zotero.Sync.Server.Data = new function() {
Zotero.debug('Processing locally deleted ' + types); 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 + '/>'); var deletexml = new XML('<' + type + '/>');
deletexml.@id = obj.id; deletexml.@key = key;
deletexml.@key = obj.key;
xml.deleted[types][type] += deletexml; 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 * Open a conflict resolution window and return the results
* *
@ -2223,7 +2381,7 @@ Zotero.Sync.Server.Data = new function() {
delete relatedItems[obj.id]; delete relatedItems[obj.id];
} }
syncSession.addToDeleted(type, obj.id, obj.left.key); syncSession.addToDeleted(type, obj.left.key);
} }
continue; continue;
} }
@ -2236,7 +2394,7 @@ Zotero.Sync.Server.Data = new function() {
// Item had been deleted locally, so remove from // Item had been deleted locally, so remove from
// deleted array // deleted array
if (obj.left == 'deleted') { 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 // 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 * 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) { function _deleteConflictingTag(syncSession, name, type) {
var tagID = Zotero.Tags.getID(name, type); var tagID = Zotero.Tags.getID(name, type);
if (tagID) { if (tagID) {
Zotero.debug("Deleting conflicting local tag " + tagID);
var tag = Zotero.Tags.get(tagID); var tag = Zotero.Tags.get(tagID);
var linkedItems = tag.getLinkedItems(true); var linkedItems = tag.getLinkedItems(true);
Zotero.Tags.erase(tagID); Zotero.Tags.erase(tagID);
// DEBUG: should purge() be called by Tags.erase()
Zotero.Tags.purge(); Zotero.Tags.purge();
syncSession.removeFromUpdated('tag', tagID); syncSession.removeFromUpdated('tag', tagID);
syncSession.addToDeleted('tag', tagID, tag.key); //syncSession.addToDeleted('tag', tag.key);
return linkedItems; return linkedItems ? linkedItems : [];
} }
return false; return false;

View file

@ -292,27 +292,33 @@ Zotero.Utilities.prototype.isInt = function(x) {
/** /**
* Compares an array with another (comparator) and returns an array with * Compares an array with another and returns an array with
* the values from comparator that don't exist in vector * the values from array2 that don't exist in array1
* *
* Code by Carlos R. L. Rodrigues * @param {Array} array1 Array that will be checked
* From http://jsfromhell.com/array/diff [rev. #1] * @param {Array} array2 Array that will be compared
* * @param {Boolean} useIndex If true, return an array containing just
* @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
* the index of the comparator's elements; * the index of the comparator's elements;
* otherwise returns the values * otherwise return the values
*/ */
Zotero.Utilities.prototype.arrayDiff = function(v, c, m) { Zotero.Utilities.prototype.arrayDiff = function(array1, array2, useIndex) {
var d = [], e = -1, h, i, j, k; if (array1.constructor.name != 'Array') {
for(i = c.length, k = v.length; i--;){ throw ("array1 is not an array in Zotero.Utilities.arrayDiff() (" + array1 + ")");
for(j = k; j && (h = c[i] !== v[--j]);); }
h && (d[++e] = m ? i : c[i]); if (array2.constructor.name != 'Array') {
throw ("array2 is not an array in Zotero.Utilities.arrayDiff() (" + array2 + ")");
} }
return d;
};
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;
}
/** /**

View file

@ -1,4 +1,4 @@
-- 46 -- 47
-- This file creates tables containing user-specific data -- any changes made -- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema() -- 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 ( CREATE TABLE syncDeleteLog (
syncObjectTypeID INT NOT NULL, syncObjectTypeID INT NOT NULL,
objectID INT NOT NULL,
key TEXT NOT NULL UNIQUE, key TEXT NOT NULL UNIQUE,
timestamp INT NOT NULL, timestamp INT NOT NULL,
FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID) FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)