Skip to content
Snippets Groups Projects
Commit 6e67e9c2 authored by Sebastian Mihalache's avatar Sebastian Mihalache :hammer_pick:
Browse files

feat(recommendations): submit revision after eic request

parent 0bdef7e5
No related branches found
No related tags found
3 merge requests!196S25 - EiC submit revision,!189S25,!177Hin 230 eic request revision
Showing
with 358 additions and 211 deletions
......@@ -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: [
{
......
......@@ -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 ({
......
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
......@@ -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.`)
}
......
......@@ -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({
......
......@@ -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
......
......@@ -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()
......
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({
......
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
},
}
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
},
}
......@@ -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')
})
......
......@@ -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')
})
})
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment