diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js index c417d2ac6b25a13854b5163eba90ea9bfeec2e7d..a673d629ba8bbdd938cb62e411bd8fcb639d3983 100644 --- a/packages/component-invite/src/helpers/Collection.js +++ b/packages/component-invite/src/helpers/Collection.js @@ -2,6 +2,7 @@ const invitationHelper = require('./Invitation') const mailService = require('pubsweet-component-mail-service') const logger = require('@pubsweet/logger') const config = require('config') +const last = require('lodash/last') const statuses = config.get('statuses') module.exports = { @@ -62,4 +63,27 @@ module.exports = { collection.visibleStatus = statuses[collection.status].private await collection.save() }, + getFragmentAndAuthorData: async (models, collection) => { + const fragment = await models.Fragment.find(last(collection.fragments)) + let { title } = fragment.metadata + title = title.replace(/<(.|\n)*?>/g, '') + + const submittingAuthorData = collection.authors.find( + author => author.isSubmitting === true, + ) + const author = await models.User.find(submittingAuthorData.userId) + const authorName = `${author.firstName} ${author.lastName}` + const { id } = fragment + return { title, authorName, id } + }, + updateReviewerCollectionStatus: async collection => { + const reviewerInvitations = collection.invitations.filter( + inv => inv.role === 'reviewer', + ) + if (reviewerInvitations.length === 0) { + collection.status = 'heAssigned' + collection.visibleStatus = statuses[collection.status].private + } + await collection.save() + }, } diff --git a/packages/component-invite/src/helpers/User.js b/packages/component-invite/src/helpers/User.js index 5da4184d5a729b6151015a92554ec57788be954f..05adf53629021ea8869c858014520e5795b7eaf2 100644 --- a/packages/component-invite/src/helpers/User.js +++ b/packages/component-invite/src/helpers/User.js @@ -1,42 +1,102 @@ const helpers = require('./helpers') const mailService = require('pubsweet-component-mail-service') const logger = require('@pubsweet/logger') +const collectionHelper = require('./Collection') -module.exports = { - setupNewUser: async ( - body, - url, - res, +const setupNewUser = async ( + body, + url, + res, + email, + role, + UserModel, + invitationType, +) => { + const { firstName, lastName, affiliation, title } = body + const newUser = await helpers.createNewUser( email, - role, + firstName, + lastName, + affiliation, + title, UserModel, - invitationType, - ) => { - const { firstName, lastName, affiliation, title } = body - const newUser = await helpers.createNewUser( - email, - firstName, - lastName, - affiliation, + role, + ) + + try { + if (role !== 'reviewer') { + await mailService.setupInviteEmail(newUser, invitationType, url) + } + + return newUser + } catch (e) { + logger.error(e.message) + return { status: 500, error: 'Email could not be sent.' } + } +} + +const getEditorInChief = async UserModel => { + const users = await UserModel.all() + const eic = users.find(user => user.editorInChief === true) + return eic +} + +const setupReviewerDeclinedEmailData = async ( + models, + collection, + req, + res, + user, + mailService, +) => { + const { + title, + authorName, + id, + } = await collectionHelper.getFragmentAndAuthorData(models, collection) + const eic = await getEditorInChief(models.User) + const toEmail = collection.handlingEditor.email + try { + await mailService.setupReviewerDeclinedEmail( + toEmail, + user, + collection, title, - UserModel, - role, + authorName, + id, + `${eic.firstName} ${eic.lastName}`, + `${req.protocol}://${req.get('host')}`, ) + await user.save() + res.status(200).json({}) + } catch (e) { + logger.error(e) + return res.status(500).json({ error: 'Email could not be sent.' }) + } +} - try { - if (role !== 'reviewer') { - await mailService.setupInviteEmail(newUser, invitationType, url) - } +const setupReviewerUnassignEmail = async ( + models, + collection, + user, + mailService, +) => { + const { title, authorName } = await collectionHelper.getFragmentAndAuthorData( + models, + collection, + ) - return newUser - } catch (e) { - logger.error(e.message) - return { status: 500, error: 'Email could not be sent.' } - } - }, - getEditorInChief: async UserModel => { - const users = await UserModel.all() - const eic = users.find(user => user.editorInChief === true) - return eic - }, + await mailService.setupReviewerUnassignEmail( + user, + collection, + title, + authorName, + ) +} + +module.exports = { + setupNewUser, + getEditorInChief, + setupReviewerDeclinedEmailData, + setupReviewerUnassignEmail, } diff --git a/packages/component-invite/src/routes/collectionsInvitations/decline.js b/packages/component-invite/src/routes/collectionsInvitations/decline.js index 036182660aec35ef2f6deab2e57340842f7056ec..d0b2682ee267458ac7a8e4ca35c0de2c95a34cc5 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/decline.js +++ b/packages/component-invite/src/routes/collectionsInvitations/decline.js @@ -1,7 +1,7 @@ -const logger = require('@pubsweet/logger') const helpers = require('../../helpers/helpers') const teamHelper = require('../../helpers/Team') const mailService = require('pubsweet-component-mail-service') +const userHelper = require('../../helpers/User') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params @@ -36,7 +36,6 @@ module.exports = models => async (req, res) => { invitation.timestamp = Date.now() invitation.hasAnswer = true - const toEmail = collection.handlingEditor.email invitation.isAccepted = false const team = await teamHelper.getTeamByGroupAndCollection( collectionId, @@ -46,30 +45,14 @@ module.exports = models => async (req, res) => { await collection.save() await teamHelper.removeTeamMember(team.id, user.id, models.Team) user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) - const fragment = await models.Fragment.find(collection.fragments[0]) - let { title } = fragment.metadata - title = title.replace(/<(.|\n)*?>/g, '') - - const submittingAuthorData = collection.authors.find( - author => author.isSubmitting === true, + return await userHelper.setupReviewerDeclinedEmailData( + models, + collection, + req, + res, + user, + mailService, ) - const author = await models.User.find(submittingAuthorData.userId) - try { - await mailService.setupReviewerDeclineEmail( - toEmail, - user, - collection, - title, - `${author.firstName} ${author.lastName}`, - ) - } catch (e) { - logger.error(e) - return res.status(500).json({ error: 'Email could not be sent.' }) - } - - await user.save() - res.status(200).json({}) - return } catch (e) { const notFoundError = await helpers.handleNotFoundError(e, 'item') return res.status(notFoundError.status).json({ diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index 3ca21d8320670c517bd688207fd640e94c119e0f..39da0de3a206fe2573b698cc2be60b675ba9abdd 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -3,6 +3,8 @@ const teamHelper = require('../../helpers/Team') const mailService = require('pubsweet-component-mail-service') const logger = require('@pubsweet/logger') const config = require('config') +const userHelper = require('../../helpers/User') +const collectionHelper = require('../../helpers/Collection') const statuses = config.get('statuses') module.exports = models => async (req, res) => { @@ -34,13 +36,7 @@ module.exports = models => async (req, res) => { collection.visibleStatus = statuses[collection.status].private delete collection.handlingEditor } else if (invitation.role === 'reviewer') { - const reviewerInvitations = collection.invitations.filter( - inv => inv.role === 'reviewer', - ) - if (reviewerInvitations.length === 0) { - collection.status = 'heAssigned' - collection.visibleStatus = statuses[collection.status].private - } + await collectionHelper.updateReviewerCollectionStatus(collection) } await collection.save() await teamHelper.removeTeamMember(team.id, invitation.userId, models.Team) @@ -48,20 +44,19 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() try { - let emailType - switch (invitation.role) { - case 'handlingEditor': - emailType = 'revoke-handling-editor' - break - case 'reviewer': - emailType = 'unassign-reviewer' - break - default: - return res.status(500).json({ - error: 'Something went wrong', - }) + if (invitation.role === 'handlingEditor') { + await mailService.setupRevokeInvitationEmail( + user.email, + 'revoke-handling-editor', + ) + } else if (invitation.role === 'reviewer') { + await userHelper.setupReviewerUnassignEmail( + models, + collection, + user, + mailService, + ) } - await mailService.setupRevokeInvitationEmail(user.email, emailType) return res.status(200).json({}) } catch (e) { diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index 1d3ebb479fd06796f2fbbb5af3d44a41ffd38fcd..c23e245eafa38622c1bac42345ff734a99aa36dd 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -4,7 +4,9 @@ const teamHelper = require('../../helpers/Team') const mailService = require('pubsweet-component-mail-service') const userHelper = require('../../helpers/User') const collectionHelper = require('../../helpers/Collection') +const config = require('config') +const statuses = config.get('statuses') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const { isAccepted, reason } = req.body @@ -17,7 +19,7 @@ module.exports = models => async (req, res) => { let user = await models.User.find(req.user) try { - const collection = await models.Collection.find(collectionId) + let collection = await models.Collection.find(collectionId) const invitation = await collection.invitations.find( invitation => invitation.id === invitationId, ) @@ -46,6 +48,13 @@ module.exports = models => async (req, res) => { toEmail = collection.handlingEditor.email if (isAccepted === true) { invitation.isAccepted = true + if ( + invitation.role === 'reviewer' && + collection.status === 'reviewersInvited' + ) { + collection.status = 'underReview' + collection.visibleStatus = statuses[collection.status].private + } await collection.save() try { await mailService.setupAgreeEmail( @@ -85,20 +94,14 @@ module.exports = models => async (req, res) => { reason, ) } else if (invitation.role === 'reviewer') { - const fragment = await models.Fragment.find(collection.fragments[0]) - let { title } = fragment.metadata - title = title.replace(/<(.|\n)*?>/g, '') - - const submittingAuthorData = collection.authors.find( - author => author.isSubmitting === true, - ) - const author = await models.User.find(submittingAuthorData.userId) - await mailService.setupReviewerDeclineEmail( - toEmail, - user, + await collectionHelper.updateReviewerCollectionStatus(collection) + return await userHelper.setupReviewerDeclinedEmailData( + models, collection, - title, - `${author.firstName} ${author.lastName}`, + req, + res, + user, + mailService, ) } } catch (e) { diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js index 2f1600567b0612eae80f9765cc9131c857f5c7aa..c03b6779d8c4b9c9c10111014e7e024f8b293385 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js @@ -10,6 +10,7 @@ jest.mock('pubsweet-component-mail-service', () => ({ setupAssignEmail: jest.fn(), setupAgreeEmail: jest.fn(), setupDeclineEmail: jest.fn(), + setupReviewerDeclinedEmail: jest.fn(), })) const reqBody = { diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index 6d4846046e1f881fff32a99ffba7eb3ea97a6618..17bcee132b9c4542df1d31f68bff7ae5407a5c2a 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -199,8 +199,7 @@ module.exports = { } return Email.send(mailData) }, - setupReviewerDeclineEmail: async ( - toEmail, + setupReviewerUnassignEmail: async ( invitedUser, collection, title, @@ -229,6 +228,42 @@ module.exports = { } return Email.send(mailData) }, + setupReviewerDeclinedEmail: async ( + toEmail, + invitedUser, + collection, + title, + authorName, + fragmentId, + eicName, + baseUrl, + ) => { + const subject = `${collection.customId}: Manuscript Reviews` + const manuscriptUrl = `${baseUrl}/projects/${ + collection.id + }/versions/${fragmentId}/details` + const replacements = { + reviewerName: `${invitedUser.firstName} ${invitedUser.lastName}`, + title, + editorName: collection.handlingEditor.name, + authorName, + manuscriptUrl, + eicName, + } + + const { htmlBody, textBody } = getEmailBody( + 'reviewer-declined', + replacements, + ) + const mailData = { + from: config.get('mailer.from'), + to: toEmail, + subject, + text: textBody, + html: htmlBody, + } + return Email.send(mailData) + }, } const getEmailBody = (emailType, replacements) => { diff --git a/packages/component-mail-service/src/templates/reviewer-declined.html b/packages/component-mail-service/src/templates/reviewer-declined.html new file mode 100644 index 0000000000000000000000000000000000000000..9cb7f95c6832939a7d88318e76d3653949bb8d30 --- /dev/null +++ b/packages/component-mail-service/src/templates/reviewer-declined.html @@ -0,0 +1,219 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> + <!--[if !mso]><!--> + <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> + <!--<![endif]--> + <!--[if (gte mso 9)|(IE)]> + <xml> + <o:OfficeDocumentSettings> + <o:AllowPNG/> + <o:PixelsPerInch>96</o:PixelsPerInch> + </o:OfficeDocumentSettings> + </xml> + <![endif]--> + <!--[if (gte mso 9)|(IE)]> + <style type="text/css"> + body {width: 600px;margin: 0 auto;} + table {border-collapse: collapse;} + table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;} + img {-ms-interpolation-mode: bicubic;} + </style> + <![endif]--> + + <style type="text/css"> + body, + p, + div { + font-family: helvetica, arial, sans-serif; + font-size: 14px; + } + + body { + color: #626262; + } + + body a { + color: #0D78F2; + text-decoration: none; + } + + p { + margin: 0; + padding: 0; + } + + table.wrapper { + width: 100% !important; + table-layout: fixed; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + img.max-width { + max-width: 100% !important; + } + + .column.of-2 { + width: 50%; + } + + .column.of-3 { + width: 33.333%; + } + + .column.of-4 { + width: 25%; + } + + @media screen and (max-width:480px) { + .preheader .rightColumnContent, + .footer .rightColumnContent { + text-align: left !important; + } + .preheader .rightColumnContent div, + .preheader .rightColumnContent span, + .footer .rightColumnContent div, + .footer .rightColumnContent span { + text-align: left !important; + } + .preheader .rightColumnContent, + .preheader .leftColumnContent { + font-size: 80% !important; + padding: 5px 0; + } + table.wrapper-mobile { + width: 100% !important; + table-layout: fixed; + } + img.max-width { + height: auto !important; + max-width: 480px !important; + } + a.bulletproof-button { + display: block !important; + width: auto !important; + font-size: 80%; + padding-left: 0 !important; + padding-right: 0 !important; + } + .columns { + width: 100% !important; + } + .column { + display: block !important; + width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; + } + } + </style> + <!--user entered Head Start--> + + <!--End Head user entered--> +</head> + +<body> + <center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;"> + <div class="webkit"> + <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4"> + <tr> + <td valign="top" bgcolor="#F4F4F4" width="100%"> + <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td width="100%"> + <table width="100%" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td> + <!--[if mso]> + <center> + <table><tr><td width="600"> + <![endif]--> + <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center"> + <tr> + <td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%" + align="left"> + + <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" + width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> + <tr> + <td role="module-content"> + <p>reviewer declined</p> + </td> + </tr> + </table> + + <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center"> + <img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;" + src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png" + alt="" width="60"> + </td> + </tr> + </table> + + <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> + <div> + <p data-pm-slice="1 1 []">Dear {{editorName}},</p> + + <p> </p> + We regret to inform you that {{reviewerName}} has declined to review the manuscript titled "{{ title }}" by {{authorName}}</p> + <p> </p> + <p>Please visit the <a href="{{manuscriptUrl}}">manuscript details page</a> to see if you need to invite any additional reviewers in order to reach a decision on the manuscript.</p> + <p> </p> + <p>Best regards, + <br /> {{eicName}} + <br /> Hindawi + </p> + <p> </p> + </div> + + </td> + </tr> + </table> + <div data-role="module-unsubscribe" class="module unsubscribe-css__unsubscribe___2CDlR" role="module" data-type="unsubscribe" + style="color:#444444;font-size:12px;line-height:20px;padding:16px 16px 16px 16px;text-align:center"> + <div class="Unsubscribe--addressLine"> + <p class="Unsubscribe--senderName" style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">Hindawi Publishing Corporation</p> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <span class="Unsubscribe--senderAddress">315 Madison Ave, Third Floor, Suite 3070</span>, + <span class="Unsubscribe--senderCity">NEW YORK</span>, + <span class="Unsubscribe--senderState">NY</span> + <span class="Unsubscribe--senderZip">10017</span> + </p> + </div> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a> + </p> + </div> + </td> + </tr> + </table> + <!--[if mso]> + </td></tr></table> + </center> + <![endif]--> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </div> + </center> +</body> + +</html> \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/reviewer-declined.txt b/packages/component-mail-service/src/templates/reviewer-declined.txt new file mode 100644 index 0000000000000000000000000000000000000000..a3925e886397805fe1c7371ea67f591d9b03afa0 --- /dev/null +++ b/packages/component-mail-service/src/templates/reviewer-declined.txt @@ -0,0 +1,6 @@ +Dear {{ editorName }} +We regret to inform you that {{ reviewerName }} has declined to review the manuscript titled "{{ title }}" by {{authorName}}. +Please visit the manuscript details page to see if you need to invite any additional reviewers in order to reach a decision on the manuscript. +Best regards, +{{eicName}} +Hindawi