Close , [Async DB] Update long tag fixer

This commit is contained in:
Dan Stillman 2016-04-21 11:08:48 -04:00
parent e2cbfbd0fe
commit e7d27ee0f3
7 changed files with 349 additions and 162 deletions
chrome
test/tests

View File

@ -132,7 +132,7 @@ var Zotero_Long_Tag_Fixer = new function () {
this.updateEditLength = function (len) { this.updateEditLength = function (len) {
document.getElementById('zotero-new-tag-character-count').value = len; document.getElementById('zotero-new-tag-character-count').value = len;
var invalid = len == 0 || len > 255; var invalid = len == 0 || len > Zotero.Tags.MAX_SYNC_LENGTH;
document.getElementById('zotero-new-tag-characters').setAttribute('invalid', invalid); document.getElementById('zotero-new-tag-characters').setAttribute('invalid', invalid);
document.getElementById('zotero-long-tag-fixer').getButton('accept').disabled = invalid; document.getElementById('zotero-long-tag-fixer').getButton('accept').disabled = invalid;
} }
@ -146,11 +146,9 @@ var Zotero_Long_Tag_Fixer = new function () {
this.save = function () { this.save = function () {
try { try {
var index = document.getElementById('zotero-new-tag-actions').selectedIndex; var result = {};
// Search for all matching tags across all libraries var index = document.getElementById('zotero-new-tag-actions').selectedIndex;
var sql = "SELECT tagID FROM tags WHERE name=?";
var oldTagIDs = Zotero.DB.columnQuery(sql, _oldTag);
switch (index) { switch (index) {
// Split // Split
@ -166,48 +164,23 @@ var Zotero_Long_Tag_Fixer = new function () {
} }
} }
Zotero.DB.beginTransaction(); result.op = 'split';
result.tags = newTags;
// Add new tags to all items linked to each matching old tag
for (var i=0; i<oldTagIDs.length; i++) {
var tag = Zotero.Tags.get(oldTagIDs[i]);
var items = tag.getLinkedItems();
if (items) {
for (var j=0; j<items.length; j++) {
items[j].addTags(newTags, tag.type);
}
}
}
// Remove old tags
// TODO: Update
Zotero.Tags.erase(oldTagIDs);
Zotero.Tags.purge();
Zotero.DB.commitTransaction();
break; break;
// Edit // Edit
case 1: case 1:
var value = document.getElementById('zotero-new-tag-editor').value; result.op = 'edit';
Zotero.DB.beginTransaction(); result.tag = document.getElementById('zotero-new-tag-editor').value;
for (var i=0; i<oldTagIDs.length; i++) {
var tag = Zotero.Tags.get(oldTagIDs[i]);
tag.name = value;
tag.save();
}
Zotero.DB.commitTransaction();
break; break;
// Delete // Delete
case 2: case 2:
Zotero.DB.beginTransaction(); result.op = 'delete';
Zotero.Tags.erase(oldTagIDs);
Zotero.Tags.purge();
Zotero.DB.commitTransaction();
break; break;
} }
_dataOut.result = true; _dataOut.result = result;
} }
catch (e) { catch (e) {

View File

@ -29,6 +29,7 @@
*/ */
Zotero.Tags = new function() { Zotero.Tags = new function() {
this.MAX_COLORED_TAGS = 6; this.MAX_COLORED_TAGS = 6;
this.MAX_SYNC_LENGTH = 255;
var _initialized = false; var _initialized = false;
var _tagsByID = new Map(); var _tagsByID = new Map();
@ -123,6 +124,15 @@ Zotero.Tags = new function() {
}); });
this.getLongTagsInLibrary = Zotero.Promise.coroutine(function* (libraryID) {
var sql = "SELECT DISTINCT tagID FROM tags "
+ "JOIN itemTags USING (tagID) "
+ "JOIN items USING (itemID) "
+ "WHERE libraryID=? AND LENGTH(name)>?"
return yield Zotero.DB.columnQueryAsync(sql, [libraryID, this.MAX_SYNC_LENGTH]);
});
/** /**
* Get all tags in library * Get all tags in library
* *

View File

@ -339,36 +339,6 @@ Zotero.Sync.Server = new function () {
} }
break; break;
case 'TAG_TOO_LONG':
if (!Zotero.Sync.Runner.background) {
var tag = xmlhttp.responseXML.firstChild.getElementsByTagName('tag');
if (tag.length) {
var tag = tag[0].firstChild.nodeValue;
setTimeout(function () {
var callback = function () {
var sql = "SELECT DISTINCT name FROM tags WHERE LENGTH(name)>255 LIMIT 1";
var tag = Zotero.DB.valueQuery(sql);
if (tag) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
var dataOut = { result: null };
lastWin.openDialog('chrome://zotero/content/longTagFixer.xul', '', 'chrome,modal,centerscreen', tag, dataOut);
if (dataOut.result) {
callback();
}
}
else {
Zotero.Sync.Runner.sync();
}
};
callback();
}, 1);
}
}
break;
// We can't reproduce it, but we can fix it // We can't reproduce it, but we can fix it
case 'WRONG_LIBRARY_TAG_ITEM': case 'WRONG_LIBRARY_TAG_ITEM':
var background = Zotero.Sync.Runner.background; var background = Zotero.Sync.Runner.background;

View File

@ -905,10 +905,13 @@ Zotero.Sync.Data.Engine.prototype._uploadObjects = Zotero.Promise.coroutine(func
// Handle failed objects // Handle failed objects
for (let index in json.results.failed) { for (let index in json.results.failed) {
let { code, message } = json.results.failed[index]; let { code, message, data } = json.results.failed[index];
let e = new Error(message); let e = new Error(message);
e.name = "ZoteroUploadObjectError"; e.name = "ZoteroObjectUploadError";
e.code = code; e.code = code;
if (data) {
e.data = data;
}
Zotero.logError("Error for " + objectType + " " + batch[index].key + " in " Zotero.logError("Error for " + objectType + " " + batch[index].key + " in "
+ this.library.name + ":\n\n" + e); + this.library.name + ":\n\n" + e);

View File

@ -73,7 +73,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
this.getAPIClient = function (options = {}) { this.getAPIClient = function (options = {}) {
return new Zotero.Sync.APIClient({ return new Zotero.Sync.APIClient({
baseURL: this.baseURL, baseURL: this.baseURL,
apiVersion: this.apiVersion, apiVersion: this.apiVersion,
@ -141,7 +140,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
let emptyLibraryContinue = yield this.checkEmptyLibrary(keyInfo); let emptyLibraryContinue = yield this.checkEmptyLibrary(keyInfo);
if (!emptyLibraryContinue) { if (!emptyLibraryContinue) {
this.end(); yield this.end(options);
Zotero.debug("Syncing cancelled because user library is empty"); Zotero.debug("Syncing cancelled because user library is empty");
return false; return false;
} }
@ -168,7 +167,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
firstInSession: _firstInSession firstInSession: _firstInSession
}; };
let librariesToSync = yield this.checkLibraries( let librariesToSync = options.libraries = yield this.checkLibraries(
client, client,
options, options,
keyInfo, keyInfo,
@ -217,10 +216,17 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
} }
finally { finally {
this.end(); yield this.end(options);
}
Zotero.debug("Done syncing"); if (options.restartSync) {
delete options.restartSync;
Zotero.debug("Restarting sync");
yield this.sync(options);
return;
}
Zotero.debug("Done syncing");
}
/*if (results.changesMade) { /*if (results.changesMade) {
Zotero.debug("Changes made during file sync " Zotero.debug("Changes made during file sync "
@ -682,33 +688,14 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
this.end = function () { this.end = Zotero.Promise.coroutine(function* (options) {
this.updateIcons(_errors); yield this.checkErrors(_errors, options);
if (!options.restartSync) {
this.updateIcons(_errors);
}
_errors = []; _errors = [];
_syncInProgress = false; _syncInProgress = false;
} });
/**
* Log a warning, but don't throw an error
*/
this.warning = function (e) {
Zotero.debug(e, 2);
Components.utils.reportError(e);
e.errorType = 'warning';
_warning = e;
}
this.error = function (e) {
if (typeof e == 'string') {
e = new Error(e);
e.errorType = 'error';
}
Zotero.debug(e, 1);
this.updateIcons(e);
throw (e);
}
/** /**
@ -846,7 +833,17 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
this.checkError = function (e, background) { this.checkErrors = Zotero.Promise.coroutine(function* (errors, options = {}) {
for (let e of errors) {
let handled = yield this.checkError(e, options);
if (handled) {
break;
}
}
});
this.checkError = Zotero.Promise.coroutine(function* (e, options = {}) {
if (e.name && e.name == 'Zotero Error') { if (e.name && e.name == 'Zotero Error') {
switch (e.error) { switch (e.error) {
case Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET: case Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET:
@ -859,9 +856,9 @@ Zotero.Sync.Runner_Module = function (options = {}) {
var msg = Zotero.getString('sync.error.invalidLogin.text'); var msg = Zotero.getString('sync.error.invalidLogin.text');
e.message = msg; e.message = msg;
e.data = {}; e.data = {};
e.data.dialogText = msg; e.dialogText = msg;
e.data.dialogButtonText = Zotero.getString('sync.openSyncPreferences'); e.dialogButtonText = Zotero.getString('sync.openSyncPreferences');
e.data.dialogButtonCallback = function () { e.dialogButtonCallback = function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator); .getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser"); var win = wm.getMostRecentWindow("navigator:browser");
@ -905,6 +902,83 @@ Zotero.Sync.Runner_Module = function (options = {}) {
break; break;
} }
} }
else if (e.name && e.name == 'ZoteroObjectUploadError') {
// Tag too long
if (e.code == 413 && e.data && e.data.tag !== undefined) {
// Show long tag fixer and handle result
e.dialogButtonText = Zotero.getString('general.fix');
e.dialogButtonCallback = Zotero.Promise.coroutine(function* () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
// Open long tag fixer for every long tag in every editable library we're syncing
var editableLibraries = options.libraries
.filter(x => Zotero.Libraries.get(x).editable);
for (let libraryID of editableLibraries) {
let oldTagIDs = yield Zotero.Tags.getLongTagsInLibrary(libraryID);
for (let oldTagID of oldTagIDs) {
let oldTag = Zotero.Tags.getName(oldTagID);
let dataOut = { result: null };
lastWin.openDialog(
'chrome://zotero/content/longTagFixer.xul',
'',
'chrome,modal,centerscreen',
oldTag,
dataOut
);
// If dialog was cancelled, stop
if (!dataOut.result) {
return;
}
switch (dataOut.result.op) {
case 'split':
for (let libraryID of editableLibraries) {
let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
yield Zotero.DB.executeTransaction(function* () {
for (let itemID of itemIDs) {
let item = yield Zotero.Items.getAsync(itemID);
for (let tag of dataOut.result.tags) {
item.addTag(tag);
}
item.removeTag(oldTag);
yield item.save();
}
yield Zotero.Tags.purge(oldTagID);
});
}
break;
case 'edit':
for (let libraryID of editableLibraries) {
let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
yield Zotero.DB.executeTransaction(function* () {
for (let itemID of itemIDs) {
let item = yield Zotero.Items.getAsync(itemID);
item.replaceTag(oldTag, dataOut.result.tag);
yield item.save();
}
});
}
break;
case 'delete':
for (let libraryID of editableLibraries) {
yield Zotero.Tags.removeFromLibrary(libraryID, oldTagID);
}
break;
}
}
}
options.restartSync = true;
});
// If not a background sync, show fixer dialog immediately
if (!options.background) {
yield e.dialogButtonCallback();
}
}
}
// TEMP // TEMP
return; return;
@ -920,8 +994,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
if (!skipReload) { if (!skipReload) {
Zotero.reloadDataObjects(); Zotero.reloadDataObjects();
} }
Zotero.Sync.EventListener.resetIgnored(); });
}
/** /**
@ -1099,20 +1172,20 @@ Zotero.Sync.Runner_Module = function (options = {}) {
/*// If not an error and there's no explicit button text, don't show /*// If not an error and there's no explicit button text, don't show
// button to report errors // button to report errors
if (e.errorType != 'error' && e.data.dialogButtonText === undefined) { if (e.errorType != 'error' && e.dialogButtonText === undefined) {
e.data.dialogButtonText = null; e.dialogButtonText = null;
}*/ }*/
if (e.data && e.data.dialogButtonText !== null) { if (e.data && e.dialogButtonText !== null) {
if (e.data.dialogButtonText === undefined) { if (e.dialogButtonText === undefined) {
var buttonText = Zotero.getString('errorReport.reportError'); var buttonText = Zotero.getString('errorReport.reportError');
var buttonCallback = function () { var buttonCallback = function () {
doc.defaultView.ZoteroPane.reportErrors(); doc.defaultView.ZoteroPane.reportErrors();
}; };
} }
else { else {
var buttonText = e.data.dialogButtonText; var buttonText = e.dialogButtonText;
var buttonCallback = e.data.dialogButtonCallback; var buttonCallback = e.dialogButtonCallback;
} }
var button = doc.createElement('button'); var button = doc.createElement('button');

View File

@ -56,6 +56,7 @@ general.openPreferences = Open Preferences
general.keys.ctrlShift = Ctrl+Shift+ general.keys.ctrlShift = Ctrl+Shift+
general.keys.cmdShift = Cmd+Shift+ general.keys.cmdShift = Cmd+Shift+
general.dontShowAgain = Dont Show Again general.dontShowAgain = Dont Show Again
general.fix = Fix…
general.operationInProgress = A Zotero operation is currently in progress. general.operationInProgress = A Zotero operation is currently in progress.
general.operationInProgress.waitUntilFinished = Please wait until it has finished. general.operationInProgress.waitUntilFinished = Please wait until it has finished.

View File

@ -670,58 +670,9 @@ describe("Zotero.Sync.Runner", function () {
assert.isAbove(lastSyncTime, new Date().getTime() - 1000); assert.isAbove(lastSyncTime, new Date().getTime() - 1000);
assert.isBelow(lastSyncTime, new Date().getTime()); assert.isBelow(lastSyncTime, new Date().getTime());
}) })
it("should show the sync error icon on error", function* () {
let pubLib = Zotero.Libraries.get(publicationsLibraryID);
pubLib.libraryVersion = 5;
yield pubLib.save();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersionsEmpty');
// My Library
setResponse({
method: "GET",
url: "users/1/settings",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: {
INVALID: true // TODO: Find a cleaner error
}
});
// No publications changes
setResponse({
method: "GET",
url: "users/1/publications/settings?since=5",
status: 304,
headers: {
"Last-Modified-Version": 5
},
json: {}
});
setResponse({
method: "GET",
url: "users/1/publications/fulltext",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: {}
});
spy = sinon.spy(runner, "updateIcons");
yield runner.sync();
assert.isTrue(spy.calledTwice);
assert.isArray(spy.args[1][0]);
assert.lengthOf(spy.args[1][0], 1);
// Not an instance of Error for some reason
var error = spy.args[1][0][0];
assert.equal(Object.getPrototypeOf(error).constructor.name, "Error");
});
}) })
describe("#createAPIKeyFromCredentials()", function() { describe("#createAPIKeyFromCredentials()", function() {
var data = { var data = {
name: "Automatic Zotero Client Key", name: "Automatic Zotero Client Key",
@ -785,5 +736,211 @@ describe("Zotero.Sync.Runner", function () {
yield runner.deleteAPIKey(); yield runner.deleteAPIKey();
}); });
}) });
describe("Error Handling", function () {
var win;
afterEach(function () {
if (win) {
win.close();
}
});
it("should show the sync error icon on error", function* () {
let pubLib = Zotero.Libraries.get(publicationsLibraryID);
pubLib.libraryVersion = 5;
yield pubLib.save();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersionsEmpty');
// My Library
setResponse({
method: "GET",
url: "users/1/settings",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: {
INVALID: true // TODO: Find a cleaner error
}
});
// No publications changes
setResponse({
method: "GET",
url: "users/1/publications/settings?since=5",
status: 304,
headers: {
"Last-Modified-Version": 5
},
json: {}
});
setResponse({
method: "GET",
url: "users/1/publications/fulltext",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: {}
});
spy = sinon.spy(runner, "updateIcons");
yield runner.sync();
assert.isTrue(spy.calledTwice);
assert.isArray(spy.args[1][0]);
assert.lengthOf(spy.args[1][0], 1);
// Not an instance of Error for some reason
var error = spy.args[1][0][0];
assert.equal(Object.getPrototypeOf(error).constructor.name, "Error");
});
// TODO: Test multiple long tags and tags across libraries
describe("Long Tag Fixer", function () {
it("should split a tag", function* () {
win = yield loadZoteroPane();
var item = yield createDataObject('item');
var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic";
item.addTag(tag, 1);
yield item.saveTx();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
var json = JSON.parse(req.requestBody);
if (json[0].tags.length == 1) {
req.respond(
200,
{
"Last-Modified-Version": 5
},
JSON.stringify({
successful: {},
success: {},
unchanged: {},
failed: {
"0": {
code: 413,
message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync",
data: {
tag
}
}
}
})
);
}
else {
let itemJSON = item.toResponseJSON();
itemJSON.version = 6;
itemJSON.data.version = 6;
req.respond(
200,
{
"Last-Modified-Version": 6
},
JSON.stringify({
successful: {
"0": itemJSON
},
success: {
"0": json[0].key
},
unchanged: {},
failed: {}
})
);
}
}
});
waitForDialog(null, 'accept', 'chrome://zotero/content/longTagFixer.xul');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));
assert.isNumber(Zotero.Tags.getID('feeling'));
});
it("should delete a tag", function* () {
win = yield loadZoteroPane();
var item = yield createDataObject('item');
var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic";
item.addTag(tag, 1);
yield item.saveTx();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
var json = JSON.parse(req.requestBody);
if (json[0].tags.length == 1) {
req.respond(
200,
{
"Last-Modified-Version": 5
},
JSON.stringify({
successful: {},
success: {},
unchanged: {},
failed: {
"0": {
code: 413,
message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync",
data: {
tag
}
}
}
})
);
}
else {
let itemJSON = item.toResponseJSON();
itemJSON.version = 6;
itemJSON.data.version = 6;
req.respond(
200,
{
"Last-Modified-Version": 6
},
JSON.stringify({
successful: {
"0": itemJSON
},
success: {
"0": json[0].key
},
unchanged: {},
failed: {}
})
);
}
}
});
waitForDialog(function (dialog) {
dialog.Zotero_Long_Tag_Fixer.switchMode(2);
}, 'accept', 'chrome://zotero/content/longTagFixer.xul');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));
assert.isFalse(Zotero.Tags.getID('feeling'));
});
});
});
}) })