From 6e67e9c2536b7dd72abb70f8a4aaa69df5144162 Mon Sep 17 00:00:00 2001 From: Sebastian Mihalache <sebi.mihalache@gmail.com> Date: Mon, 3 Dec 2018 13:25:44 +0200 Subject: [PATCH] feat(recommendations): submit revision after eic request --- .../src/fixtures/fragments.js | 21 ++++ .../src/services/Collection.js | 7 +- .../src/services/Fragment.js | 48 ++++++-- .../src/notifications/emailCopy.js | 13 +++ .../src/notifications/notification.js | 96 +++++++++++++++ .../fragments/notifications/emailCopy.js | 16 +-- .../fragments/notifications/notifications.js | 97 --------------- .../src/routes/fragments/patch.js | 110 ++++-------------- .../strategies/eicRequestRevision.js | 23 ++++ .../fragments/strategies/heRequestRevision.js | 86 ++++++++++++++ .../src/tests/collections/get.test.js | 1 - .../src/tests/fragments/patch.test.js | 51 +++++++- 12 files changed, 358 insertions(+), 211 deletions(-) create mode 100644 packages/component-manuscript-manager/src/routes/fragments/strategies/eicRequestRevision.js create mode 100644 packages/component-manuscript-manager/src/routes/fragments/strategies/heRequestRevision.js diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index 337c0d943..475a277b9 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -183,6 +183,27 @@ const fragments = { createdOn: 1542361115751, updatedOn: chance.timestamp(), }, + { + recommendation: 'revision', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: admin.id, + createdOn: chance.timestamp(), + updatedOn: chance.timestamp(), + }, ], authors: [ { diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 80739bc11..ece6c1339 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -13,8 +13,6 @@ const { set, } = require('lodash') -// const { features = {}, recommendations: configRecommendations } = config - const Fragment = require('./Fragment') class Collection { @@ -201,6 +199,11 @@ class Collection { hasHandlingEditor() { return has(this.collection, 'handlingEditor') } + + async addFragment(newFragmentId) { + this.collection.fragments.push(newFragmentId) + await this.collection.save() + } } const sendMTSPackage = async ({ diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index c990c24d7..8cdd85f60 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -1,4 +1,4 @@ -const { get, remove, findLast, pick, chain } = require('lodash') +const { get, remove, findLast, pick, chain, omit } = require('lodash') const config = require('config') const User = require('./User') @@ -124,13 +124,12 @@ class Fragment { getLatestHERequestToRevision() { const { fragment: { recommendations = [] } } = this - return recommendations - .filter( - rec => - rec.recommendationType === 'editorRecommendation' && - (rec.recommendation === 'minor' || rec.recommendation === 'major'), - ) - .sort((a, b) => b.createdOn - a.createdOn)[0] + return findLast( + recommendations, + rec => + rec.recommendationType === 'editorRecommendation' && + (rec.recommendation === 'minor' || rec.recommendation === 'major'), + ) } async getReviewers({ UserModel, type }) { @@ -255,6 +254,39 @@ class Fragment { .last() .value() } + + async createFragmentFromRevision(FragmentModel) { + const newFragmentBody = { + ...omit(this.fragment, ['revision', 'recommendations', 'id']), + ...this.fragment.revision, + invitations: this.getInvitations({ + isAccepted: true, + type: 'submitted', + }), + version: this.fragment.version + 1, + created: new Date(), + } + + let newFragment = new FragmentModel(newFragmentBody) + newFragment = await newFragment.save() + + return newFragment + } + + async removeRevision() { + delete this.fragment.revision + await this.fragment.save() + } + + getLatestEiCRequestToRevision() { + const { fragment: { recommendations = [] } } = this + return findLast( + recommendations, + rec => + rec.recommendationType === 'editorRecommendation' && + rec.recommendation === 'revision', + ) + } } module.exports = Fragment diff --git a/packages/component-manuscript-manager/src/notifications/emailCopy.js b/packages/component-manuscript-manager/src/notifications/emailCopy.js index 3aa154fcd..f55b47978 100644 --- a/packages/component-manuscript-manager/src/notifications/emailCopy.js +++ b/packages/component-manuscript-manager/src/notifications/emailCopy.js @@ -11,6 +11,7 @@ const getEmailCopy = ({ comments = '', targetUserName = '', eicName = 'Editor in Chief', + expectedDate = new Date(), }) => { let paragraph let hasLink = true @@ -131,6 +132,18 @@ const getEmailCopy = ({ paragraph = `We regret to inform you that ${titleText} has been returned with comments. Please click the link below to access the manuscript.<br/><br/> Comments: ${comments}<br/><br/>` break + case 'submitted-reviewers-after-revision': + paragraph = `The authors have submitted a new version of ${titleText}, which you reviewed for ${journalName}.<br/><br/> + As you reviewed the previous version of this manuscript, I would be grateful if you could review this revision and submit a new report by ${expectedDate}. + To download the updated PDF and proceed with the review process, please visit the manuscript details page.<br/><br/> + Thank you again for reviewing for ${journalName}.` + break + case 'he-new-version-submitted': + hasIntro = false + hasSignature = false + paragraph = `The authors of ${titleText} have submitted a revised version. <br/><br/> + To review this new submission and proceed with the review process, please visit the manuscript details page.` + break default: throw new Error(`The ${emailType} email type is not defined.`) } diff --git a/packages/component-manuscript-manager/src/notifications/notification.js b/packages/component-manuscript-manager/src/notifications/notification.js index 506e9ba19..ff6c733a1 100644 --- a/packages/component-manuscript-manager/src/notifications/notification.js +++ b/packages/component-manuscript-manager/src/notifications/notification.js @@ -629,6 +629,102 @@ class Notification { }) } + async notifyReviewersWhenAuthorSubmitsMajorRevision(newFragmentId) { + const { fragmentHelper } = await this._getNotificationProperties() + const { collection, UserModel } = this + + const handlingEditor = get(collection, 'handlingEditor') + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) + + const reviewers = await fragmentHelper.getReviewers({ + UserModel, + type: 'submitted', + }) + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'submitted-reviewers-after-revision', + titleText: `the manuscript titled "${parsedFragment.title}"`, + expectedDate: services.getExpectedDate({ daysExpected: 14 }), + }) + + reviewers.forEach(reviewer => { + const email = new Email({ + type: 'user', + fromEmail: `${handlingEditor.name} <${staffEmail}>`, + toUser: { + email: reviewer.email, + name: `${reviewer.lastName}`, + }, + content: { + subject: `${ + collection.customId + }: A manuscript you reviewed has been revised`, + paragraph, + signatureName: handlingEditor.name, + signatureJournal: journalName, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${collection.id}/versions/${newFragmentId}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: reviewer.id, + token: reviewer.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + }) + } + + async notifyHandlingEditorWhenAuthorSubmitsRevision(newFragment) { + const { collection, UserModel } = this + + const handlingEditor = get(collection, 'handlingEditor') + + const fragmentHelper = new Fragment({ fragment: newFragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'he-new-version-submitted', + titleText: `the manuscript titled "${parsedFragment.title}"`, + }) + + const heUser = await UserModel.find(handlingEditor.id) + + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: heUser.email, + }, + content: { + subject: `${collection.customId}: Revision submitted`, + paragraph, + signatureName: '', + signatureJournal: journalName, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${collection.id}/versions/${newFragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + } + async _getNotificationProperties() { const fragmentHelper = new Fragment({ fragment: this.fragment }) const parsedFragment = await fragmentHelper.getFragmentData({ diff --git a/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js index ecfadb0a4..e6ab3f502 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js +++ b/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js @@ -5,21 +5,9 @@ const journalName = config.get('journal.name') const getEmailCopy = ({ emailType, titleText, expectedDate, customId }) => { let paragraph const hasLink = true - let hasIntro = true - let hasSignature = true + const hasIntro = true + const hasSignature = true switch (emailType) { - case 'he-new-version-submitted': - hasIntro = false - hasSignature = false - paragraph = `The authors of ${titleText} have submitted a revised version. <br/><br/> - To review this new submission and proceed with the review process, please visit the manuscript details page.` - break - case 'submitted-reviewers-after-revision': - paragraph = `The authors have submitted a new version of ${titleText}, which you reviewed for ${journalName}.<br/><br/> - As you reviewed the previous version of this manuscript, I would be grateful if you could review this revision and submit a new report by ${expectedDate}. - To download the updated PDF and proceed with the review process, please visit the manuscript details page.<br/><br/> - Thank you again for reviewing for ${journalName}.` - break case 'eqs-manuscript-submitted': paragraph = `Manuscript ID ${customId} has been submitted and a package has been sent. Please click on the link below to either approve or reject the manuscript:` break diff --git a/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js index dcbe3c22f..159f71208 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js @@ -15,103 +15,6 @@ const unsubscribeSlug = config.get('unsubscribe.url') const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendHandlingEditorEmail({ baseUrl, fragment, UserModel, collection }) { - const fragmentHelper = new Fragment({ fragment }) - const handlingEditor = get(collection, 'handlingEditor') - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor, - }) - - const { paragraph, ...bodyProps } = getEmailCopy({ - emailType: 'he-new-version-submitted', - titleText: `the manuscript titled "${parsedFragment.title}"`, - }) - - const heUser = await UserModel.find(handlingEditor.id) - - const email = new Email({ - type: 'user', - fromEmail: `${journalName} <${staffEmail}>`, - toUser: { - email: heUser.email, - }, - content: { - subject: `${collection.customId}: Revision submitted`, - paragraph, - signatureName: '', - signatureJournal: journalName, - ctaLink: services.createUrl( - baseUrl, - `/projects/${collection.id}/versions/${fragment.id}/details`, - ), - ctaText: 'MANUSCRIPT DETAILS', - unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { - id: heUser.id, - token: heUser.accessTokens.unsubscribe, - }), - }, - bodyProps, - }) - - return email.sendEmail() - }, - - async sendReviewersEmail({ - baseUrl, - fragment, - UserModel, - collection, - previousVersion, - }) { - const fragmentHelper = new Fragment({ fragment: previousVersion }) - const handlingEditor = get(collection, 'handlingEditor') - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor, - }) - - const reviewers = await fragmentHelper.getReviewers({ - UserModel, - type: 'submitted', - }) - - const { paragraph, ...bodyProps } = getEmailCopy({ - emailType: 'submitted-reviewers-after-revision', - titleText: `the manuscript titled "${parsedFragment.title}"`, - expectedDate: services.getExpectedDate({ daysExpected: 14 }), - }) - - reviewers.forEach(reviewer => { - const email = new Email({ - type: 'user', - fromEmail: `${handlingEditor.name} <${staffEmail}>`, - toUser: { - email: reviewer.email, - name: `${reviewer.lastName}`, - }, - content: { - subject: `${ - collection.customId - }: A manuscript you reviewed has been revised`, - paragraph, - signatureName: handlingEditor.name, - signatureJournal: journalName, - ctaLink: services.createUrl( - baseUrl, - `/projects/${collection.id}/versions/${fragment.id}/details`, - ), - ctaText: 'MANUSCRIPT DETAILS', - unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { - id: reviewer.id, - token: reviewer.accessTokens.unsubscribe, - }), - }, - bodyProps, - }) - - return email.sendEmail() - }) - }, - async sendEQSEmail({ baseUrl, fragment, UserModel, collection }) { const userHelper = new User({ UserModel }) const eicName = await userHelper.getEiCName() diff --git a/packages/component-manuscript-manager/src/routes/fragments/patch.js b/packages/component-manuscript-manager/src/routes/fragments/patch.js index 6a3e11ef0..3337811e3 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js @@ -1,5 +1,3 @@ -const { union, omit } = require('lodash') - const { Team, services, @@ -8,7 +6,10 @@ const { authsome: authsomeHelper, } = require('pubsweet-component-helper-service') -const notifications = require('./notifications/notifications') +const Notification = require('../../notifications/notification') + +const eicRequestRevision = require('./strategies/eicRequestRevision') +const heRequestRevision = require('./strategies/heRequestRevision') module.exports = models => async (req, res) => { const { collectionId, fragmentId } = req.params @@ -42,100 +43,33 @@ module.exports = models => async (req, res) => { const collectionHelper = new Collection({ collection }) const fragmentHelper = new Fragment({ fragment }) - const heRecommendation = fragmentHelper.getLatestHERequestToRevision() - if (!heRecommendation) { - return res.status(400).json({ - error: 'No Handling Editor request to revision has been found.', - }) - } - - const newFragmentBody = { - ...omit(fragment, ['revision', 'recommendations', 'id']), - ...fragment.revision, - invitations: fragmentHelper.getInvitations({ - isAccepted: true, - type: 'submitted', - }), - version: fragment.version + 1, - created: new Date(), - } - - let newFragment = new models.Fragment(newFragmentBody) - newFragment = await newFragment.save() - const teamHelper = new Team({ - TeamModel: models.Team, - collectionId, - fragmentId: newFragment.id, - }) - delete fragment.revision - fragment.save() - - if (heRecommendation.recommendation === 'major') { - const reviewerIds = newFragment.invitations.map(inv => inv.userId) - - teamHelper.createTeam({ - role: 'reviewer', - members: reviewerIds, - objectType: 'fragment', - }) - } else { - delete newFragment.invitations - await newFragment.save() - } - - const authorIds = newFragment.authors.map(auth => { - const { id } = auth - return id - }) - - let authorsTeam = await teamHelper.getTeam({ - role: 'author', - objectType: 'fragment', - }) - if (!authorsTeam) { - authorsTeam = await teamHelper.createTeam({ - role: 'author', - members: authorIds, - objectType: 'fragment', - }) - } else { - authorsTeam.members = union(authorsTeam.members, authorIds) - await authorsTeam.save() + const strategies = { + he: heRequestRevision, + eic: eicRequestRevision, } - const fragments = await collectionHelper.getAllFragments({ - FragmentModel: models.Fragment, - }) - - await collectionHelper.updateStatusByRecommendation({ - recommendation: heRecommendation.recommendation, - fragments, - }) - - newFragment.submitted = Date.now() - newFragment = await newFragment.save() - collection.fragments.push(newFragment.id) - collection.save() + const role = collection.handlingEditor ? 'he' : 'eic' - notifications.sendHandlingEditorEmail({ - baseUrl: services.getBaseUrl(req), - fragment: newFragment, - UserModel: models.User, + const notification = new Notification({ + fragment, collection, + UserModel: models.User, + baseUrl: services.getBaseUrl(req), }) - if (heRecommendation.recommendation === 'major') { - notifications.sendReviewersEmail({ - baseUrl: services.getBaseUrl(req), - fragment: newFragment, - UserModel: models.User, - collection, - previousVersion: fragment, + try { + const newFragment = await strategies[role].execute({ + models, + notification, + fragmentHelper, + collectionHelper, + TeamHelper: Team, }) + return res.status(200).json(newFragment) + } catch (e) { + return res.status(400).json({ error: e.message }) } - - return res.status(200).json(newFragment) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') return res.status(notFoundError.status).json({ diff --git a/packages/component-manuscript-manager/src/routes/fragments/strategies/eicRequestRevision.js b/packages/component-manuscript-manager/src/routes/fragments/strategies/eicRequestRevision.js new file mode 100644 index 000000000..cbd743fe6 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/strategies/eicRequestRevision.js @@ -0,0 +1,23 @@ +module.exports = { + execute: async ({ models, fragmentHelper, collectionHelper }) => { + const eicRequestToRevision = fragmentHelper.getLatestEiCRequestToRevision() + if (!eicRequestToRevision) { + throw new Error('No Editor in Chief request to revision has been found.') + } + + let newFragment = await fragmentHelper.createFragmentFromRevision( + models.Fragment, + ) + + await fragmentHelper.removeRevision() + + await collectionHelper.updateStatus({ newStatus: 'submitted' }) + + newFragment.submitted = Date.now() + newFragment = await newFragment.save() + + await collectionHelper.addFragment(newFragment.id) + + return newFragment + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragments/strategies/heRequestRevision.js b/packages/component-manuscript-manager/src/routes/fragments/strategies/heRequestRevision.js new file mode 100644 index 000000000..fcba20ed4 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/strategies/heRequestRevision.js @@ -0,0 +1,86 @@ +const { union } = require('lodash') + +module.exports = { + execute: async ({ + models, + TeamHelper, + notification, + fragmentHelper, + collectionHelper, + }) => { + const heRequestToRevision = fragmentHelper.getLatestHERequestToRevision() + if (!heRequestToRevision) { + throw new Error('No Handling Editor request to revision has been found.') + } + + let newFragment = await fragmentHelper.createFragmentFromRevision( + models.Fragment, + ) + await fragmentHelper.removeRevision() + + const teamHelper = new TeamHelper({ + TeamModel: models.Team, + fragmentId: newFragment.id, + }) + + if (heRequestToRevision.recommendation === 'major') { + const reviewerIds = newFragment.invitations.map(inv => inv.userId) + + teamHelper.createTeam({ + role: 'reviewer', + members: reviewerIds, + objectType: 'fragment', + }) + } else { + delete newFragment.invitations + await newFragment.save() + } + + const authorIds = newFragment.authors.map(auth => { + const { id } = auth + return id + }) + + let authorsTeam = await teamHelper.getTeam({ + role: 'author', + objectType: 'fragment', + }) + + if (!authorsTeam) { + authorsTeam = await teamHelper.createTeam({ + role: 'author', + members: authorIds, + objectType: 'fragment', + }) + } else { + authorsTeam.members = union(authorsTeam.members, authorIds) + await authorsTeam.save() + } + + const fragments = await collectionHelper.getAllFragments({ + FragmentModel: models.Fragment, + }) + + await collectionHelper.updateStatusByRecommendation({ + recommendation: heRequestToRevision.recommendation, + fragments, + }) + + newFragment.submitted = Date.now() + newFragment = await newFragment.save() + + await collectionHelper.addFragment(newFragment.id) + + await notification.notifyHandlingEditorWhenAuthorSubmitsRevision( + newFragment, + ) + + if (heRequestToRevision.recommendation === 'major') { + await notification.notifyReviewersWhenAuthorSubmitsMajorRevision( + newFragment.id, + ) + } + + return newFragment + }, +} diff --git a/packages/component-manuscript-manager/src/tests/collections/get.test.js b/packages/component-manuscript-manager/src/tests/collections/get.test.js index b721805c9..28c027fd4 100644 --- a/packages/component-manuscript-manager/src/tests/collections/get.test.js +++ b/packages/component-manuscript-manager/src/tests/collections/get.test.js @@ -60,7 +60,6 @@ describe('Get collections route handler', () => { const data = JSON.parse(res._getData()) expect(data).toHaveLength(2) expect(data[0].type).toEqual('collection') - expect(data[0].currentVersion.recommendations).toHaveLength(6) expect(data[0].currentVersion.authors[0]).not.toHaveProperty('email') }) diff --git a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js index be867cbe7..b6cb60fb8 100644 --- a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js @@ -83,7 +83,7 @@ describe('Patch fragments route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Item not found') }) - it('should return an error when no HE recommendation exists', async () => { + it('should return an error when no HE request to revision exists', async () => { const { user } = testFixtures.users const { fragment } = testFixtures.fragments const { collection } = testFixtures.collections @@ -171,4 +171,53 @@ describe('Patch fragments route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Unauthorized.') }) + it('should return an error when no EiC request to revision exists', async () => { + const { user } = testFixtures.users + const { fragment } = testFixtures.fragments + const { collection } = testFixtures.collections + fragment.recommendations.length = 0 + delete collection.handlingEditor + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + 'No Editor in Chief request to revision has been found.', + ) + }) + it('should return success when an EiC request to revision exists', async () => { + const { user } = testFixtures.users + const { fragment } = testFixtures.fragments + const { collection } = testFixtures.collections + + delete collection.handlingEditor + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data).toHaveProperty('submitted') + expect(collection.status).toBe('submitted') + }) }) -- GitLab