- 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:
parent
104d39bfa6
commit
c7b2c84869
6 changed files with 389 additions and 213 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue