Fixed lots of sync bugs
- All string values are now trimmed going into the DB (with a migration step for existing values) -- this fixes erroneous conflicts due to leading/trailing whitespace in sync XML being ignored - Case disambiguation fixed on server - Added basic diff ability to collections and tags so that identical objects won't trigger conflicts - Fixed various other bugs that could cause erroneous conflicts - Moved string fields in serialize() objects into a 'fields' object for consistency with Zotero.Item Upshot of most of the above is that identical pre-upgrade libraries should now merge cleanly Also reorganized/simplified/modularized parts of the sync code
This commit is contained in:
parent
3fdb212dd6
commit
108fd304ab
12 changed files with 608 additions and 336 deletions
|
@ -1856,11 +1856,10 @@ var ZoteroPane = new function()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!popup) {
|
if (!popup) {
|
||||||
try {
|
if (!text) {
|
||||||
// trim
|
text = '';
|
||||||
text = text.replace(/^[\xA0\r\n\s]*(.*)[\xA0\r\n\s]*$/m, "$1");
|
|
||||||
}
|
}
|
||||||
catch (e){}
|
text = Zotero.Utilities.prototype.trim(text);
|
||||||
|
|
||||||
var item = new Zotero.Item(false, 'note');
|
var item = new Zotero.Item(false, 'note');
|
||||||
item.setNote(text);
|
item.setNote(text);
|
||||||
|
|
|
@ -72,6 +72,10 @@ Zotero.Collection.prototype._get = function (field) {
|
||||||
|
|
||||||
|
|
||||||
Zotero.Collection.prototype._set = function (field, val) {
|
Zotero.Collection.prototype._set = function (field, val) {
|
||||||
|
if (field == 'name') {
|
||||||
|
val = Zotero.Utilities.prototype.trim(val);
|
||||||
|
}
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'id': // set using constructor
|
case 'id': // set using constructor
|
||||||
//case 'collectionID': // set using constructor
|
//case 'collectionID': // set using constructor
|
||||||
|
@ -648,6 +652,49 @@ Zotero.Collection.prototype.hasDescendent = function(type, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this collection to another
|
||||||
|
*
|
||||||
|
* Returns a two-element array containing two objects with the differing values,
|
||||||
|
* or FALSE if no differences
|
||||||
|
*
|
||||||
|
* @param {Zotero.Collection} collection Zotero.Collection to compare this item to
|
||||||
|
* @param {Boolean} includeMatches Include all fields, even those that aren't different
|
||||||
|
* @param {Boolean} ignoreOnlyDateModified If no fields other than dateModified
|
||||||
|
* are different, just return false
|
||||||
|
*/
|
||||||
|
Zotero.Collection.prototype.diff = function (collection, includeMatches, ignoreOnlyDateModified) {
|
||||||
|
var diff = [];
|
||||||
|
var thisData = this.serialize();
|
||||||
|
var otherData = collection.serialize();
|
||||||
|
var numDiffs = Zotero.Collections.diff(thisData, otherData, diff, includeMatches);
|
||||||
|
|
||||||
|
// For the moment, just compare children and increase numDiffs if any differences
|
||||||
|
var d1 = Zotero.Utilities.prototype.arrayDiff(
|
||||||
|
thisData.childCollections, otherData.childCollections
|
||||||
|
);
|
||||||
|
var d2 = Zotero.Utilities.prototype.arrayDiff(
|
||||||
|
thisData.childCollections, otherData.childCollections
|
||||||
|
);
|
||||||
|
var d3 = Zotero.Utilities.prototype.arrayDiff(
|
||||||
|
thisData.childItems, otherData.childItems
|
||||||
|
);
|
||||||
|
var d4 = Zotero.Utilities.prototype.arrayDiff(
|
||||||
|
thisData.childItems, otherData.childItems
|
||||||
|
);
|
||||||
|
numDiffs += d1.length + d2.length + d3.length + d4.length;
|
||||||
|
|
||||||
|
// DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
|
||||||
|
if (numDiffs == 0 ||
|
||||||
|
(ignoreOnlyDateModified && numDiffs == 1
|
||||||
|
&& diff[0].primary && diff[0].primary.dateModified)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes collection and all descendent collections (and optionally items)
|
* Deletes collection and all descendent collections (and optionally items)
|
||||||
**/
|
**/
|
||||||
|
@ -721,8 +768,10 @@ Zotero.Collection.prototype.serialize = function(nested) {
|
||||||
dateModified: this.dateModified,
|
dateModified: this.dateModified,
|
||||||
key: this.key
|
key: this.key
|
||||||
},
|
},
|
||||||
name: this.name,
|
fields: {
|
||||||
parent: this.parent,
|
name: this.name,
|
||||||
|
parent: this.parent,
|
||||||
|
},
|
||||||
childCollections: this.getChildCollections(true),
|
childCollections: this.getChildCollections(true),
|
||||||
childItems: this.getChildItems(true),
|
childItems: this.getChildItems(true),
|
||||||
descendents: this.id ? this.getDescendents(nested) : []
|
descendents: this.id ? this.getDescendents(nested) : []
|
||||||
|
|
|
@ -73,6 +73,14 @@ Zotero.Creator.prototype._get = function (field) {
|
||||||
|
|
||||||
|
|
||||||
Zotero.Creator.prototype._set = function (field, val) {
|
Zotero.Creator.prototype._set = function (field, val) {
|
||||||
|
switch (field) {
|
||||||
|
case 'firstName':
|
||||||
|
case 'lastName':
|
||||||
|
case 'shortName':
|
||||||
|
val = value = Zotero.Utilities.prototype.trim(val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'id': // set using constructor
|
case 'id': // set using constructor
|
||||||
//case 'creatorID': // set using constructor
|
//case 'creatorID': // set using constructor
|
||||||
|
|
|
@ -120,5 +120,71 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
|
||||||
delete this._objectCache[ids[i]];
|
delete this._objectCache[ids[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} data1 Serialized copy of first object
|
||||||
|
* @param {Object} data2 Serialized copy of second object
|
||||||
|
* @param {Array} diff Empty array to put diff data in
|
||||||
|
* @param {Boolean} [includeMatches=false] Include all fields, even those
|
||||||
|
* that aren't different
|
||||||
|
*/
|
||||||
|
this.diff = function (data1, data2, diff, includeMatches) {
|
||||||
|
diff.push({}, {});
|
||||||
|
var numDiffs = 0;
|
||||||
|
|
||||||
|
var subs = ['primary', 'fields'];
|
||||||
|
|
||||||
|
for each(var sub in subs) {
|
||||||
|
diff[0][sub] = {};
|
||||||
|
diff[1][sub] = {};
|
||||||
|
for (var field in data1[sub]) {
|
||||||
|
if (!data1[sub][field] && !data2[sub][field]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = !data1[sub][field] || !data2[sub][field] ||
|
||||||
|
data1[sub][field] != data2[sub][field];
|
||||||
|
|
||||||
|
if (includeMatches || changed) {
|
||||||
|
diff[0][sub][field] = data1[sub][field] ?
|
||||||
|
data1[sub][field] : '';
|
||||||
|
diff[1][sub][field] = data2[sub][field] ?
|
||||||
|
data2[sub][field] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
numDiffs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEBUG: some of this is probably redundant
|
||||||
|
for (var field in data2[sub]) {
|
||||||
|
if (diff[0][sub][field] != undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data1[sub][field] && !data2[sub][field]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = !data1[sub][field] || !data2[sub][field] ||
|
||||||
|
data1[sub][field] != data2[sub][field];
|
||||||
|
|
||||||
|
if (includeMatches || changed) {
|
||||||
|
diff[0][sub][field] = data1[sub][field] ?
|
||||||
|
data1[sub][field] : '';
|
||||||
|
diff[1][sub][field] = data2[sub][field] ?
|
||||||
|
data2[sub][field] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
numDiffs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numDiffs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -541,6 +541,10 @@ Zotero.Item.prototype.inCollection = function(collectionID) {
|
||||||
* Field can be passed as fieldID or fieldName
|
* Field can be passed as fieldID or fieldName
|
||||||
*/
|
*/
|
||||||
Zotero.Item.prototype.setField = function(field, value, loadIn) {
|
Zotero.Item.prototype.setField = function(field, value, loadIn) {
|
||||||
|
if (typeof value == 'string') {
|
||||||
|
value = Zotero.Utilities.prototype.trim(value);
|
||||||
|
}
|
||||||
|
|
||||||
this._disabledCheck();
|
this._disabledCheck();
|
||||||
|
|
||||||
//Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ")");
|
//Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ")");
|
||||||
|
@ -580,8 +584,7 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If field value has changed
|
// If field value has changed
|
||||||
// dateModified is always marked as changed
|
if (this['_' + field] != value) {
|
||||||
if (this['_' + field] != value || field == 'dateModified') {
|
|
||||||
Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4);
|
Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4);
|
||||||
|
|
||||||
// Save a copy of the object before modifying
|
// Save a copy of the object before modifying
|
||||||
|
@ -1816,14 +1819,6 @@ Zotero.Item.prototype.save = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Zotero.Item.prototype.updateDateModified = function() {
|
|
||||||
var sql = "UPDATE items SET dateModified=? WHERE itemID=?";
|
|
||||||
Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]);
|
|
||||||
sql = "SELECT dateModified FROM items WHERE itemID=?";
|
|
||||||
this._dateModified = Zotero.DB.valueQuery(sql, this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Zotero.Item.prototype.isRegularItem = function() {
|
Zotero.Item.prototype.isRegularItem = function() {
|
||||||
return !(this.isNote() || this.isAttachment());
|
return !(this.isNote() || this.isAttachment());
|
||||||
}
|
}
|
||||||
|
@ -1879,7 +1874,8 @@ Zotero.Item.prototype.setSource = function(sourceItemID) {
|
||||||
throw ("setSource() can only be called on items of type 'note' or 'attachment'");
|
throw ("setSource() can only be called on items of type 'note' or 'attachment'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._sourceItemID == sourceItemID) {
|
var oldSourceItemID = this.getSource();
|
||||||
|
if (oldSourceItemID == sourceItemID) {
|
||||||
Zotero.debug("Source item has not changed in Zotero.Item.setSource()");
|
Zotero.debug("Source item has not changed in Zotero.Item.setSource()");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2029,7 +2025,14 @@ Zotero.Item.prototype.setNote = function(text) {
|
||||||
throw ("updateNote() can only be called on notes and attachments");
|
throw ("updateNote() can only be called on notes and attachments");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text == this._noteText) {
|
if (typeof text != 'string') {
|
||||||
|
throw ("text must be a string in Zotero.Item.setNote()");
|
||||||
|
}
|
||||||
|
|
||||||
|
text = Zotero.Utilities.prototype.trim(text);
|
||||||
|
|
||||||
|
var oldText = this.getNote();
|
||||||
|
if (text == oldText) {
|
||||||
Zotero.debug("Note has not changed in Zotero.Item.setNote()");
|
Zotero.debug("Note has not changed in Zotero.Item.setNote()");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2934,69 +2937,16 @@ Zotero.Item.prototype.getImageSrc = function() {
|
||||||
* Returns a two-element array containing two objects with the differing values,
|
* Returns a two-element array containing two objects with the differing values,
|
||||||
* or FALSE if no differences
|
* or FALSE if no differences
|
||||||
*
|
*
|
||||||
* @param object item Zotero.Item to compare this item to
|
* @param {Zotero.Item} item Zotero.Item to compare this item to
|
||||||
* @param bool includeMatches Include all fields, even those that aren't different
|
* @param {Boolean} includeMatches Include all fields, even those that aren't different
|
||||||
* @param bool ignoreOnlyDateModified If no fields other than dateModified
|
* @param {Boolean} ignoreOnlyDateModified If no fields other than dateModified
|
||||||
* are different, just return false
|
* are different, just return false
|
||||||
*/
|
*/
|
||||||
Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModified) {
|
Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModified) {
|
||||||
|
var diff = [];
|
||||||
var thisData = this.serialize();
|
var thisData = this.serialize();
|
||||||
var otherData = item.serialize();
|
var otherData = item.serialize();
|
||||||
|
var numDiffs = Zotero.Items.diff(thisData, otherData, diff, includeMatches);
|
||||||
var diff = [{}, {}];
|
|
||||||
var numDiffs = 0;
|
|
||||||
|
|
||||||
var subs = ['primary', 'fields'];
|
|
||||||
|
|
||||||
// TODO: base-mapped fields
|
|
||||||
for each(var sub in subs) {
|
|
||||||
diff[0][sub] = {};
|
|
||||||
diff[1][sub] = {};
|
|
||||||
for (var field in thisData[sub]) {
|
|
||||||
if (!thisData[sub][field] && !otherData[sub][field]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var changed = !thisData[sub][field] || !otherData[sub][field] ||
|
|
||||||
thisData[sub][field] != otherData[sub][field];
|
|
||||||
|
|
||||||
if (includeMatches || changed) {
|
|
||||||
diff[0][sub][field] = thisData[sub][field] ?
|
|
||||||
thisData[sub][field] : '';
|
|
||||||
diff[1][sub][field] = otherData[sub][field] ?
|
|
||||||
otherData[sub][field] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
numDiffs++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEBUG: some of this is probably redundant
|
|
||||||
for (var field in otherData[sub]) {
|
|
||||||
if (diff[0][sub][field] != undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!thisData[sub][field] && !otherData[sub][field]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var changed = !thisData[sub][field] || !otherData[sub][field] ||
|
|
||||||
thisData[sub][field] != otherData[sub][field];
|
|
||||||
|
|
||||||
if (includeMatches || changed) {
|
|
||||||
diff[0][sub][field] = thisData[sub][field] ?
|
|
||||||
thisData[sub][field] : '';
|
|
||||||
diff[1][sub][field] = otherData[sub][field] ?
|
|
||||||
otherData[sub][field] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
numDiffs++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diff[0].creators = [];
|
diff[0].creators = [];
|
||||||
diff[1].creators = [];
|
diff[1].creators = [];
|
||||||
|
@ -3012,6 +2962,7 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModif
|
||||||
|
|
||||||
// TODO: annotations
|
// TODO: annotations
|
||||||
|
|
||||||
|
// DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
|
||||||
if (numDiffs == 0 ||
|
if (numDiffs == 0 ||
|
||||||
(ignoreOnlyDateModified && numDiffs == 1
|
(ignoreOnlyDateModified && numDiffs == 1
|
||||||
&& diff[0].primary && diff[0].primary.dateModified)) {
|
&& diff[0].primary && diff[0].primary.dateModified)) {
|
||||||
|
|
|
@ -66,6 +66,10 @@ Zotero.Tag.prototype._get = function (field) {
|
||||||
|
|
||||||
|
|
||||||
Zotero.Tag.prototype._set = function (field, val) {
|
Zotero.Tag.prototype._set = function (field, val) {
|
||||||
|
if (field == 'name') {
|
||||||
|
val = Zotero.Utilities.prototype.trim(val);
|
||||||
|
}
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'id': // set using constructor
|
case 'id': // set using constructor
|
||||||
//case 'tagID': // set using constructor
|
//case 'tagID': // set using constructor
|
||||||
|
@ -386,6 +390,39 @@ Zotero.Tag.prototype.save = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this tag to another
|
||||||
|
*
|
||||||
|
* Returns a two-element array containing two objects with the differing values,
|
||||||
|
* or FALSE if no differences
|
||||||
|
*
|
||||||
|
* @param {Zotero.Tag} tag Zotero.Tag to compare this item to
|
||||||
|
* @param {Boolean} includeMatches Include all fields, even those that aren't different
|
||||||
|
* @param {Boolean} ignoreOnlyDateModified If no fields other than dateModified
|
||||||
|
* are different, just return false
|
||||||
|
*/
|
||||||
|
Zotero.Tag.prototype.diff = function (tag, includeMatches, ignoreOnlyDateModified) {
|
||||||
|
var diff = [];
|
||||||
|
var thisData = this.serialize();
|
||||||
|
var otherData = tag.serialize();
|
||||||
|
var numDiffs = Zotero.Tags.diff(thisData, otherData, diff, includeMatches);
|
||||||
|
|
||||||
|
// For the moment, just compare linked items and increase numDiffs if any differences
|
||||||
|
var d1 = Zotero.Utilities.prototype.arrayDiff(
|
||||||
|
thisData.linkedItems, otherData.linkedItems
|
||||||
|
);
|
||||||
|
|
||||||
|
// DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
|
||||||
|
if (numDiffs == 0 ||
|
||||||
|
(ignoreOnlyDateModified && numDiffs == 1
|
||||||
|
&& diff[0].primary && diff[0].primary.dateModified)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Zotero.Tag.prototype.serialize = function () {
|
Zotero.Tag.prototype.serialize = function () {
|
||||||
var obj = {
|
var obj = {
|
||||||
primary: {
|
primary: {
|
||||||
|
@ -393,15 +430,16 @@ Zotero.Tag.prototype.serialize = function () {
|
||||||
dateModified: this.dateModified,
|
dateModified: this.dateModified,
|
||||||
key: this.key
|
key: this.key
|
||||||
},
|
},
|
||||||
name: this.name,
|
fields: {
|
||||||
type: this.type,
|
name: this.name,
|
||||||
|
type: this.type,
|
||||||
|
},
|
||||||
linkedItems: this.getLinkedItems(true),
|
linkedItems: this.getLinkedItems(true),
|
||||||
};
|
};
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove tag from all linked items
|
* Remove tag from all linked items
|
||||||
*
|
*
|
||||||
|
|
|
@ -313,7 +313,7 @@ Zotero.Report = new function() {
|
||||||
content += '<h3 class="tags">' + escapeXML(str) + '</h3>\n';
|
content += '<h3 class="tags">' + escapeXML(str) + '</h3>\n';
|
||||||
content += '<ul class="tags">\n';
|
content += '<ul class="tags">\n';
|
||||||
for each(var tag in arr.tags) {
|
for each(var tag in arr.tags) {
|
||||||
content += '<li>' + escapeXML(tag.name) + '</li>\n';
|
content += '<li>' + escapeXML(tag.fields.name) + '</li>\n';
|
||||||
}
|
}
|
||||||
content += '</ul>\n';
|
content += '</ul>\n';
|
||||||
}
|
}
|
||||||
|
|
|
@ -2049,6 +2049,57 @@ Zotero.Schema = new function(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (i==45) {
|
||||||
|
var rows = Zotero.DB.query("SELECT * FROM itemDataValues WHERE value REGEXP '(^\\s+|\\s+$)'");
|
||||||
|
if (rows) {
|
||||||
|
for each(var row in rows) {
|
||||||
|
var trimmed = Zotero.Utilities.prototype.trim(row.value);
|
||||||
|
var valueID = Zotero.DB.valueQuery("SELECT valueID FROM itemDataValues WHERE value=?", trimmed);
|
||||||
|
if (valueID) {
|
||||||
|
Zotero.DB.query("UPDATE itemData SET valueID=? WHERE valueID=?", [valueID, row.valueID]);
|
||||||
|
Zotero.DB.query("DELETE FROM itemDataValues WHERE valueID=?", row.valueID);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.DB.query("UPDATE itemDataValues SET value=? WHERE valueID=?", [trimmed, row.valueID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.DB.query("UPDATE creatorData SET firstName=TRIM(firstName), lastName=TRIM(lastName)");
|
||||||
|
var rows = Zotero.DB.query("SELECT * FROM creatorData ORDER BY lastName, firstName, creatorDataID");
|
||||||
|
if (rows) {
|
||||||
|
for (var j=0; j<rows.length-1; j++) {
|
||||||
|
var k = j + 1;
|
||||||
|
while (rows[k].lastName == rows[j].lastName &&
|
||||||
|
rows[k].firstName == rows[j].firstName &&
|
||||||
|
rows[k].fieldMode == rows[j].fieldMode) {
|
||||||
|
Zotero.DB.query("UPDATE creators SET creatorDataID=? WHERE creatorDataID=?", [rows[j].creatorDataID, rows[k].creatorDataID]);
|
||||||
|
Zotero.DB.query("DELETE FROM creatorData WHERE creatorDataID=?", rows[k].creatorDataID);
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = Zotero.DB.query("SELECT * FROM tags WHERE name REGEXP '(^\\s+|\\s+$)'");
|
||||||
|
if (rows) {
|
||||||
|
for each(var row in rows) {
|
||||||
|
var trimmed = Zotero.Utilities.prototype.trim(row.name);
|
||||||
|
var tagID = Zotero.DB.valueQuery("SELECT tagID FROM tags WHERE name=?", trimmed);
|
||||||
|
if (tagID) {
|
||||||
|
Zotero.DB.query("UPDATE itemTags SET tagID=? WHERE tagID=?", [tagID, row.tagID]);
|
||||||
|
Zotero.DB.query("DELETE FROM tags WHERE tagID=?", row.tagID);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.DB.query("UPDATE itemTags SET tag=? WHERE tagID=?", [trimmed, row.tagID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.DB.query("UPDATE itemNotes SET note=TRIM(note)");
|
||||||
|
Zotero.DB.query("UPDATE collections SET collectionName=TRIM(collectionName)");
|
||||||
|
Zotero.DB.query("UPDATE savedSearches SET savedSearchName=TRIM(savedSearchName)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateDBVersion('userdata', toVersion);
|
_updateDBVersion('userdata', toVersion);
|
||||||
|
|
|
@ -86,6 +86,10 @@ Zotero.Search.prototype._get = function (field) {
|
||||||
|
|
||||||
|
|
||||||
Zotero.Search.prototype._set = function (field, val) {
|
Zotero.Search.prototype._set = function (field, val) {
|
||||||
|
if (field == 'name') {
|
||||||
|
val = Zotero.Utilities.prototype.trim(val);
|
||||||
|
}
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
//case 'id': // set using constructor
|
//case 'id': // set using constructor
|
||||||
case 'searchID':
|
case 'searchID':
|
||||||
|
@ -814,7 +818,9 @@ Zotero.Search.prototype.serialize = function() {
|
||||||
dateModified: this.dateModified,
|
dateModified: this.dateModified,
|
||||||
key: this.key
|
key: this.key
|
||||||
},
|
},
|
||||||
name: this.name,
|
fields: {
|
||||||
|
name: this.name,
|
||||||
|
},
|
||||||
conditions: this.getSearchConditions()
|
conditions: this.getSearchConditions()
|
||||||
};
|
};
|
||||||
return obj;
|
return obj;
|
||||||
|
|
|
@ -2,12 +2,9 @@ Zotero.Sync = new function() {
|
||||||
this.init = init;
|
this.init = init;
|
||||||
this.getObjectTypeID = getObjectTypeID;
|
this.getObjectTypeID = getObjectTypeID;
|
||||||
this.getObjectTypeName = getObjectTypeName;
|
this.getObjectTypeName = getObjectTypeName;
|
||||||
this.buildUploadIDs = buildUploadIDs;
|
|
||||||
this.getUpdatedObjects = getUpdatedObjects;
|
this.getUpdatedObjects = getUpdatedObjects;
|
||||||
this.addToUpdated = addToUpdated;
|
|
||||||
this.getDeletedObjects = getDeletedObjects;
|
this.getDeletedObjects = getDeletedObjects;
|
||||||
this.purgeDeletedObjects = purgeDeletedObjects;
|
this.purgeDeletedObjects = purgeDeletedObjects;
|
||||||
this.removeFromDeleted = removeFromDeleted;
|
|
||||||
|
|
||||||
// Keep in sync with syncObjectTypes table
|
// Keep in sync with syncObjectTypes table
|
||||||
this.__defineGetter__('syncObjects', function () {
|
this.__defineGetter__('syncObjects', function () {
|
||||||
|
@ -80,25 +77,6 @@ Zotero.Sync = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildUploadIDs() {
|
|
||||||
var uploadIDs = {};
|
|
||||||
|
|
||||||
uploadIDs.updated = {};
|
|
||||||
uploadIDs.changed = {};
|
|
||||||
uploadIDs.deleted = {};
|
|
||||||
|
|
||||||
for each(var syncObject in Zotero.Sync.syncObjects) {
|
|
||||||
var types = syncObject.plural.toLowerCase(); // 'items'
|
|
||||||
|
|
||||||
uploadIDs.updated[types] = [];
|
|
||||||
uploadIDs.changed[types] = {};
|
|
||||||
uploadIDs.deleted[types] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadIDs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param object lastSyncDate JS Date object
|
* @param object lastSyncDate JS Date object
|
||||||
* @return object { items: [123, 234, ...], creators: [321, 432, ...], ... }
|
* @return object { items: [123, 234, ...], creators: [321, 432, ...], ... }
|
||||||
|
@ -125,28 +103,6 @@ Zotero.Sync = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function addToUpdated(updated, ids) {
|
|
||||||
ids = Zotero.flattenArguments(ids);
|
|
||||||
for each(var id in ids) {
|
|
||||||
if (updated.indexOf(id) == -1) {
|
|
||||||
updated.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.removeFromUpdated = function (updated, ids) {
|
|
||||||
ids = Zotero.flattenArguments(ids);
|
|
||||||
var index;
|
|
||||||
for each(var id in ids) {
|
|
||||||
index = updated.indexOf(id);
|
|
||||||
if (index != -1) {
|
|
||||||
updated.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param object lastSyncDate JS Date object
|
* @param object lastSyncDate JS Date object
|
||||||
* @return mixed Returns object with deleted ids
|
* @return mixed Returns object with deleted ids
|
||||||
|
@ -217,16 +173,6 @@ Zotero.Sync = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeFromDeleted(deleted, id, key) {
|
|
||||||
for (var i=0; i<deleted.length; i++) {
|
|
||||||
if (deleted[i].id == id && deleted[i].key == key) {
|
|
||||||
deleted.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function _loadObjectTypes() {
|
function _loadObjectTypes() {
|
||||||
var sql = "SELECT * FROM syncObjectTypes";
|
var sql = "SELECT * FROM syncObjectTypes";
|
||||||
var types = Zotero.DB.query(sql);
|
var types = Zotero.DB.query(sql);
|
||||||
|
@ -804,14 +750,14 @@ Zotero.Sync.Server = new function () {
|
||||||
var lastLocalSyncDate = lastLocalSyncTime ?
|
var lastLocalSyncDate = lastLocalSyncTime ?
|
||||||
new Date(lastLocalSyncTime * 1000) : false;
|
new Date(lastLocalSyncTime * 1000) : false;
|
||||||
|
|
||||||
var uploadIDs = Zotero.Sync.buildUploadIDs();
|
var syncSession = new Zotero.Sync.Server.Session;
|
||||||
uploadIDs.updated = Zotero.Sync.getUpdatedObjects(lastLocalSyncDate);
|
syncSession.uploadIDs.updated = Zotero.Sync.getUpdatedObjects(lastLocalSyncDate);
|
||||||
var deleted = Zotero.Sync.getDeletedObjects(lastLocalSyncDate);
|
var deleted = Zotero.Sync.getDeletedObjects(lastLocalSyncDate);
|
||||||
if (deleted == -1) {
|
if (deleted == -1) {
|
||||||
_error('Sync delete log starts after last sync date in Zotero.Sync.Server.sync()');
|
_error('Sync delete log starts after last sync date in Zotero.Sync.Server.sync()');
|
||||||
}
|
}
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
uploadIDs.deleted = deleted;
|
syncSession.uploadIDs.deleted = deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextLocalSyncDate = Zotero.DB.transactionDate;
|
var nextLocalSyncDate = Zotero.DB.transactionDate;
|
||||||
|
@ -821,7 +767,7 @@ Zotero.Sync.Server = new function () {
|
||||||
// Reconcile and save updated data from server and
|
// Reconcile and save updated data from server and
|
||||||
// prepare local data to upload
|
// prepare local data to upload
|
||||||
var xmlstr = Zotero.Sync.Server.Data.processUpdatedXML(
|
var xmlstr = Zotero.Sync.Server.Data.processUpdatedXML(
|
||||||
xml.updated, lastLocalSyncDate, uploadIDs
|
xml.updated, lastLocalSyncDate, syncSession
|
||||||
);
|
);
|
||||||
|
|
||||||
//Zotero.debug(xmlstr);
|
//Zotero.debug(xmlstr);
|
||||||
|
@ -830,7 +776,10 @@ Zotero.Sync.Server = new function () {
|
||||||
if (xmlstr === false) {
|
if (xmlstr === false) {
|
||||||
Zotero.debug("Sync cancelled");
|
Zotero.debug("Sync cancelled");
|
||||||
Zotero.DB.rollbackTransaction();
|
Zotero.DB.rollbackTransaction();
|
||||||
Zotero.Sync.Server.unlock();
|
Zotero.Sync.Server.unlock(function () {
|
||||||
|
Zotero.Sync.Runner.reset();
|
||||||
|
Zotero.Sync.Runner.next();
|
||||||
|
});
|
||||||
Zotero.reloadDataObjects();
|
Zotero.reloadDataObjects();
|
||||||
_syncInProgress = false;
|
_syncInProgress = false;
|
||||||
return;
|
return;
|
||||||
|
@ -1305,9 +1254,109 @@ Zotero.BufferedInputListener.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores information about a sync session
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @property {Object} uploadIDs IDs to be uploaded to server
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* updated: {
|
||||||
|
* items: [123, 234, 345, 456],
|
||||||
|
* creators: [321, 432, 543, 654]
|
||||||
|
* },
|
||||||
|
* changed: {
|
||||||
|
* items: {
|
||||||
|
* 1234: { oldID: 1234, newID: 5678 }, ...
|
||||||
|
* },
|
||||||
|
* creators: {
|
||||||
|
* 1234: { oldID: 1234, newID: 5678 }, ...
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* deleted: {
|
||||||
|
* items: [
|
||||||
|
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
|
||||||
|
* ],
|
||||||
|
* creators: [
|
||||||
|
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
Zotero.Sync.Server.Session = function () {
|
||||||
|
this.uploadIDs = {};
|
||||||
|
this.uploadIDs.updated = {};
|
||||||
|
this.uploadIDs.changed = {};
|
||||||
|
this.uploadIDs.deleted = {};
|
||||||
|
|
||||||
|
for each(var syncObject in Zotero.Sync.syncObjects) {
|
||||||
|
var types = syncObject.plural.toLowerCase(); // 'items'
|
||||||
|
|
||||||
|
this.uploadIDs.updated[types] = [];
|
||||||
|
this.uploadIDs.changed[types] = {};
|
||||||
|
this.uploadIDs.deleted[types] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Sync.Server.Session.prototype.addToUpdated = function (syncObjectTypeName, ids) {
|
||||||
|
var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
|
||||||
|
var updated = this.uploadIDs.updated[pluralType];
|
||||||
|
|
||||||
|
ids = Zotero.flattenArguments(ids);
|
||||||
|
for each(var id in ids) {
|
||||||
|
if (updated.indexOf(id) == -1) {
|
||||||
|
updated.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (syncObjectTypeName, ids) {
|
||||||
|
var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
|
||||||
|
var updated = this.uploadIDs.updated[pluralType];
|
||||||
|
|
||||||
|
ids = Zotero.flattenArguments(ids);
|
||||||
|
var index;
|
||||||
|
for each(var id in ids) {
|
||||||
|
index = updated.indexOf(id);
|
||||||
|
if (index != -1) {
|
||||||
|
updated.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, id, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleted.push({ id: id, key: key});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, id, 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) {
|
||||||
|
deleted.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Zotero.Sync.Server.Data = new function() {
|
Zotero.Sync.Server.Data = new function() {
|
||||||
this.processUpdatedXML = processUpdatedXML;
|
this.processUpdatedXML = processUpdatedXML;
|
||||||
this.buildUploadXML = buildUploadXML;
|
|
||||||
this.itemToXML = itemToXML;
|
this.itemToXML = itemToXML;
|
||||||
this.xmlToItem = xmlToItem;
|
this.xmlToItem = xmlToItem;
|
||||||
this.removeMissingRelatedItems = removeMissingRelatedItems;
|
this.removeMissingRelatedItems = removeMissingRelatedItems;
|
||||||
|
@ -1392,10 +1441,10 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) {
|
function processUpdatedXML(xml, lastLocalSyncDate, syncSession) {
|
||||||
if (xml.children().length() == 0) {
|
if (xml.children().length() == 0) {
|
||||||
Zotero.debug('No changes received from server');
|
Zotero.debug('No changes received from server');
|
||||||
return Zotero.Sync.Server.Data.buildUploadXML(uploadIDs);
|
return Zotero.Sync.Server.Data.buildUploadXML(syncSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
xml = _preprocessUpdatedXML(xml);
|
xml = _preprocessUpdatedXML(xml);
|
||||||
|
@ -1404,9 +1453,6 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
var relatedItemsStore = {};
|
var relatedItemsStore = {};
|
||||||
var itemStorageModTimes = {};
|
var itemStorageModTimes = {};
|
||||||
|
|
||||||
//Zotero.debug("Updated IDs:");
|
|
||||||
//Zotero.debug(uploadIDs);
|
|
||||||
|
|
||||||
Zotero.DB.beginTransaction();
|
Zotero.DB.beginTransaction();
|
||||||
|
|
||||||
for each(var syncObject in Zotero.Sync.syncObjects) {
|
for each(var syncObject in Zotero.Sync.syncObjects) {
|
||||||
|
@ -1419,10 +1465,8 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var toSaveParents = [];
|
var toSave = [];
|
||||||
var toSaveChildren = [];
|
var toDelete = [];
|
||||||
var toDeleteParents = [];
|
|
||||||
var toDeleteChildren = [];
|
|
||||||
var toReconcile = [];
|
var toReconcile = [];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -1452,7 +1496,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// date equal to Zotero.Sync.Server.nextLocalSyncDate
|
// date equal to Zotero.Sync.Server.nextLocalSyncDate
|
||||||
// and therefore excluded above (example: an item
|
// and therefore excluded above (example: an item
|
||||||
// linked to a creator whose id changed)
|
// linked to a creator whose id changed)
|
||||||
|| uploadIDs.updated[types].indexOf(obj.id) != -1) {
|
|| syncSession.uploadIDs[types].indexOf(obj.id) != -1) {
|
||||||
|
|
||||||
// Merge and store related items, since CR doesn't
|
// Merge and store related items, since CR doesn't
|
||||||
// affect related items
|
// affect related items
|
||||||
|
@ -1477,7 +1521,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// Some types we don't bother to reconcile
|
// Some types we don't bother to reconcile
|
||||||
if (_noMergeTypes.indexOf(type) != -1) {
|
if (_noMergeTypes.indexOf(type) != -1) {
|
||||||
if (obj.dateModified > remoteObj.dateModified) {
|
if (obj.dateModified > remoteObj.dateModified) {
|
||||||
Zotero.Sync.addToUpdated(uploadIDs.updated.items, obj.id);
|
syncSession.addToUpdated(type, obj.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1487,45 +1531,58 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
else {
|
else {
|
||||||
// Skip item if dateModified is the only modified
|
// Skip item if dateModified is the only modified
|
||||||
// field (and no linked creators changed)
|
// field (and no linked creators changed)
|
||||||
if (type == 'item') {
|
switch (type) {
|
||||||
var diff = obj.diff(remoteObj, false, true);
|
// Will be handled by item CR for now
|
||||||
if (!diff) {
|
case 'creator':
|
||||||
// Check if creators changed
|
remoteCreatorStore[remoteObj.id] = remoteObj;
|
||||||
var creatorsChanged = false;
|
syncSession.removeFromUpdated(type, obj.id);
|
||||||
|
continue;
|
||||||
|
|
||||||
var creators = obj.getCreators();
|
case 'item':
|
||||||
var remoteCreators = remoteObj.getCreators();
|
var diff = obj.diff(remoteObj, false, true);
|
||||||
|
if (!diff) {
|
||||||
if (creators.length != remoteCreators.length) {
|
// Check if creators changed
|
||||||
creatorsChanged = true;
|
var creatorsChanged = false;
|
||||||
}
|
|
||||||
else {
|
var creators = obj.getCreators();
|
||||||
creators = creators.concat(remoteCreators);
|
var remoteCreators = remoteObj.getCreators();
|
||||||
for each(var creator in creators) {
|
|
||||||
var r = remoteCreatorStore[creator.ref.id];
|
if (creators.length != remoteCreators.length) {
|
||||||
// Doesn't include dateModified
|
creatorsChanged = true;
|
||||||
if (r && !r.equals(creator.ref)) {
|
}
|
||||||
creatorsChanged = true;
|
else {
|
||||||
break;
|
creators = creators.concat(remoteCreators);
|
||||||
|
for each(var creator in creators) {
|
||||||
|
var r = remoteCreatorStore[creator.ref.id];
|
||||||
|
// Doesn't include dateModified
|
||||||
|
if (r && !r.equals(creator.ref)) {
|
||||||
|
creatorsChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!creatorsChanged) {
|
||||||
|
syncSession.removeFromUpdated(type, obj.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!creatorsChanged) {
|
break;
|
||||||
|
|
||||||
|
case 'collection':
|
||||||
|
case 'tag':
|
||||||
|
var diff = obj.diff(remoteObj, false, true);
|
||||||
|
if (!diff) {
|
||||||
|
syncSession.removeFromUpdated(type, obj.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
|
default:
|
||||||
// Will be handled by item CR for now
|
Zotero.debug(obj);
|
||||||
if (type == 'creator') {
|
Zotero.debug(remoteObj);
|
||||||
remoteCreatorStore[remoteObj.id] = remoteObj;
|
var msg = "Reconciliation unimplemented for " + types;
|
||||||
continue;
|
alert(msg);
|
||||||
}
|
throw(msg);
|
||||||
|
|
||||||
if (type != 'item') {
|
|
||||||
var msg = "Reconciliation unimplemented for " + types;
|
|
||||||
alert(msg);
|
|
||||||
throw(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.isAttachment()) {
|
if (obj.isAttachment()) {
|
||||||
|
@ -1566,15 +1623,15 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
//
|
//
|
||||||
// Object might not appear in local update array if server
|
// Object might not appear in local update array if server
|
||||||
// data was cleared and synched from another client
|
// data was cleared and synched from another client
|
||||||
var index = uploadIDs.updated[types].indexOf(oldID);
|
var index = syncSession.uploadIDs.updated[types].indexOf(oldID);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
uploadIDs.updated[types][index] = newID;
|
syncSession.uploadIDs.updated[types][index] = newID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update id in local deletions array
|
// Update id in local deletions array
|
||||||
for (var i in uploadIDs.deleted[types]) {
|
for (var i in syncSession.uploadIDs.deleted[types]) {
|
||||||
if (uploadIDs.deleted[types][i].id == oldID) {
|
if (syncSession.uploadIDs.deleted[types][i].id == oldID) {
|
||||||
uploadIDs.deleted[types][i] = newID;
|
syncSession.uploadIDs.deleted[types][i] = newID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1587,11 +1644,11 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
if (type == 'creator') {
|
if (type == 'creator') {
|
||||||
var linkedItems = obj.getLinkedItems();
|
var linkedItems = obj.getLinkedItems();
|
||||||
if (linkedItems) {
|
if (linkedItems) {
|
||||||
Zotero.Sync.addToUpdated(uploadIDs.updated.items, linkedItems);
|
syncSession.addToUpdated('item', linkedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadIDs.changed[types][oldID] = {
|
syncSession.uploadIDs.changed[types][oldID] = {
|
||||||
oldID: oldID,
|
oldID: oldID,
|
||||||
newID: newID
|
newID: newID
|
||||||
};
|
};
|
||||||
|
@ -1605,7 +1662,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
isNewObject = true;
|
isNewObject = true;
|
||||||
|
|
||||||
// Check if object has been deleted locally
|
// Check if object has been deleted locally
|
||||||
for each(var pair in uploadIDs.deleted[types]) {
|
for each(var pair in syncSession.uploadIDs.deleted[types]) {
|
||||||
if (pair.id != parseInt(xmlNode.@id) ||
|
if (pair.id != parseInt(xmlNode.@id) ||
|
||||||
pair.key != xmlNode.@key.toString()) {
|
pair.key != xmlNode.@key.toString()) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1630,7 +1687,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
+ " from '" + oldKey + "' to '" + newKey + "'", 2);
|
+ " from '" + oldKey + "' to '" + newKey + "'", 2);
|
||||||
keyObj.key = newKey;
|
keyObj.key = newKey;
|
||||||
keyObj.save();
|
keyObj.save();
|
||||||
Zotero.Sync.addToUpdated(uploadIDs.updated[types], keyObj.id);
|
syncSession.addToUpdated(type, keyObj.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1652,13 +1709,13 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
var tagName = xmlNode.@name.toString();
|
var tagName = xmlNode.@name.toString();
|
||||||
var tagType = xmlNode.@type.toString()
|
var tagType = xmlNode.@type.toString()
|
||||||
? parseInt(xmlNode.@type) : 0;
|
? parseInt(xmlNode.@type) : 0;
|
||||||
var linkedItems = _deleteConflictingTag(tagName, tagType, uploadIDs);
|
var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType);
|
||||||
if (linkedItems) {
|
if (linkedItems) {
|
||||||
obj.dateModified = Zotero.DB.transactionDateTime;
|
obj.dateModified = Zotero.DB.transactionDateTime;
|
||||||
for each(var id in linkedItems) {
|
for each(var id in linkedItems) {
|
||||||
obj.addItem(id);
|
obj.addItem(id);
|
||||||
}
|
}
|
||||||
Zotero.Sync.addToUpdated(uploadIDs.updated.tags, parseInt(xmlNode.@id));
|
syncSession.addToUpdated('tag', parseInt(xmlNode.@id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1670,12 +1727,8 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
obj
|
obj
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// Child items have to be saved after parent items
|
|
||||||
else if (type == 'item' && obj.getSource()) {
|
|
||||||
toSaveChildren.push(obj);
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
toSaveParents.push(obj);
|
toSave.push(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't use assigned-but-unsaved ids for new ids
|
// Don't use assigned-but-unsaved ids for new ids
|
||||||
|
@ -1689,11 +1742,10 @@ 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) {
|
||||||
Zotero.Sync.removeFromDeleted(
|
syncSession.removeFromDeleted(
|
||||||
uploadIDs.deleted.creators,
|
'creator',
|
||||||
creator.ref.id,
|
creator.ref.id,
|
||||||
creator.ref.key,
|
creator.ref.key
|
||||||
true
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1741,12 +1793,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
}
|
}
|
||||||
// Local object hasn't been modified -- delete
|
// Local object hasn't been modified -- delete
|
||||||
else {
|
else {
|
||||||
if (type == 'item' && obj.getSource()) {
|
toDelete.push(id);
|
||||||
toDeleteChildren.push(id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toDeleteParents.push(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1755,80 +1802,20 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// Reconcile objects that have changed locally and remotely
|
// Reconcile objects that have changed locally and remotely
|
||||||
//
|
//
|
||||||
if (toReconcile.length) {
|
if (toReconcile.length) {
|
||||||
var io = {
|
var mergeData = _reconcile(type, toReconcile, remoteCreatorStore);
|
||||||
dataIn: {
|
if (!mergeData) {
|
||||||
captions: [
|
// TODO: throw?
|
||||||
// TODO: localize
|
|
||||||
'Local Item',
|
|
||||||
'Remote Item',
|
|
||||||
'Merged Item'
|
|
||||||
],
|
|
||||||
objects: toReconcile
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type == 'item') {
|
|
||||||
io.dataIn.changedCreators = remoteCreatorStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
||||||
.getService(Components.interfaces.nsIWindowMediator);
|
|
||||||
var lastWin = wm.getMostRecentWindow("navigator:browser");
|
|
||||||
lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
|
|
||||||
|
|
||||||
if (io.dataOut) {
|
|
||||||
for each(var obj in io.dataOut) {
|
|
||||||
// TODO: do we need to make sure item isn't already being saved?
|
|
||||||
|
|
||||||
// Handle items deleted during merge
|
|
||||||
if (obj.ref == 'deleted') {
|
|
||||||
// Deleted item was remote
|
|
||||||
if (obj.left != 'deleted') {
|
|
||||||
if (type == 'item' && obj.left.getSource()) {
|
|
||||||
toDeleteParents.push(obj.id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toDeleteChildren.push(obj.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relatedItemsStore[obj.id]) {
|
|
||||||
delete relatedItemsStore[obj.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadIDs.deleted[types].push({
|
|
||||||
id: obj.id,
|
|
||||||
key: obj.left.key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == 'item' && obj.ref.getSource()) {
|
|
||||||
toSaveParents.push(obj.ref);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toSaveChildren.push(obj.ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't use assigned-but-unsaved ids for new ids
|
|
||||||
Zotero.ID.skip(types, obj.id);
|
|
||||||
|
|
||||||
// Item had been deleted locally, so remove from
|
|
||||||
// deleted array
|
|
||||||
if (obj.left == 'deleted') {
|
|
||||||
Zotero.Sync.removeFromDeleted(uploadIDs.deleted[types], obj.id, obj.ref.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: only upload if the local item was chosen
|
|
||||||
// or remote item was changed
|
|
||||||
|
|
||||||
Zotero.Sync.addToUpdated(uploadIDs.updated[types], obj.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Zotero.DB.rollbackTransaction();
|
Zotero.DB.rollbackTransaction();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_processMergeData(
|
||||||
|
syncSession,
|
||||||
|
type,
|
||||||
|
mergeData,
|
||||||
|
toSave,
|
||||||
|
toDelete,
|
||||||
|
relatedItemsStore
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1836,7 +1823,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// Temporarily remove and store subcollections before saving
|
// Temporarily remove and store subcollections before saving
|
||||||
// since referenced collections may not exist yet
|
// since referenced collections may not exist yet
|
||||||
var collections = [];
|
var collections = [];
|
||||||
for each(var obj in toSaveParents) {
|
for each(var obj in toSave) {
|
||||||
var colIDs = obj.getChildCollections(true);
|
var colIDs = obj.getChildCollections(true);
|
||||||
// TODO: use exist(), like related items above
|
// TODO: use exist(), like related items above
|
||||||
obj.childCollections = [];
|
obj.childCollections = [];
|
||||||
|
@ -1850,10 +1837,18 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
|
|
||||||
// Save objects
|
// Save objects
|
||||||
Zotero.debug('Saving merged ' + types);
|
Zotero.debug('Saving merged ' + types);
|
||||||
for each(var obj in toSaveParents) {
|
// Save parent items first
|
||||||
obj.save();
|
if (type == 'item') {
|
||||||
|
for each(var obj in toSave) {
|
||||||
|
if (!obj.getSource()) {
|
||||||
|
obj.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for each(var obj in toSaveChildren) {
|
for each(var obj in toSave) {
|
||||||
|
if (type == 'item' && !obj.getSource()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
obj.save();
|
obj.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1882,17 +1877,37 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
Zotero.debug('Deleting merged ' + types);
|
Zotero.debug('Deleting merged ' + types);
|
||||||
if (toDeleteChildren.length) {
|
if (toDelete.length) {
|
||||||
Zotero.Sync.EventListener.ignoreDeletions(type, toDeleteChildren);
|
// Items have to be deleted children-first
|
||||||
Zotero[Types].erase(toDeleteChildren);
|
if (type == 'item') {
|
||||||
Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteChildren);
|
var parents = [];
|
||||||
|
var children = [];
|
||||||
|
for each(var id in toDelete) {
|
||||||
|
var item = Zotero.Items.get(id);
|
||||||
|
if (item.getSource()) {
|
||||||
|
children.push(item.id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parents.push(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (children.length) {
|
||||||
|
Zotero.Sync.EventListener.ignoreDeletions('item', children);
|
||||||
|
Zotero.Items.erase(children);
|
||||||
|
Zotero.Sync.EventListener.unignoreDeletions('item', children);
|
||||||
|
}
|
||||||
|
if (parents.length) {
|
||||||
|
Zotero.Sync.EventListener.ignoreDeletions('item', parents);
|
||||||
|
Zotero.Items.erase(parents);
|
||||||
|
Zotero.Sync.EventListener.unignoreDeletions('item', parents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.Sync.EventListener.ignoreDeletions(type, toDelete);
|
||||||
|
Zotero[Types].erase(toDelete);
|
||||||
|
Zotero.Sync.EventListener.unignoreDeletions(type, toDelete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (toDeleteParents.length) {
|
|
||||||
Zotero.Sync.EventListener.ignoreDeletions(type, toDeleteParents);
|
|
||||||
Zotero[Types].erase(toDeleteParents);
|
|
||||||
Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteParents);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Check mod times of updated items against stored time to see
|
// Check mod times of updated items against stored time to see
|
||||||
// if they've been updated elsewhere and mark for download if so
|
// if they've been updated elsewhere and mark for download if so
|
||||||
|
@ -1907,7 +1922,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(uploadIDs);
|
var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(syncSession);
|
||||||
|
|
||||||
//Zotero.debug(xmlstr);
|
//Zotero.debug(xmlstr);
|
||||||
//throw ('break');
|
//throw ('break');
|
||||||
|
@ -1919,30 +1934,11 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ids = {
|
* @param {Zotero.Sync.Server.Session} syncSession
|
||||||
* updated: {
|
|
||||||
* items: [123, 234, 345, 456],
|
|
||||||
* creators: [321, 432, 543, 654]
|
|
||||||
* },
|
|
||||||
* changed: {
|
|
||||||
* items: {
|
|
||||||
* oldID: { oldID: 1234, newID: 5678 }, ...
|
|
||||||
* },
|
|
||||||
* creators: {
|
|
||||||
* oldID: { oldID: 1234, newID: 5678 }, ...
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* deleted: {
|
|
||||||
* items: [
|
|
||||||
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
|
|
||||||
* ],
|
|
||||||
* creators: [
|
|
||||||
* { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ...
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
*/
|
*/
|
||||||
function buildUploadXML(ids) {
|
this.buildUploadXML = function (syncSession) {
|
||||||
|
var ids = syncSession.uploadIDs;
|
||||||
|
|
||||||
var xml = <data/>
|
var xml = <data/>
|
||||||
|
|
||||||
// Add API version attribute
|
// Add API version attribute
|
||||||
|
@ -1968,7 +1964,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
case 'item':
|
case 'item':
|
||||||
var objs = Zotero[Types].get(ids.updated[types]);
|
var objs = Zotero[Types].get(ids.updated[types]);
|
||||||
for each(var obj in objs) {
|
for each(var obj in objs) {
|
||||||
xml[types][type] += this[type + 'ToXML'](obj);
|
xml[types][type] += this[type + 'ToXML'](obj, syncSession);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -2013,9 +2009,87 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a Zotero.Item object to an E4X <item> object
|
* Open a conflict resolution window and return the results
|
||||||
|
*
|
||||||
|
* @param {String} type 'item', 'collection', etc.
|
||||||
|
* @param {Array[]} objectPairs Array of arrays of pairs of Item, Collection, etc.
|
||||||
*/
|
*/
|
||||||
function itemToXML(item) {
|
function _reconcile(type, objectPairs, changedCreators) {
|
||||||
|
var io = {
|
||||||
|
dataIn: {
|
||||||
|
captions: [
|
||||||
|
// TODO: localize
|
||||||
|
'Local Item',
|
||||||
|
'Remote Item',
|
||||||
|
'Merged Item'
|
||||||
|
],
|
||||||
|
objects: objectPairs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == 'item') {
|
||||||
|
io.dataIn.changedCreators = changedCreators;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
||||||
|
.getService(Components.interfaces.nsIWindowMediator);
|
||||||
|
var lastWin = wm.getMostRecentWindow("navigator:browser");
|
||||||
|
lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
|
||||||
|
|
||||||
|
return io.dataOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the results of conflict resolution
|
||||||
|
*/
|
||||||
|
function _processMergeData(syncSession, type, data, toSave, toDelete, relatedItems) {
|
||||||
|
var types = Zotero.Sync.syncObjects[type].plural.toLowerCase();
|
||||||
|
|
||||||
|
for each(var obj in data) {
|
||||||
|
// TODO: do we need to make sure item isn't already being saved?
|
||||||
|
|
||||||
|
// Handle items deleted during merge
|
||||||
|
if (obj.ref == 'deleted') {
|
||||||
|
// Deleted item was remote
|
||||||
|
if (obj.left != 'deleted') {
|
||||||
|
toDelete.push(obj.id);
|
||||||
|
|
||||||
|
if (relatedItems[obj.id]) {
|
||||||
|
delete relatedItems[obj.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
syncSession.addToDeleted(type, obj.id, obj.left.key);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toSave.push(obj.ref);
|
||||||
|
|
||||||
|
// Don't use assigned-but-unsaved ids for new ids
|
||||||
|
Zotero.ID.skip(types, obj.id);
|
||||||
|
|
||||||
|
// Item had been deleted locally, so remove from
|
||||||
|
// deleted array
|
||||||
|
if (obj.left == 'deleted') {
|
||||||
|
syncSession.removeFromDeleted(type, obj.id, obj.ref.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: only upload if the local item was chosen
|
||||||
|
// or remote item was changed
|
||||||
|
|
||||||
|
syncSession.addToUpdated(type, obj.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Zotero.Item object to an E4X <item> object
|
||||||
|
*
|
||||||
|
* @param {Zotero.Item} item
|
||||||
|
* @param {Zotero.Sync.Server.Session} [syncSession]
|
||||||
|
*/
|
||||||
|
function itemToXML(item, syncSession) {
|
||||||
var xml = <item/>;
|
var xml = <item/>;
|
||||||
var item = item.serialize();
|
var item = item.serialize();
|
||||||
|
|
||||||
|
@ -2083,10 +2157,21 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// Creators
|
// Creators
|
||||||
for (var index in item.creators) {
|
for (var index in item.creators) {
|
||||||
var newCreator = <creator/>;
|
var newCreator = <creator/>;
|
||||||
newCreator.@id = item.creators[index].creatorID;
|
var creatorID = item.creators[index].creatorID;
|
||||||
|
newCreator.@id = creatorID;
|
||||||
newCreator.@creatorType = item.creators[index].creatorType;
|
newCreator.@creatorType = item.creators[index].creatorType;
|
||||||
newCreator.@index = index;
|
newCreator.@index = index;
|
||||||
xml.creator += newCreator;
|
xml.creator += newCreator;
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Add creator XML as glue if not already included in sync session
|
||||||
|
if (syncSession &&
|
||||||
|
syncSession.uploadIDs.updated.creators.indexOf(creatorID) == -1) {
|
||||||
|
var creator = Zotero.Creators.get(creatorID);
|
||||||
|
var creatorXML = Zotero.Sync.Server.Data.creatorToXML(creator);
|
||||||
|
xml.creator.creator = creatorXML;
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// Related items
|
// Related items
|
||||||
|
@ -2315,7 +2400,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
collection.name = xmlCollection.@name.toString();
|
collection.name = xmlCollection.@name.toString();
|
||||||
if (!skipPrimary) {
|
if (!skipPrimary) {
|
||||||
collection.parent = xmlCollection.@parent.toString() ?
|
collection.parent = xmlCollection.@parent.toString() ?
|
||||||
parseInt(xmlCollection.@parent) : false;
|
parseInt(xmlCollection.@parent) : null;
|
||||||
collection.dateAdded = xmlCollection.@dateAdded.toString();
|
collection.dateAdded = xmlCollection.@dateAdded.toString();
|
||||||
collection.dateModified = xmlCollection.@dateModified.toString();
|
collection.dateModified = xmlCollection.@dateModified.toString();
|
||||||
collection.key = xmlCollection.@key.toString();
|
collection.key = xmlCollection.@key.toString();
|
||||||
|
@ -2600,7 +2685,7 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
* deleted tag, or FALSE if no
|
* deleted tag, or FALSE if no
|
||||||
* matching tag found
|
* matching tag found
|
||||||
*/
|
*/
|
||||||
function _deleteConflictingTag(name, type, uploadIDs) {
|
function _deleteConflictingTag(syncSession, name, type) {
|
||||||
var tagID = Zotero.Tags.getID(name, type);
|
var tagID = Zotero.Tags.getID(name, type);
|
||||||
if (tagID) {
|
if (tagID) {
|
||||||
var tag = Zotero.Tags.get(tagID);
|
var tag = Zotero.Tags.get(tagID);
|
||||||
|
@ -2609,14 +2694,8 @@ Zotero.Sync.Server.Data = new function() {
|
||||||
// DEBUG: should purge() be called by Tags.erase()
|
// DEBUG: should purge() be called by Tags.erase()
|
||||||
Zotero.Tags.purge();
|
Zotero.Tags.purge();
|
||||||
|
|
||||||
Zotero.Sync.removeFromUpdated(
|
syncSession.removeFromUpdated('tag', tagID);
|
||||||
uploadIDs.updated.tags, tagID
|
syncSession.addToDeleted('tag', tagID, tag.key);
|
||||||
);
|
|
||||||
|
|
||||||
uploadIDs.deleted.tags.push({
|
|
||||||
id: tagID,
|
|
||||||
key: tag.key
|
|
||||||
});
|
|
||||||
|
|
||||||
return linkedItems;
|
return linkedItems;
|
||||||
}
|
}
|
||||||
|
|
|
@ -290,6 +290,31 @@ Zotero.Utilities.prototype.isInt = function(x) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares an array with another (comparator) and returns an array with
|
||||||
|
* the values from comparator that don't exist in vector
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the index of the comparator's elements;
|
||||||
|
* otherwise returns 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random integer between min and max inclusive
|
* Generate a random integer between min and max inclusive
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
-- 44
|
-- 45
|
||||||
|
|
||||||
-- 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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue