Prompt to reset library data/files on loss of write access
On reset, items are overwritten with pristine versions if available and deleted otherwise, and then the library is marked for a full sync. Unsynced/changed files are deleted and marked for download. Closes #1002 Todo: - Handle API key access change (#953, in part) - Handle 403 from data/file upload for existing users (#1041)
This commit is contained in:
parent
ac34f2c4f4
commit
5fee2bf4ca
|
@ -23,6 +23,8 @@
|
||||||
***** END LICENSE BLOCK *****
|
***** END LICENSE BLOCK *****
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
Zotero.Group = function (params = {}) {
|
Zotero.Group = function (params = {}) {
|
||||||
params.libraryType = 'group';
|
params.libraryType = 'group';
|
||||||
Zotero.Group._super.call(this, params);
|
Zotero.Group._super.call(this, params);
|
||||||
|
@ -240,23 +242,7 @@ Zotero.Group.prototype.fromJSON = function (json, userID) {
|
||||||
var editable = false;
|
var editable = false;
|
||||||
var filesEditable = false;
|
var filesEditable = false;
|
||||||
if (userID) {
|
if (userID) {
|
||||||
// If user is owner or admin, make library editable, and make files editable unless they're
|
({ editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(json, userID));
|
||||||
// disabled altogether
|
|
||||||
if (json.owner == userID || (json.admins && json.admins.indexOf(userID) != -1)) {
|
|
||||||
editable = true;
|
|
||||||
if (json.fileEditing != 'none') {
|
|
||||||
filesEditable = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If user is member, make library and files editable if they're editable by all members
|
|
||||||
else if (json.members && json.members.indexOf(userID) != -1) {
|
|
||||||
if (json.libraryEditing == 'members') {
|
|
||||||
editable = true;
|
|
||||||
if (json.fileEditing == 'members') {
|
|
||||||
filesEditable = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.editable = editable;
|
this.editable = editable;
|
||||||
this.filesEditable = filesEditable;
|
this.filesEditable = filesEditable;
|
||||||
|
|
|
@ -116,4 +116,31 @@ Zotero.Groups = new function () {
|
||||||
|
|
||||||
return this._cache.libraryIDByGroupID[groupID] || false;
|
return this._cache.libraryIDByGroupID[groupID] || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.getPermissionsFromJSON = function (json, userID) {
|
||||||
|
if (!json.owner) throw new Error("Invalid JSON provided for group data");
|
||||||
|
if (!userID) throw new Error("userID not provided");
|
||||||
|
|
||||||
|
var editable = false;
|
||||||
|
var filesEditable = false;
|
||||||
|
// If user is owner or admin, make library editable, and make files editable unless they're
|
||||||
|
// disabled altogether
|
||||||
|
if (json.owner == userID || (json.admins && json.admins.indexOf(userID) != -1)) {
|
||||||
|
editable = true;
|
||||||
|
if (json.fileEditing != 'none') {
|
||||||
|
filesEditable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If user is member, make library and files editable if they're editable by all members
|
||||||
|
else if (json.members && json.members.indexOf(userID) != -1) {
|
||||||
|
if (json.libraryEditing == 'members') {
|
||||||
|
editable = true;
|
||||||
|
if (json.fileEditing == 'members') {
|
||||||
|
filesEditable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { editable, filesEditable };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2399,7 +2399,7 @@ Zotero.Item.prototype.getFilename = function () {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous cached check for file existence, used for items view
|
* Asynchronous check for file existence
|
||||||
*/
|
*/
|
||||||
Zotero.Item.prototype.fileExists = Zotero.Promise.coroutine(function* () {
|
Zotero.Item.prototype.fileExists = Zotero.Promise.coroutine(function* () {
|
||||||
if (!this.isAttachment()) {
|
if (!this.isAttachment()) {
|
||||||
|
|
|
@ -155,6 +155,207 @@ Zotero.Sync.Data.Local = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<Boolean>} - True if library updated, false to cancel
|
||||||
|
*/
|
||||||
|
checkLibraryForAccess: Zotero.Promise.coroutine(function* (win, libraryID, editable, filesEditable) {
|
||||||
|
var library = Zotero.Libraries.get(libraryID);
|
||||||
|
|
||||||
|
// If library is going from editable to non-editable and there's unsynced local data, prompt
|
||||||
|
if (library.editable && !editable
|
||||||
|
&& ((yield this._libraryHasUnsyncedData(libraryID))
|
||||||
|
|| (yield this._libraryHasUnsyncedFiles(libraryID)))) {
|
||||||
|
let index = this._showWriteAccessLostPrompt(win, library);
|
||||||
|
|
||||||
|
// Reset library
|
||||||
|
if (index == 0) {
|
||||||
|
yield this._resetUnsyncedLibraryData(libraryID);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip library
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (library.filesEditable && !filesEditable && (yield this._libraryHasUnsyncedFiles(libraryID))) {
|
||||||
|
let index = this._showFileWriteAccessLostPrompt(win, library);
|
||||||
|
|
||||||
|
// Reset library files
|
||||||
|
if (index == 0) {
|
||||||
|
yield this._resetUnsyncedLibraryFiles(libraryID);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip library
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
_libraryHasUnsyncedData: Zotero.Promise.coroutine(function* (libraryID) {
|
||||||
|
let settings = yield Zotero.SyncedSettings.getUnsynced(libraryID);
|
||||||
|
if (Object.keys(settings).length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(libraryID)) {
|
||||||
|
let ids = yield Zotero.Sync.Data.Local.getUnsynced(objectType, libraryID);
|
||||||
|
if (ids.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys = yield Zotero.Sync.Data.Local.getDeleted(objectType, libraryID);
|
||||||
|
if (keys.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
_libraryHasUnsyncedFiles: Zotero.Promise.coroutine(function* (libraryID) {
|
||||||
|
yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID);
|
||||||
|
return !!(yield Zotero.Sync.Storage.Local.getFilesToUpload(libraryID));
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
_showWriteAccessLostPrompt: function (win, library) {
|
||||||
|
var libraryType = library.libraryType;
|
||||||
|
switch (libraryType) {
|
||||||
|
case 'group':
|
||||||
|
var msg = Zotero.getString('sync.error.groupWriteAccessLost',
|
||||||
|
[library.name, ZOTERO_CONFIG.DOMAIN_NAME])
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('sync.error.groupCopyChangedItems')
|
||||||
|
var button1Text = Zotero.getString('sync.resetGroupAndSync');
|
||||||
|
var button2Text = Zotero.getString('sync.skipGroup');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported library type " + libraryType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||||
|
.getService(Components.interfaces.nsIPromptService);
|
||||||
|
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ ps.BUTTON_DELAY_ENABLE;
|
||||||
|
|
||||||
|
return ps.confirmEx(
|
||||||
|
win,
|
||||||
|
Zotero.getString('general.permissionDenied'),
|
||||||
|
msg,
|
||||||
|
buttonFlags,
|
||||||
|
button1Text,
|
||||||
|
button2Text,
|
||||||
|
null,
|
||||||
|
null, {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
_showFileWriteAccessLostPrompt: function (win, library) {
|
||||||
|
var libraryType = library.libraryType;
|
||||||
|
switch (libraryType) {
|
||||||
|
case 'group':
|
||||||
|
var msg = Zotero.getString('sync.error.groupFileWriteAccessLost',
|
||||||
|
[library.name, ZOTERO_CONFIG.DOMAIN_NAME])
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('sync.error.groupCopyChangedFiles')
|
||||||
|
var button1Text = Zotero.getString('sync.resetGroupFilesAndSync');
|
||||||
|
var button2Text = Zotero.getString('sync.skipGroup');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported library type " + libraryType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||||
|
.getService(Components.interfaces.nsIPromptService);
|
||||||
|
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ ps.BUTTON_DELAY_ENABLE;
|
||||||
|
|
||||||
|
return ps.confirmEx(
|
||||||
|
win,
|
||||||
|
Zotero.getString('general.permissionDenied'),
|
||||||
|
msg,
|
||||||
|
buttonFlags,
|
||||||
|
button1Text,
|
||||||
|
button2Text,
|
||||||
|
null,
|
||||||
|
null, {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
_resetUnsyncedLibraryData: Zotero.Promise.coroutine(function* (libraryID) {
|
||||||
|
let settings = yield Zotero.SyncedSettings.getUnsynced(libraryID);
|
||||||
|
if (Object.keys(settings).length) {
|
||||||
|
yield Zotero.Promise.each(Object.keys(settings), function (key) {
|
||||||
|
return Zotero.SyncedSettings.clear(libraryID, key, { skipDeleteLog: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(libraryID)) {
|
||||||
|
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
||||||
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||||
|
|
||||||
|
// New/modified objects
|
||||||
|
let ids = yield Zotero.Sync.Data.Local.getUnsynced(objectType, libraryID);
|
||||||
|
let keys = ids.map(id => objectsClass.getLibraryAndKeyFromID(id).key);
|
||||||
|
let cacheVersions = yield this.getLatestCacheObjectVersions(objectType, libraryID, keys);
|
||||||
|
let toDelete = [];
|
||||||
|
for (let key of keys) {
|
||||||
|
let obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
||||||
|
|
||||||
|
// If object is in cache, overwrite with pristine data
|
||||||
|
if (cacheVersions[key]) {
|
||||||
|
let json = yield this.getCacheObject(objectType, libraryID, key, cacheVersions[key]);
|
||||||
|
yield Zotero.DB.executeTransaction(function* () {
|
||||||
|
yield this._saveObjectFromJSON(obj, json, {});
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
// Otherwise, erase
|
||||||
|
else {
|
||||||
|
toDelete.push(objectsClass.getIDFromLibraryAndKey(libraryID, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toDelete.length) {
|
||||||
|
yield objectsClass.erase(toDelete, { skipDeleteLog: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted objects
|
||||||
|
keys = yield Zotero.Sync.Data.Local.getDeleted(objectType, libraryID);
|
||||||
|
yield this.removeObjectsFromDeleteLog(objectType, libraryID, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark library for full sync
|
||||||
|
var library = Zotero.Libraries.get(libraryID);
|
||||||
|
library.libraryVersion = -1;
|
||||||
|
yield library.saveTx();
|
||||||
|
|
||||||
|
yield this._resetUnsyncedLibraryFiles(libraryID);
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete unsynced files from library
|
||||||
|
*
|
||||||
|
* _libraryHasUnsyncedFiles(), which checks for updated files, must be called first.
|
||||||
|
*/
|
||||||
|
_resetUnsyncedLibraryFiles: Zotero.Promise.coroutine(function* (libraryID) {
|
||||||
|
var itemIDs = yield Zotero.Sync.Storage.Local.getFilesToUpload(libraryID);
|
||||||
|
for (let itemID of itemIDs) {
|
||||||
|
let item = Zotero.Items.get(itemID);
|
||||||
|
yield item.deleteAttachmentFile();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
getSkippedLibraries: function () {
|
getSkippedLibraries: function () {
|
||||||
return this._getSkippedLibrariesByPrefix("L");
|
return this._getSkippedLibrariesByPrefix("L");
|
||||||
},
|
},
|
||||||
|
@ -1117,11 +1318,11 @@ Zotero.Sync.Data.Local = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
_saveObjectFromJSON: Zotero.Promise.coroutine(function* (obj, json, options) {
|
_saveObjectFromJSON: Zotero.Promise.coroutine(function* (obj, json, options) {
|
||||||
|
var results = {};
|
||||||
try {
|
try {
|
||||||
|
results.key = json.key;
|
||||||
json = this._checkCacheJSON(json);
|
json = this._checkCacheJSON(json);
|
||||||
var results = {
|
|
||||||
key: json.key
|
|
||||||
};
|
|
||||||
if (!options.skipData) {
|
if (!options.skipData) {
|
||||||
obj.fromJSON(json.data);
|
obj.fromJSON(json.data);
|
||||||
}
|
}
|
||||||
|
@ -1385,6 +1586,8 @@ Zotero.Sync.Data.Local = {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
removeObjectsFromDeleteLog: function (objectType, libraryID, keys) {
|
removeObjectsFromDeleteLog: function (objectType, libraryID, keys) {
|
||||||
|
if (!keys.length) Zotero.Promise.resolve();
|
||||||
|
|
||||||
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
||||||
var sql = "DELETE FROM syncDeleteLog WHERE libraryID=? AND syncObjectTypeID=? AND key IN (";
|
var sql = "DELETE FROM syncDeleteLog WHERE libraryID=? AND syncObjectTypeID=? AND key IN (";
|
||||||
return Zotero.DB.executeTransaction(function* () {
|
return Zotero.DB.executeTransaction(function* () {
|
||||||
|
|
|
@ -293,15 +293,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
*/
|
*/
|
||||||
this.checkLibraries = Zotero.Promise.coroutine(function* (client, options, keyInfo, libraries = []) {
|
this.checkLibraries = Zotero.Promise.coroutine(function* (client, options, keyInfo, libraries = []) {
|
||||||
var access = keyInfo.access;
|
var access = keyInfo.access;
|
||||||
|
|
||||||
/* var libraries = [
|
|
||||||
Zotero.Libraries.userLibraryID,
|
|
||||||
Zotero.Libraries.publicationsLibraryID,
|
|
||||||
// Groups sorted by name
|
|
||||||
...(Zotero.Groups.getAll().map(x => x.libraryID))
|
|
||||||
];
|
|
||||||
*/
|
|
||||||
|
|
||||||
var syncAllLibraries = !libraries || !libraries.length;
|
var syncAllLibraries = !libraries || !libraries.length;
|
||||||
|
|
||||||
// TODO: Ability to remove or disable editing of user library?
|
// TODO: Ability to remove or disable editing of user library?
|
||||||
|
@ -309,7 +300,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
if (syncAllLibraries) {
|
if (syncAllLibraries) {
|
||||||
if (access.user && access.user.library) {
|
if (access.user && access.user.library) {
|
||||||
libraries = [Zotero.Libraries.userLibraryID, Zotero.Libraries.publicationsLibraryID];
|
libraries = [Zotero.Libraries.userLibraryID, Zotero.Libraries.publicationsLibraryID];
|
||||||
// Remove skipped libraries
|
// If syncing all libraries, remove skipped libraries
|
||||||
libraries = Zotero.Utilities.arrayDiff(
|
libraries = Zotero.Utilities.arrayDiff(
|
||||||
libraries, Zotero.Sync.Data.Local.getSkippedLibraries()
|
libraries, Zotero.Sync.Data.Local.getSkippedLibraries()
|
||||||
);
|
);
|
||||||
|
@ -472,7 +463,22 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
throw new Error("Group " + groupID + " not found");
|
throw new Error("Group " + groupID + " not found");
|
||||||
}
|
}
|
||||||
let group = Zotero.Groups.get(groupID);
|
let group = Zotero.Groups.get(groupID);
|
||||||
if (!group) {
|
if (group) {
|
||||||
|
// Check if the user's permissions for the group have changed, and prompt to reset
|
||||||
|
// data if so
|
||||||
|
let { editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(
|
||||||
|
info.data, keyInfo.userID
|
||||||
|
);
|
||||||
|
let keepGoing = yield Zotero.Sync.Data.Local.checkLibraryForAccess(
|
||||||
|
null, group.libraryID, editable, filesEditable
|
||||||
|
);
|
||||||
|
// User chose to skip library
|
||||||
|
if (!keepGoing) {
|
||||||
|
Zotero.debug("Skipping sync of group " + group.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
group = new Zotero.Group;
|
group = new Zotero.Group;
|
||||||
group.id = groupID;
|
group.id = groupID;
|
||||||
}
|
}
|
||||||
|
|
|
@ -825,6 +825,8 @@ sync.syncWith = Sync with %S
|
||||||
sync.cancel = Cancel Sync
|
sync.cancel = Cancel Sync
|
||||||
sync.openSyncPreferences = Open Sync Preferences
|
sync.openSyncPreferences = Open Sync Preferences
|
||||||
sync.resetGroupAndSync = Reset Group and Sync
|
sync.resetGroupAndSync = Reset Group and Sync
|
||||||
|
sync.resetGroupFilesAndSync = Reset Group Files and Sync
|
||||||
|
sync.skipGroup = Skip Group
|
||||||
sync.removeGroupsAndSync = Remove Groups and Sync
|
sync.removeGroupsAndSync = Remove Groups and Sync
|
||||||
|
|
||||||
sync.error.usernameNotSet = Username not set
|
sync.error.usernameNotSet = Username not set
|
||||||
|
@ -840,9 +842,10 @@ sync.error.loginManagerCorrupted1 = Zotero cannot access your login information,
|
||||||
sync.error.loginManagerCorrupted2 = Close %1$S, remove cert8.db, key3.db, and logins.json from your %2$S profile directory, and re-enter your Zotero login information in the Sync pane of the Zotero preferences.
|
sync.error.loginManagerCorrupted2 = Close %1$S, remove cert8.db, key3.db, and logins.json from your %2$S profile directory, and re-enter your Zotero login information in the Sync pane of the Zotero preferences.
|
||||||
sync.error.syncInProgress = A sync operation is already in progress.
|
sync.error.syncInProgress = A sync operation is already in progress.
|
||||||
sync.error.syncInProgress.wait = Wait for the previous sync to complete or restart %S.
|
sync.error.syncInProgress.wait = Wait for the previous sync to complete or restart %S.
|
||||||
sync.error.writeAccessLost = You no longer have write access to the Zotero group '%S', and items you've added or edited cannot be synced to the server.
|
sync.error.groupWriteAccessLost = You no longer have write access to the group ‘%1$S’, and changes you’ve made locally cannot be uploaded. If you continue, your copy of the group will be reset to its state on %2$S, and local changes to items and files will be lost.
|
||||||
sync.error.groupWillBeReset = If you continue, your copy of the group will be reset to its state on the server, and local modifications to items and files will be lost.
|
sync.error.groupFileWriteAccessLost = You no longer have file editing access for the group ‘%1$S’, and files you’ve changed locally cannot be uploaded. If you continue, all group files will be reset to their state on %2$S.
|
||||||
sync.error.copyChangedItems = If you would like a chance to copy your changes elsewhere or to request write access from a group administrator, cancel the sync now.
|
sync.error.groupCopyChangedItems = If you would like a chance to copy your changes elsewhere or to request write access from a group administrator, you can skip syncing of the group now.
|
||||||
|
sync.error.groupCopyChangedFiles = If you would like a chance to copy modified files elsewhere or to request file editing access from a group administrator, you can skip syncing of the group now.
|
||||||
sync.error.manualInterventionRequired = Conflicts have suspended automatic syncing.
|
sync.error.manualInterventionRequired = Conflicts have suspended automatic syncing.
|
||||||
sync.error.clickSyncIcon = Click the sync icon to resolve them.
|
sync.error.clickSyncIcon = Click the sync icon to resolve them.
|
||||||
sync.error.invalidClock = The system clock is set to an invalid time. You will need to correct this to sync with the Zotero server.
|
sync.error.invalidClock = The system clock is set to an invalid time. You will need to correct this to sync with the Zotero server.
|
||||||
|
|
|
@ -96,6 +96,216 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#checkLibraryForAccess()", function () {
|
||||||
|
//
|
||||||
|
// editable
|
||||||
|
//
|
||||||
|
it("should prompt if library is changing from editable to non-editable and reset library on accept", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
var promise = waitForDialog(function (dialog) {
|
||||||
|
var text = dialog.document.documentElement.textContent;
|
||||||
|
assert.include(text, group.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
||||||
|
mock.expects("_resetUnsyncedLibraryData").once().returns(Zotero.Promise.resolve());
|
||||||
|
mock.expects("_resetUnsyncedLibraryFiles").never();
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
||||||
|
);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prompt if library is changing from editable to non-editable but not reset library on cancel", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
var promise = waitForDialog(function (dialog) {
|
||||||
|
var text = dialog.document.documentElement.textContent;
|
||||||
|
assert.include(text, group.name);
|
||||||
|
}, "cancel");
|
||||||
|
|
||||||
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
||||||
|
mock.expects("_resetUnsyncedLibraryData").never();
|
||||||
|
mock.expects("_resetUnsyncedLibraryFiles").never();
|
||||||
|
|
||||||
|
assert.isFalse(
|
||||||
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
||||||
|
);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not prompt if library is changing from editable to non-editable", function* () {
|
||||||
|
var group = yield createGroup({ editable: false, filesEditable: false });
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// filesEditable
|
||||||
|
//
|
||||||
|
it("should prompt if library is changing from filesEditable to non-filesEditable and reset library files on accept", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
var promise = waitForDialog(function (dialog) {
|
||||||
|
var text = dialog.document.documentElement.textContent;
|
||||||
|
assert.include(text, group.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
||||||
|
mock.expects("_resetUnsyncedLibraryData").never();
|
||||||
|
mock.expects("_resetUnsyncedLibraryFiles").once().returns(Zotero.Promise.resolve());
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
||||||
|
);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prompt if library is changing from filesEditable to non-filesEditable but not reset library files on cancel", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
var promise = waitForDialog(function (dialog) {
|
||||||
|
var text = dialog.document.documentElement.textContent;
|
||||||
|
assert.include(text, group.name);
|
||||||
|
}, "cancel");
|
||||||
|
|
||||||
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
||||||
|
mock.expects("_resetUnsyncedLibraryData").never();
|
||||||
|
mock.expects("_resetUnsyncedLibraryFiles").never();
|
||||||
|
|
||||||
|
assert.isFalse(
|
||||||
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
||||||
|
);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#_libraryHasUnsyncedData()", function () {
|
||||||
|
it("should return true for unsynced setting", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
||||||
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for unsynced item", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
yield createDataObject('item', { libraryID });
|
||||||
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if no changes", function* () {
|
||||||
|
var group = yield createGroup();
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
assert.isFalse(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#_resetUnsyncedLibraryData()", function () {
|
||||||
|
it("should revert group and mark for full sync", function* () {
|
||||||
|
var group = yield createGroup({
|
||||||
|
version: 1,
|
||||||
|
libraryVersion: 2
|
||||||
|
});
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
|
||||||
|
// New setting
|
||||||
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
||||||
|
|
||||||
|
// Changed collection
|
||||||
|
var changedCollection = yield createDataObject('collection', { libraryID, version: 1 });
|
||||||
|
var originalCollectionName = changedCollection.name;
|
||||||
|
yield Zotero.Sync.Data.Local.saveCacheObject(
|
||||||
|
'collection', libraryID, changedCollection.toJSON()
|
||||||
|
);
|
||||||
|
yield modifyDataObject(changedCollection);
|
||||||
|
|
||||||
|
// Unchanged item
|
||||||
|
var unchangedItem = yield createDataObject('item', { libraryID, version: 1, synced: true });
|
||||||
|
yield Zotero.Sync.Data.Local.saveCacheObject(
|
||||||
|
'item', libraryID, unchangedItem.toJSON()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Changed item
|
||||||
|
var changedItem = yield createDataObject('item', { libraryID, version: 1 });
|
||||||
|
var originalChangedItemTitle = changedItem.getField('title');
|
||||||
|
yield Zotero.Sync.Data.Local.saveCacheObject('item', libraryID, changedItem.toJSON());
|
||||||
|
yield modifyDataObject(changedItem);
|
||||||
|
|
||||||
|
// New item
|
||||||
|
var newItem = yield createDataObject('item', { libraryID, version: 1 });
|
||||||
|
var newItemKey = newItem.key;
|
||||||
|
|
||||||
|
// Delete item
|
||||||
|
var deletedItem = yield createDataObject('item', { libraryID });
|
||||||
|
var deletedItemKey = deletedItem.key;
|
||||||
|
yield deletedItem.eraseTx();
|
||||||
|
|
||||||
|
yield Zotero.Sync.Data.Local._resetUnsyncedLibraryData(libraryID);
|
||||||
|
|
||||||
|
assert.isNull(Zotero.SyncedSettings.get(group.libraryID, "testSetting"));
|
||||||
|
|
||||||
|
assert.equal(changedCollection.name, originalCollectionName);
|
||||||
|
assert.isTrue(changedCollection.synced);
|
||||||
|
|
||||||
|
assert.isTrue(unchangedItem.synced);
|
||||||
|
|
||||||
|
assert.equal(changedItem.getField('title'), originalChangedItemTitle);
|
||||||
|
assert.isTrue(changedItem.synced);
|
||||||
|
|
||||||
|
assert.isFalse(Zotero.Items.get(newItemKey));
|
||||||
|
|
||||||
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, deletedItemKey));
|
||||||
|
|
||||||
|
assert.equal(group.libraryVersion, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#_resetUnsyncedLibraryFiles", function () {
|
||||||
|
it("should delete unsynced files", function* () {
|
||||||
|
var group = yield createGroup({
|
||||||
|
version: 1,
|
||||||
|
libraryVersion: 2
|
||||||
|
});
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
|
||||||
|
var attachment1 = yield importFileAttachment('test.png', { libraryID });
|
||||||
|
attachment1.attachmentSyncState = "in_sync";
|
||||||
|
attachment1.attachmentSyncedModificationTime = 1234567890000;
|
||||||
|
attachment1.attachmentSyncedHash = "8caf2ee22919d6725eb0648b98ef6bad";
|
||||||
|
var attachment2 = yield importFileAttachment('test.pdf', { libraryID });
|
||||||
|
|
||||||
|
// Has to be called before _resetUnsyncedLibraryFiles()
|
||||||
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedFiles(libraryID));
|
||||||
|
|
||||||
|
yield Zotero.Sync.Data.Local._resetUnsyncedLibraryFiles(libraryID);
|
||||||
|
|
||||||
|
assert.isFalse(yield attachment1.fileExists());
|
||||||
|
assert.isFalse(yield attachment2.fileExists());
|
||||||
|
assert.equal(
|
||||||
|
attachment1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
attachment2.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("#getLatestCacheObjectVersions", function () {
|
describe("#getLatestCacheObjectVersions", function () {
|
||||||
before(function* () {
|
before(function* () {
|
||||||
yield resetDB({
|
yield resetDB({
|
||||||
|
|
|
@ -309,9 +309,16 @@ describe("Zotero.Sync.Runner", function () {
|
||||||
setResponse('userGroups.groupVersions');
|
setResponse('userGroups.groupVersions');
|
||||||
setResponse('groups.ownerGroup');
|
setResponse('groups.ownerGroup');
|
||||||
setResponse('groups.memberGroup');
|
setResponse('groups.memberGroup');
|
||||||
|
// Simulate acceptance of library reset for group 2 editable change
|
||||||
|
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
|
||||||
|
.returns(Zotero.Promise.resolve(true));
|
||||||
|
|
||||||
var libraries = yield runner.checkLibraries(
|
var libraries = yield runner.checkLibraries(
|
||||||
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
|
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(stub.calledTwice);
|
||||||
|
stub.restore();
|
||||||
assert.lengthOf(libraries, 4);
|
assert.lengthOf(libraries, 4);
|
||||||
assert.sameMembers(
|
assert.sameMembers(
|
||||||
libraries,
|
libraries,
|
||||||
|
@ -350,12 +357,19 @@ describe("Zotero.Sync.Runner", function () {
|
||||||
setResponse('userGroups.groupVersions');
|
setResponse('userGroups.groupVersions');
|
||||||
setResponse('groups.ownerGroup');
|
setResponse('groups.ownerGroup');
|
||||||
setResponse('groups.memberGroup');
|
setResponse('groups.memberGroup');
|
||||||
|
// Simulate acceptance of library reset for group 2 editable change
|
||||||
|
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
|
||||||
|
.returns(Zotero.Promise.resolve(true));
|
||||||
|
|
||||||
var libraries = yield runner.checkLibraries(
|
var libraries = yield runner.checkLibraries(
|
||||||
runner.getAPIClient({ apiKey }),
|
runner.getAPIClient({ apiKey }),
|
||||||
false,
|
false,
|
||||||
responses.keyInfo.fullAccess.json,
|
responses.keyInfo.fullAccess.json,
|
||||||
[group1.libraryID, group2.libraryID]
|
[group1.libraryID, group2.libraryID]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(stub.calledTwice);
|
||||||
|
stub.restore();
|
||||||
assert.lengthOf(libraries, 2);
|
assert.lengthOf(libraries, 2);
|
||||||
assert.sameMembers(libraries, [group1.libraryID, group2.libraryID]);
|
assert.sameMembers(libraries, [group1.libraryID, group2.libraryID]);
|
||||||
|
|
||||||
|
@ -443,6 +457,74 @@ describe("Zotero.Sync.Runner", function () {
|
||||||
assert.lengthOf(libraries, 0);
|
assert.lengthOf(libraries, 0);
|
||||||
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
|
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should prompt to revert local changes on loss of library write access", function* () {
|
||||||
|
var group = yield createGroup({
|
||||||
|
version: 1,
|
||||||
|
libraryVersion: 2
|
||||||
|
});
|
||||||
|
var libraryID = group.libraryID;
|
||||||
|
|
||||||
|
setResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: "users/1/groups?format=versions",
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Last-Modified-Version": 3
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
[group.id]: 3
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: "groups/" + group.id,
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Last-Modified-Version": 3
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
id: group.id,
|
||||||
|
version: 2,
|
||||||
|
data: {
|
||||||
|
// Make group read-only
|
||||||
|
id: group.id,
|
||||||
|
version: 2,
|
||||||
|
name: group.name,
|
||||||
|
description: group.description,
|
||||||
|
owner: 2,
|
||||||
|
type: "Private",
|
||||||
|
libraryEditing: "admins",
|
||||||
|
libraryReading: "all",
|
||||||
|
fileEditing: "admins",
|
||||||
|
admins: [],
|
||||||
|
members: [1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// First, test cancelling
|
||||||
|
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
|
||||||
|
.returns(Zotero.Promise.resolve(false));
|
||||||
|
var libraries = yield runner.checkLibraries(
|
||||||
|
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
|
||||||
|
);
|
||||||
|
assert.notInclude(libraries, group.libraryID);
|
||||||
|
assert.isTrue(stub.calledOnce);
|
||||||
|
assert.isTrue(group.editable);
|
||||||
|
stub.reset();
|
||||||
|
|
||||||
|
// Next, reset
|
||||||
|
stub.returns(Zotero.Promise.resolve(true));
|
||||||
|
libraries = yield runner.checkLibraries(
|
||||||
|
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
|
||||||
|
);
|
||||||
|
assert.include(libraries, group.libraryID);
|
||||||
|
assert.isTrue(stub.calledOnce);
|
||||||
|
assert.isFalse(group.editable);
|
||||||
|
|
||||||
|
stub.reset();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#sync()", function () {
|
describe("#sync()", function () {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user