Mendeley Import: Auth using direct login

* Importer will now ask user for a login and password via form and will
  perform sign-in directly using credentials rather than oauth
* Signing in this way enables importer to obtain desktop document ID
  which is now stored for each item
* It's possible to switch back to the old method (ouath) by setting
  `import.mendeleyUseOAuth` pref to `true`.
* New option to only import new items. This options only appears if
  database contains previously imported items.
* Importer will now update mendeleyDB:documentUUID on existing items to
  match value used in Mendeley Desktop if available
* Importer will no longer create collections when no new items are
  imported * Importer will only report number of new items imported on
  re-import * Importer will now preserve dateAdded on re-import

Co-authored-by: Dan Stillman <dstillman@zotero.org>
This commit is contained in:
Tom Najdek 2022-10-16 20:10:58 +02:00
parent 05e9523cba
commit 4b523555d6
No known key found for this signature in database
GPG key ID: EEC61A7B4C667D77
14 changed files with 510 additions and 63 deletions

View file

@ -444,10 +444,12 @@ var Zotero_File_Interface = new function() {
var translation;
if (options.mendeleyCode) {
if (options.mendeleyAuth || options.mendeleyCode) {
translation = yield _getMendeleyTranslation();
translation.createNewCollection = createNewCollection;
translation.mendeleyAuth = options.mendeleyAuth;
translation.mendeleyCode = options.mendeleyCode;
translation.newItemsOnly = options.newItemsOnly;
}
else if (options.folder) {
Components.utils.import("chrome://zotero/content/import/folderImport.js");

View file

@ -1,7 +1,7 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2022 Corporation for Digital Scholarship
Copyright © 2023 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
@ -22,23 +22,28 @@
***** END LICENSE BLOCK *****
*/
/* eslint camelcase: ["error", {allow: ["Zotero_File_Interface", "Zotero_Import_Wizard"]} ] */
/* global Zotero_File_Interface: false, mendeleyAPIUtils: false */
import FilePicker from 'zotero/filePicker';
import React from 'react';
import ReactDOM from 'react-dom';
import ProgressQueueTable from 'components/progressQueueTable';
/* eslint camelcase: ["error", {allow: ["Zotero_File_Interface", "Zotero_Import_Wizard"]} ] */
/* global Zotero_File_Interface: false */
Components.utils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleyAPIUtils.js");
const { directAuth } = mendeleyAPIUtils;
const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
wizard: null,
folder: null,
file: null,
mendeleyCode: null,
folder: null,
libraryID: null,
mendeleyAuth: null,
mendeleyCode: null,
mendeleyHasPreviouslyImported: false,
translation: null,
wizard: null,
async getShouldCreateCollection() {
const sql = "SELECT ROWID FROM collections WHERE libraryID=?1 "
@ -57,15 +62,22 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
const { mendeleyCode, libraryID } = window.arguments[0].wrappedJSObject ?? {};
this.libraryID = libraryID;
this.mendeleyCode = mendeleyCode;
const predicateID = Zotero.RelationPredicates.getID('mendeleyDB:documentUUID');
if (predicateID) {
const relSQL = 'SELECT ROWID FROM itemRelations WHERE predicateID = ? LIMIT 1';
this.mendeleyHasPreviouslyImported = !!(await Zotero.DB.valueQueryAsync(relSQL, predicateID));
}
this.wizard = document.getElementById('import-wizard');
this.wizard.getPageById('page-start')
.addEventListener('pageadvanced', this.onImportSourceAdvance.bind(this));
this.wizard.getPageById('page-mendeley-online-intro')
.addEventListener('pageshow', this.onMendeleyOnlineShow.bind(this));
this.wizard.getPageById('page-mendeley-online-intro')
.addEventListener('pagerewound', this.goToStart.bind(this));
this.wizard.getPageById('page-mendeley-online-intro')
.addEventListener('pageadvanced', this.openMendeleyAuthWindow.bind(this));
.addEventListener('pageadvanced', this.onMendeleyOnlineAdvance.bind(this));
this.wizard.getPageById('page-options')
.addEventListener('pageshow', this.onOptionsPageShow.bind(this));
this.wizard.getPageById('page-options')
@ -90,6 +102,10 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
document
.querySelector('#page-done-error > button')
.addEventListener('keydown', this.onReportErrorInteract.bind(this));
document
.getElementById('mendeley-username').addEventListener('keyup', this.onMendeleyAuthKeyUp.bind(this));
document
.getElementById('mendeley-password').addEventListener('keyup', this.onMendeleyAuthKeyUp.bind(this));
this.wizard.addEventListener('pageshow', this.updateFocus.bind(this));
this.wizard.addEventListener('wizardcancel', this.onCancel.bind(this));
@ -101,7 +117,8 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
this.wizard.shadowRoot
.querySelector('.wizard-header-label').style.fontSize = '16px';
if (mendeleyCode) {
if (mendeleyCode && Zotero.Prefs.get("import.mendeleyUseOAuth")) {
this.mendeleyCode = mendeleyCode;
this.wizard.goTo('page-options');
}
},
@ -194,6 +211,50 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
this.wizard.goTo('page-options');
},
async onMendeleyOnlineShow() {
document.getElementById('import-online-intro').l10nId = Zotero.Prefs.get("import.mendeleyUseOAuth")
? 'import-online-intro'
: 'import-online-form-intro';
document.getElementById('mendeley-login').style.display = Zotero.Prefs.get("import.mendeleyUseOAuth") ? 'none' : '';
document.getElementById('mendeley-online-login-feedback').style.display = 'none';
// If we use oAuth, form doesn't show and we can advance, otherwise need to fill-in form first so disable
this.wizard.canAdvance = Zotero.Prefs.get("import.mendeleyUseOAuth");
},
async onMendeleyOnlineAdvance(ev) {
ev.preventDefault();
if (Zotero.Prefs.get("import.mendeleyUseOAuth")) {
this.openMendeleyAuthWindow();
}
else {
const userNameEl = document.getElementById('mendeley-username');
const passwordEl = document.getElementById('mendeley-password');
userNameEl.disabled = true;
passwordEl.disabled = true;
try {
this.mendeleyAuth = await directAuth(userNameEl.value, passwordEl.value);
this.wizard.goTo('page-options');
}
catch (e) {
const feedbackEl = document.getElementById('mendeley-online-login-feedback');
feedbackEl.style.display = '';
this.wizard.canAdvance = false; // change to either of the inputs will reset thi
}
finally {
userNameEl.disabled = false;
passwordEl.disabled = false;
}
}
},
onMendeleyAuthKeyUp() {
document.getElementById('mendeley-online-login-feedback').style.display = 'none';
this.wizard.canAdvance = document.getElementById('mendeley-username').value.length > 0
&& document.getElementById('mendeley-password').value.length > 0;
},
async onImportSourceAdvance(ev) {
const selectedMode = document.getElementById('import-source-group').selectedItem.value;
ev.preventDefault();
@ -226,13 +287,16 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
onOptionsPageShow() {
document.getElementById('page-options-folder-import').style.display = this.folder ? 'block' : 'none';
document.getElementById('page-options-file-handling').style.display = this.mendeleyCode ? 'none' : 'block';
document.getElementById('page-options-file-handling').style.display = (this.mendeleyCode || this.mendeleyAuth) ? 'none' : 'block';
const hideExtraMendeleyOptions = !this.mendeleyHasPreviouslyImported || !(this.mendeleyAuth || this.mendeleyCode);
document.getElementById('page-options-mendeley').style.display = hideExtraMendeleyOptions ? 'none' : 'block';
if (hideExtraMendeleyOptions) {
document.getElementById('new-items-only-checkbox').checked = false;
}
this.wizard.canRewind = false;
},
openMendeleyAuthWindow(ev) {
ev.preventDefault();
openMendeleyAuthWindow() {
const arg = Components.classes["@mozilla.org/supports-string;1"]
.createInstance(Components.interfaces.nsISupportsString);
arg.data = 'mendeleyImport';
@ -317,6 +381,7 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
const fileTypes = document.getElementById('import-other').checked
? document.getElementById('other-files').value
: null;
const newItemsOnly = document.getElementById('new-items-only-checkbox').checked;
try {
const result = await Zotero_File_Interface.importFile({
@ -325,8 +390,10 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
fileTypes,
folder: this.folder,
linkFiles,
mendeleyAuth: this.mendeleyAuth,
mendeleyCode: this.mendeleyCode,
mimeTypes,
newItemsOnly,
onBeforeImport: this.onBeforeImport.bind(this),
recreateStructure
});
@ -345,12 +412,12 @@ const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars
}
catch (e) {
if (e.message == 'Encrypted Mendeley database') {
this.skipToDonePage('general.error', [], false, true);
this.skipToDonePage('general-error', [], false, true);
}
else {
const translatorLabel = this.translation?.translator?.[0]?.label;
this.skipToDonePage(
'general.error',
'general-error',
translatorLabel
? ['file-interface-import-error-translator', { translator: translatorLabel }]
: 'file-interface-import-error',

View file

@ -1,7 +1,7 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
@ -30,6 +30,7 @@
data-header-label-id="import-online-intro-title"
>
<div
id="import-online-intro"
class="mendeley-online-intro"
data-l10n-id="import-online-intro"
data-l10n-args='{"targetAppOnline": "Mendeley Reference Manager", "targetApp": "Mendeley"}'></div>
@ -37,6 +38,17 @@
class="mendeley-online-intro"
data-l10n-id="import-online-intro2"
data-l10n-args='{"targetApp": "Mendeley"}'></div>
<fieldset id="mendeley-login">
<div class="field">
<html:label for="mendeley-username" data-l10n-id="import-mendeley-username" />
<html:input type="text" id="mendeley-username" />
</div>
<div class="field">
<html:label for="mendeley-password" data-l10n-id="import-mendeley-password" />
<html:input type="password" id="mendeley-password" />
</div>
</fieldset>
<div id="mendeley-online-login-feedback" data-l10n-id="import-online-wrong-credentials" />
</wizardpage>
<wizardpage
pageid="page-options"
@ -66,6 +78,9 @@
</radiogroup>
<div class="page-options-file-handling-description" data-l10n-id="import-fileHandling-description"></div>
</div>
<div class="options-group" id="page-options-mendeley">
<checkbox native="true" id="new-items-only-checkbox" data-l10n-id="import-online-new" checked="true" />
</div>
</wizardpage>
<wizardpage pageid="page-progress" data-header-label-id="import-importing">
<html:progress id="import-progress" max="100" value="0" />

View file

@ -1,22 +1,67 @@
// eslint-disable-next-line no-unused-vars
var mendeleyAPIUtils = (function () {
const OAUTH_URL = 'https://www.zotero.org/utils/mendeley/oauth';
const ZOTERO_OAUTH_URL = 'https://www.zotero.org/utils/mendeley/oauth';
const OAUTH_URL = 'https://api.mendeley.com/oauth/token';
const MENDELEY_API_URL = 'https://api.mendeley.com';
const CLIENT_ID = '6';
const CLIENT_NOT_VERY_SECRET = 'JtSAMzFdwC6RAED3RMZU';
const USER_AGENT = 'Mendeley Desktop/1.18';
const getTokens = async (codeOrRefreshToken, isRefresh = false) => {
const options = {
body: isRefresh
? `grant_type=refresh_token&refresh_token=${codeOrRefreshToken}`
: `grant_type=authorization_code&code=${codeOrRefreshToken}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
const response = await Zotero.HTTP.request('POST', OAUTH_URL, options);
const getTokens = async (url, bodyProps, headers = {}, options = {}) => {
const body = Object.entries(bodyProps)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
headers = { ...headers, 'Content-Type': 'application/x-www-form-urlencoded' };
if (!Zotero.Prefs.get('import.mendeleyUseOAuth')) {
headers['User-Agent'] = USER_AGENT;
}
options = { ...options, body, headers, timeout: 30000 };
const response = await Zotero.HTTP.request('POST', url, options);
return JSON.parse(response.responseText);
};
const directAuth = async (username, password, headers = {}, options = {}) => {
const bodyProps = {
client_id: CLIENT_ID, // eslint-disable-line camelcase
client_secret: CLIENT_NOT_VERY_SECRET, // eslint-disable-line camelcase
grant_type: 'password', // eslint-disable-line camelcase
password,
scope: 'all',
username
};
return getTokens(OAUTH_URL, bodyProps, headers, options);
};
const codeAuth = async (code, headers = {}, options = {}) => {
const bodyProps = {
grant_type: 'authorization_code', // eslint-disable-line camelcase
code,
};
return getTokens(ZOTERO_OAUTH_URL, bodyProps, headers, options);
};
const refreshAuth = async (refreshToken, headers = {}, options = {}) => {
const bodyProps = {
grant_type: 'refresh_token', // eslint-disable-line camelcase
refresh_token: refreshToken, // eslint-disable-line camelcase
};
if (!Zotero.Prefs.get('import.mendeleyUseOAuth')) {
bodyProps.client_id = CLIENT_ID; // eslint-disable-line camelcase
bodyProps.client_secret = CLIENT_NOT_VERY_SECRET; // eslint-disable-line camelcase
}
return getTokens(
Zotero.Prefs.get('import.mendeleyUseOAuth') ? ZOTERO_OAUTH_URL : OAUTH_URL,
bodyProps, headers, options
);
};
const getNextLinkFromResponse = (response) => {
let next = null;
let links = response.getResponseHeader('link');
@ -42,7 +87,7 @@ const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => {
}
catch (e) {
if (e.status === 401 || e.status === 403) {
const newTokens = await getTokens(tokens.refresh_token, true);
const newTokens = await refreshAuth(tokens.refresh_token);
// update tokens in the tokens object and in the header for next request
tokens.access_token = newTokens.access_token; // eslint-disable-line camelcase
tokens.refresh_token = newTokens.refresh_token; // eslint-disable-line camelcase
@ -81,6 +126,5 @@ const getAll = async (tokens, endPoint, params = {}, headers = {}, options = {},
return data;
};
return { getNextLinkFromResponse, getTokens, apiFetch, apiFetchUrl, get, getAll };
return { codeAuth, directAuth, getNextLinkFromResponse, apiFetch, apiFetchUrl, get, getAll };
})();

View file

@ -10,7 +10,7 @@ Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/men
Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleySchemaMap.js");
const { apiTypeToDBType, apiFieldToDBField } = mendeleyOnlineMappings;
const { apiFetch, get, getAll, getTokens } = mendeleyAPIUtils;
const { apiFetch, codeAuth, directAuth, get, getAll } = mendeleyAPIUtils;
const colorMap = new Map();
colorMap.set('rgb(255, 245, 173)', '#ffd400');
@ -28,7 +28,9 @@ var Zotero_Import_Mendeley = function () {
this.createNewCollection = null;
this.linkFiles = null;
this.newItems = [];
this.mendeleyCode = null;
this.newCollections = [];
this.mendeleyAuth = null;
this.newItemsOnly = false;
this._tokens = null;
this._db = null;
@ -115,8 +117,11 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
throw new Error("Not a valid Mendeley database");
}
if (this.mendeleyCode) {
this._tokens = await getTokens(this.mendeleyCode);
if (this.mendeleyAuth) {
this._tokens = this.mendeleyAuth;
}
else if (this.mendeleyCode) {
this._tokens = await codeAuth(this.mendeleyCode);
}
if (!this._file && !this._tokens) {
@ -303,9 +308,13 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
annotations.get(document.id)
);
}
this.newItems.push(Zotero.Items.get(documentIDMap.get(document.id)));
this._interruptChecker(true);
}
if (this.newItemsOnly && rootCollectionKey && this.newItems.length === 0) {
Zotero.debug(`Mendeley Import detected no new items, removing import collection containing ${this.newCollections.length} collections created during the import`);
const rootCollection = await Zotero.Collections.getAsync(options.collections[0]);
await rootCollection.eraseTx(this._saveOptions);
}
Zotero.debug(`Completed Mendeley import in ${Math.round((Date.now() - this._started) / 1000)}s. (Started: ${this._started})`);
}
catch (e) {
@ -480,6 +489,7 @@ Zotero_Import_Mendeley.prototype._saveCollections = async function (libraryID, j
collection.key = collectionJSON.key;
await collection.loadPrimaryData();
}
this.newCollections.push(collection);
}
// Remove external ids before saving
@ -541,11 +551,19 @@ Zotero_Import_Mendeley.prototype._getDocumentsAPI = async function (groupID) {
}
return (await getAll(this._tokens, 'documents', params, headers, {}, this._interruptChecker)).map(d => ({
...d,
uuid: d.id,
remoteUuid: d.id
}));
return (await getAll(this._tokens, 'documents', params, headers, {}, this._interruptChecker)).map(d => {
const processedDocument = { ...d, remoteUuid: d.id };
try {
const clientData = JSON.parse(d.client_data);
processedDocument.uuid = clientData.desktop_id ? clientData.desktop_id : d.id;
}
catch (_) {
processedDocument.uuid = d.id;
}
return processedDocument;
});
};
/**
@ -1279,10 +1297,12 @@ Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) {
var lastExistingParentItem;
for (let i = 0; i < json.length; i++) {
let itemJSON = json[i];
let isMappedToExisting = false;
// Check if the item has been previously imported
let item = await this._findExistingItem(libraryID, itemJSON, lastExistingParentItem);
if (item) {
isMappedToExisting = true;
if (item.isRegularItem()) {
lastExistingParentItem = item;
@ -1310,8 +1330,25 @@ Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) {
// Remove external id before save
let toSave = Object.assign({}, itemJSON);
delete toSave.documentID;
item.fromJSON(toSave);
if ((this.newItemsOnly && !isMappedToExisting) || !this.newItemsOnly) {
if (isMappedToExisting) {
// dateAdded shouldn't change on an updated item. See #2881
delete toSave.dateAdded;
}
item.fromJSON(toSave);
this.newItems.push(item);
}
else if (isMappedToExisting && toSave.relations) {
const predicate = 'mendeleyDB:documentUUID';
const existingRels = item.getRelationsByPredicate(predicate);
const newRel = toSave.relations[predicate];
if (existingRels.length && newRel && existingRels[0] !== newRel) {
Zotero.debug(`Migrating relation ${predicate} for existing item ${item.key} from ${existingRels[0]} to ${newRel}`);
item.removeRelation(predicate, existingRels[0]);
item.addRelation(predicate, newRel);
}
}
await item.saveTx({
skipDateModifiedUpdate: true,
...this._saveOptions

View file

@ -28,6 +28,7 @@ var mendeleyOnlineMappings = {
arxiv: 'arxivId',
accessed: 'dateAccessed',
authors: false, // all author types handled separately
client_data: false, // desktop_id extraction handled separately
citation_key: 'citationKey',
created: 'added',
edition: 'edition',

View file

@ -188,6 +188,7 @@
<!ENTITY zotero.import.size "Size">
<!ENTITY zotero.import.createCollection "Place imported collections and items into new collection">
<!ENTITY zotero.import.fileHandling "File Handling">
<!ENTITY zotero.import.online.newItemsOnly "Download new items only; dont update previously imported items">
<!ENTITY zotero.exportOptions.title "Export…">
<!ENTITY zotero.exportOptions.format.label "Format:">

View file

@ -38,6 +38,10 @@ import-file-handling-store =
import-file-handling-link =
.label = Link to files in original location
import-fileHandling-description = Linked files cannot be synced by { -app-name }.
import-online-new =
.label = Download new items only; dont update previously imported items
import-mendeley-username = Username
import-mendeley-password = Password
general-error = Error
file-interface-import-error = An error occurred while trying to import the selected file. Please ensure that the file is valid and try again.
@ -52,11 +56,10 @@ import-mendeley-encrypted = The selected Mendeley database cannot be read, likel
file-interface-import-error-translator = An error occurred importing the selected file with “{ $translator }”. Please ensure that the file is valid and try again.
# Variables:
# $targetAppOnline (String)
# $targetApp (String)
import-online-intro=In the next step you will be asked to log in to { $targetAppOnline } and grant { -app-name } access. This is necessary to import your { $targetApp } library into { -app-name }.
import-online-intro2={ -app-name } will never see or store your { $targetApp } password.
import-online-form-intro = Please enter your credentials to log in to { $targetAppOnline }. This is necessary to import your { $targetApp } library into { -app-name }.
import-online-wrong-credentials = Login to { $targetApp } failed. Please re-enter credentials and try again.
report-error =
.label = Report Error…

View file

@ -93,6 +93,8 @@ general.loading = Loading…
general.richText = Rich Text
general.clearSelection = Clear Selection
general.insert = Insert
general.username = Username
general.password = Password
general.yellow = Yellow
general.red = Red

View file

@ -106,6 +106,46 @@
}
}
#mendeley-login {
padding: 1.5em 1em 0;
font-size: 13px;
display: flex;
flex-direction: column;
align-items: center;
border: none;
.field {
display: flex;
max-width: 300px;
line-height: 1.5em;
align-items: center;
}
.field+.field {
margin-top: .5em;
}
label {
flex: 0 0 90px;
width: 90px;
margin-right: 8px;
text-align: right;
}
input {
flex: 1 1 auto;
font-size: 13px;
}
}
#mendeley-online-login-feedback {
text-align: center;
font-size: 13px;
margin-top: 1.3em;
color: red;
font-weight: bold;
}
#import-progress {
margin-top: 1em;
}

View file

@ -0,0 +1,88 @@
[
{
"authored": false,
"confirmed": true,
"created": "2021-11-02T09:54:28.353Z",
"file_attached": false,
"hidden": false,
"id": "7fea3cb3-f97d-3f16-8fad-f59caaa71688",
"last_modified": "2021-11-02T12:26:30.025Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"read": false,
"source": "lorem ipsum",
"identifiers":
{
"doi": "10.1111",
"pmid": "PMID: 11111111",
"arxiv": "1111.2222"
},
"starred": false,
"title": "Foo Bar",
"authors":
[
{
"first_name": "Tom",
"last_name": "Najdek"
},
{
"first_name": "Lorem",
"last_name": "Ipsum"
}
],
"type": "journal",
"folder_uuids": [
"8d2f262d-49b3-4dfc-8968-0bb71bcd92ea"
],
"year": 1987
}, {
"authored": false,
"confirmed": true,
"created": "2021-11-04T11:53:10.353Z",
"file_attached": false,
"hidden": false,
"id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef",
"last_modified": "2021-11-04T11:53:10.353Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"read": false,
"starred": false,
"title": "Sample Report",
"type": "report",
"year": 2002
}, {
"title": "Item with PDF",
"type": "journal",
"year": 2005,
"source": "Zotero",
"pages": "1-11",
"websites":
[
"https://zotero.org"
],
"id": "c54b0c6f-c4ce-4706-8742-bc7d032df862",
"created": "2021-11-09T10:26:15.201Z",
"file_attached": true,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"last_modified": "2021-11-09T10:26:16.303Z",
"read": false,
"starred": false,
"authored": false,
"confirmed": true,
"hidden": false,
"private_publication": false,
"abstract": "Lorem Ipsum. Nostrud elit ullamco laborum cillum.",
"files":
[
{
"id": "19fb5e5b-1a39-4851-b513-d48441a670e1",
"document_id": "c54b0c6f-c4ce-4706-8742-bc7d032df862",
"mime_type": "application/pdf",
"file_name": "item.pdf",
"size": 123456,
"created": "2021-11-09T10:26:16.292Z",
"filehash": "cc22c6611277df346ff8dc7386ba3880b2bafa15"
}
]
}
]

View file

@ -6,6 +6,7 @@
"file_attached": false,
"hidden": false,
"id": "7fea3cb3-f97d-3f16-8fad-f59caaa71688",
"client_data": "{\"desktop_id\":\"b5f57b1a-f083-486c-aec7-5d5edd366dd2\"}",
"last_modified": "2021-11-02T12:26:30.025Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
@ -42,6 +43,7 @@
"file_attached": false,
"hidden": false,
"id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef",
"client_data": "{\"desktop_id\":\"616ec6d1-8d23-4414-8b6e-7bb129677577\"}",
"last_modified": "2021-11-04T11:53:10.353Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
@ -61,6 +63,7 @@
"https://zotero.org"
],
"id": "c54b0c6f-c4ce-4706-8742-bc7d032df862",
"client_data": "{\"desktop_id\":\"3630a4bf-d97e-46c4-8611-61ec50f840c6\"}",
"created": "2021-11-09T10:26:15.201Z",
"file_attached": true,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",

View file

@ -2,10 +2,11 @@
{
"authored": false,
"confirmed": true,
"created": "2021-11-04T11:53:10.353Z",
"created": "2021-12-05T12:00:00.000Z",
"file_attached": false,
"hidden": false,
"id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef",
"client_data": "{\"desktop_id\":\"616ec6d1-8d23-4414-8b6e-7bb129677577\"}",
"last_modified": "2021-11-05T11:53:10.353Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
@ -15,5 +16,23 @@
"type": "journal",
"source": "lorem ipsum",
"year": 2002
},
{
"authored": false,
"confirmed": true,
"created": "2021-11-05T12:33:18.353Z",
"file_attached": false,
"hidden": false,
"id": "31a8251f-88b0-497b-9d30-1b2516771057",
"client_data": "{\"desktop_id\":\"86e56a00-5ae5-4fe8-a977-9298a03b16d6\"}",
"last_modified": "2021-11-05T12:33:18.353Z",
"private_publication": false,
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"read": false,
"starred": true,
"title": "Completely new item",
"type": "book",
"source": "lorem ipsum",
"year": 1999
}
]

View file

@ -1,12 +1,17 @@
/* global setHTTPResponse:false, sinon: false, Zotero_Import_Mendeley: false, HttpServer: false */
describe('Zotero_Import_Mendeley', function () {
var server, importer, httpd, httpdURL;
var server, httpd, httpdURL, importers;
const getImporter = () => {
const importer = new Zotero_Import_Mendeley();
importer.mendeleyAuth = { access_token: 'access_token', refresh_token: 'refresh_token' };// eslint-disable-line camelcase
importers.push(importer);
return importer;
};
before(async () => {
Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js');
importer = new Zotero_Import_Mendeley();
importer.mendeleyCode = 'CODE';
// real http server is used to deliver an empty pdf so that annotations can be processed during import
Components.utils.import("resource://zotero-unit/httpd.js");
@ -25,12 +30,15 @@ describe('Zotero_Import_Mendeley', function () {
});
beforeEach(async () => {
importers = [];
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
server = sinon.fakeServer.create();
server = sinon.fakeServer.create({
unsafeHeadersEnabled: false
});
server.autoRespond = true;
setHTTPResponse(server, 'https://www.zotero.org/', {
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'POST',
url: `utils/mendeley/oauth`,
url: `oauth/token`,
status: 200,
headers: {},
json: {
@ -127,12 +135,21 @@ describe('Zotero_Import_Mendeley', function () {
});
});
afterEach(() => {
afterEach(async () => {
await Promise.all(
importers
.map(importer => ([
Zotero.Items.erase(Array.from(new Set(importer.newItems)).map(i => i.id)),
Zotero.Collections.erase(Array.from(new Set(importer.newCollections)).map(c => c.id))
]))
.reduce((prev, a) => ([...prev, ...a]), []) // .flat() in >= FF62
);
Zotero.HTTP.mock = null;
});
describe('#import', () => {
it("should import collections, items, attachments & annotations", async () => {
const importer = getImporter();
await importer.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
@ -140,17 +157,17 @@ describe('Zotero_Import_Mendeley', function () {
});
const journal = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '7fea3cb3-f97d-3f16-8fad-f59caaa71688'))
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', 'b5f57b1a-f083-486c-aec7-5d5edd366dd2'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
const report = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef'))
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
const withpdf = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', 'c54b0c6f-c4ce-4706-8742-bc7d032df862'))
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '3630a4bf-d97e-46c4-8611-61ec50f840c6'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
@ -159,10 +176,14 @@ describe('Zotero_Import_Mendeley', function () {
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.equal(journal.getRelations()['mendeleyDB:remoteDocumentUUID'], '7fea3cb3-f97d-3f16-8fad-f59caaa71688');
assert.equal(journal.getField('title'), 'Foo Bar');
assert.equal(journal.itemTypeID, Zotero.ItemTypes.getID('journalArticle'));
assert.equal(report.getRelations()['mendeleyDB:remoteDocumentUUID'], '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef');
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report'));
assert.equal(withpdf.getRelations()['mendeleyDB:remoteDocumentUUID'], 'c54b0c6f-c4ce-4706-8742-bc7d032df862');
assert.equal(withpdf.getField('title'), 'Item with PDF');
assert.equal(withpdf.itemTypeID, Zotero.ItemTypes.getID('journalArticle'));
@ -237,22 +258,23 @@ describe('Zotero_Import_Mendeley', function () {
assert.equal(parentCollection.name, 'folder1');
});
it("should update previously imported item", async () => {
const importer = new Zotero_Import_Mendeley();
importer.mendeleyCode = 'CODE';
await importer.translate({
it("should update previously imported item, based on config", async () => {
const importer1 = getImporter();
await importer1.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
const report = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef'))
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.getField('year'), '2002');
assert.equal(report.getField('dateAdded'), '2021-11-04 11:53:10');
assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report'));
assert.lengthOf(report.getTags(), 0);
@ -266,7 +288,9 @@ describe('Zotero_Import_Mendeley', function () {
)
});
await importer.translate({
const importer2 = getImporter();
importer2.newItemsOnly = false;
await importer2.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
@ -276,6 +300,107 @@ describe('Zotero_Import_Mendeley', function () {
assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('journalArticle'));
assert.equal(report.getField('year'), '2002');
assert.sameMembers(report.getTags().map(t => t.tag), ['\u2605']);
// dateAdded shouldn't change on an updated item. See #2881
assert.equal(report.getField('dateAdded'), '2021-11-04 11:53:10');
});
it("shouldn't update previously imported item, based on config", async () => {
const importer1 = getImporter();
await importer1.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
const report = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
const noNewItemHere = await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '86e56a00-5ae5-4fe8-a977-9298a03b16d6');
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.getField('year'), '2002');
assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report'));
assert.lengthOf(report.getTags(), 0);
assert.lengthOf(noNewItemHere, 0);
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: `documents?view=all&limit=500`,
status: 200,
headers: {},
json: JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-updated.json')
)
});
const importer2 = getImporter();
importer2.newItemsOnly = true;
await importer2.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report'));
assert.equal(report.getField('year'), '2002');
assert.lengthOf(report.getTags(), 0);
const newItem = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '86e56a00-5ae5-4fe8-a977-9298a03b16d6'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.equal(newItem.getField('title'), 'Completely new item');
});
it("should correct IDs if available on subsequent import", async () => {
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: `documents?view=all&limit=500`,
status: 200,
headers: {},
json: JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-simple-no-desktop-id.json')
)
});
const importer = getImporter();
importer.newItemsOnly = true;
await importer.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
const report = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:remoteDocumentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.getRelations()['mendeleyDB:documentUUID'], '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef');
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: `documents?view=all&limit=500`,
status: 200,
headers: {},
json: JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-simple.json')
)
});
await importer.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
assert.equal(report.getField('title'), 'Sample Report');
assert.equal(report.getRelations()['mendeleyDB:documentUUID'], '616ec6d1-8d23-4414-8b6e-7bb129677577');
});
});
});