diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 1493dcaf07f232dc17d2ec03e4ea9e9090719b44..3a774ca94dbc52c0786a550c3dc096001946b53e 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -270,17 +270,9 @@ export const getHERecommendation = (state, collectionId, fragmentId) => { ) } -const canMakeDecisionStatuses = [ - 'submitted', - 'pendingApproval', - 'underReview', - 'reviewCompleted', -] export const canMakeDecision = (state, collection = {}) => { - const status = get(collection, 'status', 'draft') - const isEIC = currentUserIs(state, 'adminEiC') - return isEIC && canMakeDecisionStatuses.includes(status) + return isEIC } const collectionReviewerReports = state => diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js index acd472f65bebb023a945a626abc54357b33555d7..27a9917dc42060ebe0e8784d19ad17d87ba56444 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js @@ -3,6 +3,7 @@ import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { required } from 'xpub-validators' import { Button, FilePicker, Menu, Spinner, ValidatedField } from '@pubsweet/ui' +import { initial } from 'lodash' import { Row, @@ -51,7 +52,9 @@ const ReviewerReportForm = ({ > <Label required>Recommendation</Label> <ValidatedField - component={input => <Menu {...input} options={recommendations} />} + component={input => ( + <Menu {...input} options={initial(recommendations)} /> + )} name="recommendation" validate={[required]} /> diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js index dc5314f613c215bb949c5735f25c395f83e8b5f6..07fcac0087453c6943265160174beb18b23f95f0 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js @@ -1,5 +1,5 @@ import React from 'react' -import { get, last } from 'lodash' +import { get, initial } from 'lodash' import { compose, withProps } from 'recompose' import styled from 'styled-components' import { reduxForm } from 'redux-form' @@ -39,11 +39,24 @@ const eicDecisions = [ modalTitle: 'Reject Manuscript', modalSubtitle: 'A rejection decision is final', }, + { + value: 'revision', + label: 'Request Revision', + modalTitle: 'Request Revision', + }, ] +const filterOptions = (eicDecisions, status) => { + if (status === 'submitted') return eicDecisions.slice(2) + if (status === 'pendingApproval') return initial(eicDecisions) + return eicDecisions.slice(2, 3) +} const ManuscriptEicDecision = ({ - isFetching, + status, + options, + decision, formValues, + isFetching, handleSubmit, messagesLabel, collection = {}, @@ -55,42 +68,33 @@ const ManuscriptEicDecision = ({ {...rest} > <Root> - <Row justify="flex-start"> + <Row justify="flex-start" pl={1} pt={1}> <ItemOverrideAlert flex={0} vertical> <Label required>Decision</Label> <ValidatedField - component={input => ( - <Menu - {...input} - options={ - get(collection, 'status', 'submitted') !== 'pendingApproval' - ? [last(eicDecisions)] - : eicDecisions - } - /> - )} + component={input => <Menu {...input} options={options} />} name="decision" validate={[required]} /> </ItemOverrideAlert> </Row> - {get(formValues, 'decision') !== 'publish' && ( - <Row mt={2}> + {decision !== 'publish' && ( + <Row mt={2} pl={1} pr={1}> <Item vertical> - <Label required> + <Label required={decision !== 'reject'}> {messagesLabel[get(formValues, 'decision', 'reject')]} </Label> <ValidatedField component={ValidatedTextArea} name="message" - validate={[required]} + validate={decision !== 'reject' ? [required] : undefined} /> </Item> </Row> )} - <Row justify="flex-end" mt={4}> + <Row justify="flex-end" mt={1} pr={1}> <Button onClick={handleSubmit} primary size="medium"> SUBMIT DECISION </Button> @@ -106,13 +110,18 @@ export default compose( modalKey: 'eic-decision', modalComponent: MultiAction, })), - withProps(({ formValues }) => ({ + withProps(({ formValues, collection }) => ({ modalTitle: eicDecisions.find( o => o.value === get(formValues, 'decision', 'publish'), ).modalTitle, modalSubtitle: eicDecisions.find( o => o.value === get(formValues, 'decision', 'publish'), ).modalSubtitle, + decision: get(formValues, 'decision'), + options: filterOptions( + eicDecisions, + get(collection, 'status', 'submitted'), + ), })), reduxForm({ form: 'eic-decision', diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js index a964c2c498c452599e68dd3457a24eac4c0a3b49..a6082362b1083a2123f8013aea6451fdb4b23023 100644 --- a/packages/component-fixture-manager/src/fixtures/collectionIDs.js +++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js @@ -4,9 +4,13 @@ const chance = new Chance() module.exports = { standardCollID: chance.guid(), - collectionReviewCompletedID: chance.guid(), collectionNoInvitesID: chance.guid(), twoVersionsCollectionId: chance.guid(), + minorRevisionCollectionID: chance.guid(), + majorRevisionCollectionID: chance.guid(), + collectionReviewCompletedID: chance.guid(), oneReviewedFragmentCollectionID: chance.guid(), noEditorRecomedationCollectionID: chance.guid(), + minorRevisionWithoutReviewCollectionID: chance.guid(), + majorRevisionWithoutReviewCollectionID: chance.guid(), } diff --git a/packages/component-fixture-manager/src/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js index 0fbff658ca84fc9266a31674461679e769a1ff13..4d92999127af2422edb3c3edd927ec744f25ea92 100644 --- a/packages/component-fixture-manager/src/fixtures/collections.js +++ b/packages/component-fixture-manager/src/fixtures/collections.js @@ -1,24 +1,35 @@ const Chance = require('chance') + const { user, handlingEditor, answerHE, noRecommendationHE, } = require('./userData') + const { fragment, fragment1, - reviewCompletedFragment, noInvitesFragment, + noInvitesFragment1, + minorRevisionWithReview, + majorRevisionWithReview, + reviewCompletedFragment, + minorRevisionWithoutReview, noEditorRecomedationFragment, } = require('./fragments') + const { standardCollID, - collectionReviewCompletedID, collectionNoInvitesID, twoVersionsCollectionId, + minorRevisionCollectionID, + majorRevisionCollectionID, + collectionReviewCompletedID, oneReviewedFragmentCollectionID, noEditorRecomedationCollectionID, + minorRevisionWithoutReviewCollectionID, + majorRevisionWithoutReviewCollectionID, } = require('./collectionIDs') const chance = new Chance() @@ -302,6 +313,152 @@ const collections = { status: 'revisionRequested', customId: chance.natural({ min: 999999, max: 9999999 }), }, + minorRevisionCollection: { + id: minorRevisionCollectionID, + delete: jest.fn(), + title: chance.sentence(), + type: 'collection', + fragments: [minorRevisionWithReview.id, noInvitesFragment1.id], + owners: [user.id], + save: jest.fn(() => collections.minorRevisionCollection), + getFragments: jest.fn(() => [minorRevisionWithReview, noInvitesFragment1]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: true, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: true, + isAccepted: true, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + technicalChecks: { + token: chance.guid(), + }, + status: 'reviewCompleted', + customId: chance.natural({ min: 999999, max: 9999999 }), + }, + minorRevisionWithoutReviewCollection: { + id: minorRevisionWithoutReviewCollectionID, + delete: jest.fn(), + title: chance.sentence(), + type: 'collection', + fragments: [minorRevisionWithoutReview.id, noInvitesFragment1.id], + owners: [user.id], + save: jest.fn(() => collections.minorRevisionWithoutReviewCollection), + getFragments: jest.fn(() => [ + minorRevisionWithoutReview, + noInvitesFragment1, + ]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: true, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: true, + isAccepted: true, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + technicalChecks: { + token: chance.guid(), + }, + status: 'reviewCompleted', + customId: chance.natural({ min: 999999, max: 9999999 }), + }, + majorRevisionCollection: { + id: majorRevisionCollectionID, + delete: jest.fn(), + title: chance.sentence(), + type: 'collection', + fragments: [majorRevisionWithReview.id, reviewCompletedFragment.id], + owners: [user.id], + save: jest.fn(() => collections.minorRevisionCollection), + getFragments: jest.fn(() => [ + majorRevisionWithReview, + reviewCompletedFragment, + ]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: true, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: true, + isAccepted: true, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + technicalChecks: { + token: chance.guid(), + }, + status: 'reviewCompleted', + customId: chance.natural({ min: 999999, max: 9999999 }), + }, + majorRevisionWithoutReviewCollection: { + id: majorRevisionWithoutReviewCollectionID, + delete: jest.fn(), + title: chance.sentence(), + type: 'collection', + fragments: [majorRevisionWithReview.id, noInvitesFragment1.id], + owners: [user.id], + save: jest.fn(() => collections.majorRevisionWithoutReviewCollection), + getFragments: jest.fn(() => [majorRevisionWithReview, noInvitesFragment1]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: true, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: true, + isAccepted: true, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + technicalChecks: { + token: chance.guid(), + }, + status: 'reviewCompleted', + customId: chance.natural({ min: 999999, max: 9999999 }), + }, } module.exports = collections diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index 337c0d943c7e6fd87e47fd3e31ff3458c0fb567d..475a277b97116a14d17d41e0c92bbdb6daba8c75 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-fixture-manager/src/fixtures/teamIDs.js b/packages/component-fixture-manager/src/fixtures/teamIDs.js index 162cdb391585ed6789018579dbb1469271f8d2f2..1de0429354609073c2809e77c3f470ccc8094562 100644 --- a/packages/component-fixture-manager/src/fixtures/teamIDs.js +++ b/packages/component-fixture-manager/src/fixtures/teamIDs.js @@ -5,9 +5,10 @@ const chance = new Chance() module.exports = { heTeamID: chance.guid(), revTeamID: chance.guid(), + rev1TeamID: chance.guid(), authorTeamID: chance.guid(), + majorRevisionHeTeamID: chance.guid(), revRecommendationTeamID: chance.guid(), - rev1TeamID: chance.guid(), heNoRecommendationTeamID: chance.guid(), revNoEditorRecommendationTeamID: chance.guid(), } diff --git a/packages/component-fixture-manager/src/fixtures/teams.js b/packages/component-fixture-manager/src/fixtures/teams.js index dbd51a4aca783c1315e45fe3749b7c2bd1f8145c..79b650069ccefcad84384488bc42b4294186fde9 100644 --- a/packages/component-fixture-manager/src/fixtures/teams.js +++ b/packages/component-fixture-manager/src/fixtures/teams.js @@ -5,21 +5,28 @@ const fragments = require('./fragments') const { heTeamID, revTeamID, + rev1TeamID, authorTeamID, + majorRevisionHeTeamID, revRecommendationTeamID, - rev1TeamID, heNoRecommendationTeamID, revNoEditorRecommendationTeamID, } = require('./teamIDs') const { submittingAuthor } = require('./userData') -const { collection, noEditorRecomedationCollection } = collections +const { + collection, + majorRevisionCollection, + noEditorRecomedationCollection, +} = collections + const { fragment, - reviewCompletedFragment, fragment1, + reviewCompletedFragment, noEditorRecomedationFragment, } = fragments + const { handlingEditor, reviewer, @@ -148,5 +155,22 @@ const teams = { updateProperties: jest.fn(() => teams.revNoEditorRecommendationTeam), id: revNoEditorRecommendationTeamID, }, + majorRevisionHeTeam: { + teamType: { + name: 'handlingEditor', + permissions: 'handlingEditor', + }, + group: 'handlingEditor', + name: 'HandlingEditor', + object: { + type: 'collection', + id: majorRevisionCollection.id, + }, + members: [handlingEditor.id], + save: jest.fn(() => teams.majorRevisionHeTeam), + delete: jest.fn(), + updateProperties: jest.fn(() => teams.majorRevisionHeTeam), + id: majorRevisionHeTeamID, + }, } module.exports = teams diff --git a/packages/component-fixture-manager/src/fixtures/users.js b/packages/component-fixture-manager/src/fixtures/users.js index 0ec653aab24e414415d7a11904367c2fdf94ede3..2b03c59776ada663e00e2a9ac58f8c73a4060db7 100644 --- a/packages/component-fixture-manager/src/fixtures/users.js +++ b/packages/component-fixture-manager/src/fixtures/users.js @@ -5,9 +5,10 @@ const chance = new Chance() const { heTeamID, revTeamID, + rev1TeamID, authorTeamID, + majorRevisionHeTeamID, revRecommendationTeamID, - rev1TeamID, heNoRecommendationTeamID, revNoEditorRecommendationTeamID, } = require('./teamIDs') @@ -24,7 +25,7 @@ users = keys.reduce((obj, item) => { teams = [heTeamID] break case 'handlingEditor': - teams = [heTeamID] + teams = [heTeamID, majorRevisionHeTeamID] break case 'noRecommendationHE': teams = [heNoRecommendationTeamID] diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index b0789f05aa93601db62053ce8f5f18382b944973..e95d5e8666a94464384fac61f693d199fe3c154b 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -1,4 +1,17 @@ -const { findLast, isEmpty, maxBy, get, flatMap } = require('lodash') +const config = require('config') +const { v4 } = require('uuid') +const logger = require('@pubsweet/logger') + +const { + findLast, + isEmpty, + maxBy, + get, + flatMap, + last, + has, + set, +} = require('lodash') const Fragment = require('./Fragment') @@ -33,25 +46,6 @@ class Collection { return this.updateStatus({ newStatus }) } - async updateFinalStatusByRecommendation({ recommendation }) { - let newStatus - switch (recommendation) { - case 'reject': - newStatus = 'rejected' - break - case 'publish': - newStatus = 'accepted' - break - case 'return-to-handling-editor': - newStatus = 'reviewCompleted' - break - default: - break - } - - await this.updateStatus({ newStatus }) - } - async updateStatus({ newStatus }) { this.collection.status = newStatus await this.collection.save() @@ -89,21 +83,6 @@ class Collection { await this.updateStatus({ newStatus: 'heAssigned' }) } - async updateStatusOnRecommendation({ isEditorInChief, recommendation }) { - if (isEditorInChief) { - if (recommendation === 'return-to-handling-editor') { - return this.updateStatus({ newStatus: 'reviewCompleted' }) - } - return this.updateFinalStatusByRecommendation({ - recommendation, - }) - } - return this.updateStatusByRecommendation({ - recommendation, - isHandlingEditor: true, - }) - } - getHELastName() { const [firstName, lastName] = this.collection.handlingEditor.name.split(' ') return lastName || firstName @@ -114,7 +93,8 @@ class Collection { const allCollectionInvitations = flatMap( allCollectionFragments, fragment => fragment.invitations, - ) + ).filter(Boolean) + const allNumberedInvitationsForUser = allCollectionInvitations .filter(invite => invite.userId === userId) .filter(invite => invite.reviewerNumber) @@ -155,7 +135,9 @@ class Collection { if (lastEditorRecommendation.recommendation === 'minor') { return this.hasAtLeastOneReviewReport(fragments) - } else if (lastEditorRecommendation.recommendation === 'major') { + } else if ( + ['major', 'revision'].includes(lastEditorRecommendation.recommendation) + ) { return fragmentHelper.hasReviewReport() } @@ -169,6 +151,92 @@ class Collection { ), ) } + + isLatestVersion(fragmentId) { + return last(this.collection.fragments) === fragmentId + } + + hasEQA() { + const technicalChecks = get(this.collection, 'technicalChecks', {}) + return has(technicalChecks, 'eqa') + } + + async setTechnicalChecks() { + set(this.collection, 'technicalChecks.token', v4()) + set(this.collection, 'technicalChecks.eqa', false) + await this.collection.save() + } + + async sendToMTS({ FragmentModel, UserModel, fragmentHelper }) { + await Promise.all( + this.collection.fragments.map(async fragmentId => { + const fragment = await FragmentModel.find(fragmentId) + + let fragmentUsers = [] + try { + fragmentUsers = await fragmentHelper.getReviewersAndEditorsData({ + collection: this.collection, + UserModel, + }) + + await sendMTSPackage({ + collection: this.collection, + fragment, + isEQA: true, + fragmentUsers, + }) + } catch (e) { + logger.error(e) + } + }), + ).catch(e => { + throw new Error('Something went wrong.') + }) + } + + async removeTechnicalChecks() { + this.collection.technicalChecks = {} + await this.collection.save() + } + + hasHandlingEditor() { + return has(this.collection, 'handlingEditor') + } + + async addFragment(newFragmentId) { + this.collection.fragments.push(newFragmentId) + await this.collection.save() + } +} + +const sendMTSPackage = async ({ + fragment, + collection, + isEQA = false, + fragmentUsers = [], +}) => { + const s3Config = get(config, 'pubsweet-component-aws-s3', {}) + const mtsConfig = get(config, 'mts-service', {}) + const { sendPackage } = require('pubsweet-component-mts-package') + + const { journal, xmlParser, ftp } = mtsConfig + const packageFragment = { + ...fragment, + metadata: { + ...fragment.metadata, + customId: collection.customId, + }, + } + + await sendPackage({ + isEQA, + s3Config, + fragmentUsers, + ftpConfig: ftp, + config: journal, + options: xmlParser, + fragment: packageFragment, + }) } module.exports = Collection diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index 7a0f27ae03d656269c6e5c757c094dd083f1ddb8..8cdd85f609bc51bbf045a3fc3007746b7bf09470 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -1,4 +1,5 @@ -const { get, remove, findLast, last } = require('lodash') +const { get, remove, findLast, pick, chain, omit } = require('lodash') + const config = require('config') const User = require('./User') @@ -123,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 }) { @@ -150,16 +150,18 @@ class Fragment { ) } - canHEMakeAnotherRecommendation(currentUserRecommendations) { - const lastHERecommendation = last(currentUserRecommendations) + canHEMakeAnotherRecommendation(lastHERecommendation) { const { fragment: { recommendations = [] } } = this + const returnToHERecommendation = findLast( recommendations, r => r.recommendation === 'return-to-handling-editor', ) + if (!returnToHERecommendation) return false return returnToHERecommendation.createdOn > lastHERecommendation.createdOn } + async getReviewersAndEditorsData({ collection, UserModel }) { const { invitations = [], @@ -220,6 +222,71 @@ class Fragment { return revAndEditorData } + + async addRecommendation(newRecommendation) { + this.fragment.recommendations = this.fragment.recommendations || [] + + this.fragment.recommendations.push(newRecommendation) + await this.fragment.save() + } + + async addRevision() { + this.fragment.revision = pick(this.fragment, [ + 'authors', + 'files', + 'metadata', + ]) + await this.fragment.save() + } + + hasReviewers() { + const { fragment: invitations = [] } = this + return invitations.length > 0 + } + + getLatestUserRecommendation(userId) { + return findLast(this.fragment.recommendations, r => r.userId === userId) + } + + getLatestRecommendation() { + return chain(this.fragment) + .get('recommendations', []) + .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-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js index dbd0eb1f2bf7b6e5995a27df258efbb6a532fb3d..1878092704ad027560176e161e3a526367ea3363 100644 --- a/packages/component-helper-service/src/services/User.js +++ b/packages/component-helper-service/src/services/User.js @@ -67,7 +67,7 @@ class User { async updateUserTeams({ userId, teamId }) { const user = await this.UserModel.find(userId) user.teams.push(teamId) - user.save() + await user.save() } async getActiveAuthors({ fragmentAuthors }) { diff --git a/packages/component-helper-service/src/tests/fragment.test.js b/packages/component-helper-service/src/tests/fragment.test.js index c5623dc18a8d7cf4812d3695189319f48b7e2202..ec7fbb6073f311f844c2ff574c936dfe00b9ff35 100644 --- a/packages/component-helper-service/src/tests/fragment.test.js +++ b/packages/component-helper-service/src/tests/fragment.test.js @@ -329,12 +329,13 @@ describe('Fragment helper', () => { updatedOn: chance.timestamp(), }, ] - const currentUserRecommendations = testFragment.recommendations.filter( - r => r.userId === handlingEditorId, - ) const fragmentHelper = new Fragment({ fragment: testFragment }) + const latestUserRecommendation = fragmentHelper.getLatestUserRecommendation( + handlingEditorId, + ) + const canHEMakeAnotherRecommendation = await fragmentHelper.canHEMakeAnotherRecommendation( - currentUserRecommendations, + latestUserRecommendation, ) expect(canHEMakeAnotherRecommendation).toBe(true) }) @@ -362,12 +363,12 @@ describe('Fragment helper', () => { updatedOn: chance.timestamp(), }, ] - const currentUserRecommendations = testFragment.recommendations.filter( - r => r.userId === handlingEditorId, - ) const fragmentHelper = new Fragment({ fragment: testFragment }) + const latestUserRecommendation = fragmentHelper.getLatestUserRecommendation( + handlingEditorId, + ) const canHEMakeAnotherRecommendation = await fragmentHelper.canHEMakeAnotherRecommendation( - currentUserRecommendations, + latestUserRecommendation, ) expect(canHEMakeAnotherRecommendation).toBe(false) }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 7d404e0cec4a91d3a74808722332a7266963aa78..8e354cf84a9b9b912b10980435ea169741ff994e 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -50,7 +50,6 @@ module.exports = models => async (req, res) => { error: 'Unauthorized.', }) - // check collection status if (!['submitted', 'heInvited'].includes(collection.status)) { return res.status(400).json({ error: `Cannot invite HE while collection is in the status: ${ diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js index de48dc4f84033d9090d303bd4ce1563c1298a992..94dc618a3165e9535d1cb6fce0d4fa92e23eb354 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js @@ -192,7 +192,36 @@ describe('Post collections invitations route handler', () => { }, }) - // expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + + expect(data.error).toEqual( + `Cannot invite HE while collection is in the status: ${ + collection.status + }.`, + ) + }) + it('should return an error when the collection is in the revision requested status', async () => { + const { user, editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + collection.status = 'revisionRequested' + + body = { + email: user.email, + role: 'handlingEditor', + } + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + }, + }) + + expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual( diff --git a/packages/component-manuscript-manager/src/notifications/emailCopy.js b/packages/component-manuscript-manager/src/notifications/emailCopy.js index 3aa154fcdd356c251d191cf3d38b02778e1379cf..67bfe04a10871271dec8db264da90e0b619fb7b0 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,29 @@ 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 + case 'eic-revision-published': + 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 'author-request-to-revision-from-eic': + paragraph = `In order for ${titleText} to proceed to the review process, there needs to be a revision. <br/><br/> + ${comments}<br/><br/> + For more information about what is required, please click the link below.<br/><br/>` + 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 506e9ba191a51fd7a1e34142dac3f5a03895de23..c80f5c29a2efa52a545033d57ca6929d5365ccd5 100644 --- a/packages/component-manuscript-manager/src/notifications/notification.js +++ b/packages/component-manuscript-manager/src/notifications/notification.js @@ -407,6 +407,53 @@ class Notification { return email.sendEmail() } + async notifySAWhenEiCRequestsRevision() { + const { + eicName, + submittingAuthor, + titleText, + } = await this._getNotificationProperties() + + const authorNoteText = helpers.getPrivateNoteTextForAuthor({ + newRecommendation: this.newRecommendation, + }) + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'author-request-to-revision-from-eic', + titleText, + comments: authorNoteText, + }) + + const email = new Email({ + type: 'user', + toUser: { + email: submittingAuthor.email, + name: submittingAuthor.lastName, + }, + fromEmail: `${eicName} <${staffEmail}>`, + content: { + subject: `${this.collection.customId}: Revision requested`, + paragraph, + signatureName: eicName, + ctaText: 'MANUSCRIPT DETAILS', + signatureJournal: journalName, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: submittingAuthor.id, + token: submittingAuthor.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + bodyProps, + }) + + return email.sendEmail() + } + async notifyReviewersWhenHEMakesRecommendation() { const { eicName, @@ -629,6 +676,144 @@ class Notification { }) } + async notifyEditorInChiefWhenAuthorSubmitsRevision(newFragment) { + const { titleText } = await this._getNotificationProperties() + + const userHelper = new User({ UserModel: this.UserModel }) + const editors = await userHelper.getEditorsInChief() + + const { paragraph, ...bodyProps } = getEmailCopy({ + titleText, + emailType: 'eic-revision-published', + }) + + editors.forEach(eic => { + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: eic.email, + }, + content: { + subject: `${this.collection.customId}: Revision submitted`, + paragraph, + signatureName: '', + signatureJournal: journalName, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + newFragment.id + }/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: eic.id, + token: eic.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + }) + } + + 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 ecfadb0a4ae20fb38e8e9511b10120fb33fabfaf..e6ab3f502678fcf09ecd40557b7d9b63607f9fea 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 c901c343b92e974cc532becf8b6c881411212e89..a1c7422bc45bbe2440213d04d4d6b06524b58896 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 6a3e11ef06411ccd3868b8da6aca8389289f0d6a..4f975c391c34232517734c2c3e920ad4bfefe7ca 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js @@ -1,14 +1,16 @@ -const { union, omit } = require('lodash') - const { Team, + User, services, Fragment, Collection, 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 +44,35 @@ 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', - }) + const userHelper = new User({ UserModel: models.User }) - 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, + userHelper, + 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 0000000000000000000000000000000000000000..eb99d655a56a86d441b9746b51aaac4af9810501 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/strategies/eicRequestRevision.js @@ -0,0 +1,48 @@ +module.exports = { + execute: async ({ + models, + TeamHelper, + fragmentHelper, + collectionHelper, + notification, + userHelper, + }) => { + 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() + + const teamHelper = new TeamHelper({ + TeamModel: models.Team, + fragmentId: newFragment.id, + }) + + const authorIds = newFragment.authors.map(auth => auth.id) + + const { id: teamId } = await teamHelper.createTeam({ + role: 'author', + members: authorIds, + objectType: 'fragment', + }) + authorIds.forEach(id => { + userHelper.updateUserTeams({ userId: id, teamId }) + }) + + await collectionHelper.updateStatus({ newStatus: 'submitted' }) + + newFragment.submitted = Date.now() + newFragment = await newFragment.save() + + await collectionHelper.addFragment(newFragment.id) + + await notification.notifyEditorInChiefWhenAuthorSubmitsRevision(newFragment) + + 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 0000000000000000000000000000000000000000..2a20da7ba12d003e43ddd97e7e0e814bc1ed225e --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/strategies/heRequestRevision.js @@ -0,0 +1,75 @@ +module.exports = { + execute: async ({ + models, + userHelper, + 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 => auth.id) + + const { id: teamId } = await teamHelper.createTeam({ + role: 'author', + members: authorIds, + objectType: 'fragment', + }) + authorIds.forEach(id => { + userHelper.updateUserTeams({ userId: id, teamId }) + }) + + 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/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index b47234c1f1a017149269ba6e415ee49d5af6b159..abfd90990a00b6eef6bcea5a764b4e3f028d03b3 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -1,8 +1,5 @@ -const uuid = require('uuid') -const { pick, get, set, has, isEmpty, last, chain } = require('lodash') const config = require('config') const { v4 } = require('uuid') -const logger = require('@pubsweet/logger') const { services, @@ -11,21 +8,31 @@ const { authsome: authsomeHelper, } = require('pubsweet-component-helper-service') -const { features = {}, recommendations } = config +const { recommendations } = config +const rejectAsHE = require('./strategies/heReject') +const publishAsHE = require('./strategies/hePublish') +const rejectAsEiC = require('./strategies/eicReject') +const publishAsEiC = require('./strategies/eicPublish') +const returnToHE = require('./strategies/eicReturnToHE') const Notification = require('../../notifications/notification') +const createReview = require('./strategies/reviewerCreateReview') +const requestRevisionAsHE = require('./strategies/heRequestRevision') +const requestRevisionAsEiC = require('./strategies/eicRequestRevision') module.exports = models => async (req, res) => { - const { recommendation, comments, recommendationType } = req.body - if (!services.checkForUndefinedParams(recommendationType)) + const { recommendation, comments = [], recommendationType } = req.body + if (!services.checkForUndefinedParams(recommendationType, recommendation)) return res.status(400).json({ error: 'Recommendation type is required.' }) const reqUser = await models.User.find(req.user) + const userId = reqUser.id + const isEditorInChief = reqUser.editorInChief || reqUser.admin const { collectionId, fragmentId } = req.params - let collection, fragment, fragments + let collection, fragment try { collection = await models.Collection.find(collectionId) @@ -43,28 +50,6 @@ module.exports = models => async (req, res) => { const collectionHelper = new Collection({ collection }) - try { - fragments = await collectionHelper.getAllFragments({ - FragmentModel: models.Fragment, - }) - } catch (e) { - const notFoundError = await services.handleNotFoundError(e, 'Item') - fragments = [] - return res.status(notFoundError.status).json({ - error: notFoundError.message, - }) - } - const currentUserRecommendations = get( - fragment, - 'recommendations', - [], - ).filter(r => r.userId === req.user) - - const lastFragmentRecommendation = chain(fragment) - .get('recommendations', []) - .last() - .value() - const authsome = authsomeHelper.getAuthsome(models) const target = { fragment, @@ -77,219 +62,83 @@ module.exports = models => async (req, res) => { }) const fragmentHelper = new Fragment({ fragment }) - if ( - recommendationType === recommendations.type.editor && - last(collection.fragments) !== fragmentId - ) { - return res - .status(400) - .json({ error: 'Cannot make a recommendation on an older version.' }) - } - - if ( - recommendationType === recommendations.type.review && - last(collection.fragments) !== fragmentId - ) { - return res - .status(400) - .json({ error: 'Cannot write a review on an older version.' }) - } - if ( - last(collection.fragments) === fragmentId && - !isEmpty(currentUserRecommendations) - ) { - if (recommendationType === recommendations.type.review) { - return res - .status(400) - .json({ error: 'Cannot write another review on this version.' }) - } - if ( - recommendationType === recommendations.type.editor && - !isEditorInChief && - !fragmentHelper.canHEMakeAnotherRecommendation(currentUserRecommendations) - ) { - return res.status(400).json({ - error: 'Cannot make another recommendation on this version.', - }) - } - if ( - recommendationType === recommendations.type.editor && - isEditorInChief && - recommendation !== recommendations.reject && - lastFragmentRecommendation.recommendation === 'return-to-handling-editor' - ) { - return res.status(400).json({ - error: 'Cannot make another recommendation on this version.', - }) - } - } - if ( - recommendation === recommendations.publish && - recommendationType === recommendations.type.editor && - collection.handlingEditor && - collection.handlingEditor.id === req.user - ) { - if (!collectionHelper.canHEMakeRecommendation(fragments, fragmentHelper)) { - return res.status(400).json({ - error: 'Cannot publish without at least one reviewer report.', - }) - } + if (!collectionHelper.isLatestVersion(fragmentId)) { + const error = + recommendationType === recommendations.type.editor + ? 'Cannot make a recommendation on an older version.' + : 'Cannot write a review on an older version.' + return res.status(400).json({ error }) } - fragment.recommendations = fragment.recommendations || [] const newRecommendation = { - id: uuid.v4(), - userId: reqUser.id, + userId, + id: v4(), + comments, + recommendation, + recommendationType, createdOn: Date.now(), updatedOn: Date.now(), - recommendationType, } - newRecommendation.recommendation = recommendation || undefined - newRecommendation.comments = comments || undefined - - if (recommendationType === 'editorRecommendation') { - await collectionHelper.updateStatusOnRecommendation({ - isEditorInChief, - recommendation, - }) - - if (!isEditorInChief && ['minor', 'major'].includes(recommendation)) { - fragment.revision = pick(fragment, ['authors', 'files', 'metadata']) - } - - const technicalChecks = get(collection, 'technicalChecks', {}) - const hasEQA = has(technicalChecks, 'eqa') - // the manuscript has not yet passed through the EQA process so we need to upload it to the FTP server - if (isEditorInChief && recommendation === 'publish' && !hasEQA) { - if (features.mts) { - await Promise.all( - collection.fragments.map(async fragmentId => { - const fragment = await models.Fragment.find(fragmentId) - const fragmentHelper = new Fragment({ fragment }) + const notification = new Notification({ + fragment, + collection, + newRecommendation, + UserModel: models.User, + baseUrl: services.getBaseUrl(req), + }) - let fragmentUsers = [] - try { - fragmentUsers = await fragmentHelper.getReviewersAndEditorsData({ - collection, - UserModel: models.User, - }) + const strategies = { + he: { + reject: rejectAsHE, + publish: publishAsHE, + major: requestRevisionAsHE, + minor: requestRevisionAsHE, + }, + eic: { + reject: rejectAsEiC, + publish: publishAsEiC, + revision: requestRevisionAsEiC, + 'return-to-handling-editor': returnToHE, + }, + } - await sendMTSPackage({ - collection, - fragment, - isEQA: true, - fragmentUsers, - }) - } catch (e) { - logger.error(e) - } - }), - ).catch(e => - res.status(500).json({ - error: 'Something went wrong.', - }), - ) + let role = '' + switch (recommendationType) { + case 'review': + role = 'reviewer' + try { + await createReview.execute({ + userId, + fragmentHelper, + newRecommendation, + }) + } catch (e) { + return res.status(400).json({ error: e.message }) } + return res.status(200).json(newRecommendation) + case 'editorRecommendation': + role = isEditorInChief ? 'eic' : 'he' + break + default: + return res.status(400).json({ + error: `Recommendation ${recommendation} is not defined.`, + }) + } - collection.status = 'inQA' - set(collection, 'technicalChecks.token', v4()) - set(collection, 'technicalChecks.eqa', false) - await collection.save() - } - - /* if the EiC returns the manuscript to the HE after the EQA has been performed - then remove all properties from the technicalChecks property so that the manuscript - can go through the EQA process again - */ - if ( - isEditorInChief && - recommendation === 'return-to-handling-editor' && - hasEQA - ) { - collection.technicalChecks = {} - await collection.save() - } - - const notification = new Notification({ - fragment, - collection, + try { + await strategies[role][recommendation].execute({ + userId, + models, + notification, + fragmentHelper, + collectionHelper, newRecommendation, - UserModel: models.User, - baseUrl: services.getBaseUrl(req), }) - - const hasPeerReview = !isEmpty(collection.handlingEditor) - - if (isEditorInChief) { - if (recommendation === 'publish' && collection.status === 'inQA') { - notification.notifyEAWhenEiCRequestsEQAApproval() - } - - if (recommendation === 'publish' && collection.status === 'accepted') { - notification.notifyEAWhenEiCMakesFinalDecision() - } - - if (hasPeerReview && (recommendation !== 'publish' || hasEQA)) { - if (recommendation === 'return-to-handling-editor') { - notification.notifyHEWhenEiCReturnsToHE() - } else { - notification.notifyHEWhenEiCMakesDecision() - notification.notifyReviewersWhenEiCMakesDecision() - } - } - - if ( - recommendation !== 'return-to-handling-editor' && - (recommendation !== 'publish' || hasEQA) - ) { - notification.notifyAuthorsWhenEiCMakesDecision() - } - } else { - if (collection.status === 'revisionRequested') { - notification.notifySAWhenHERequestsRevision() - } - - if (hasPeerReview) { - notification.notifyReviewersWhenHEMakesRecommendation() - notification.notifyEiCWhenHEMakesRecommendation() - } - } + } catch (e) { + return res.status(400).json({ error: e.message }) } - fragment.recommendations.push(newRecommendation) - fragment.save() - return res.status(200).json(newRecommendation) } - -const sendMTSPackage = async ({ - fragment, - collection, - isEQA = false, - fragmentUsers = [], -}) => { - const s3Config = get(config, 'pubsweet-component-aws-s3', {}) - const mtsConfig = get(config, 'mts-service', {}) - const { sendPackage } = require('pubsweet-component-mts-package') - - const { journal, xmlParser, ftp } = mtsConfig - const packageFragment = { - ...fragment, - metadata: { - ...fragment.metadata, - customId: collection.customId, - }, - } - - await sendPackage({ - isEQA, - s3Config, - fragmentUsers, - ftpConfig: ftp, - config: journal, - options: xmlParser, - fragment: packageFragment, - }) -} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicPublish.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicPublish.js new file mode 100644 index 0000000000000000000000000000000000000000..6915c54324dbaa41b6d1aea25c4634c333940d5a --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicPublish.js @@ -0,0 +1,45 @@ +const config = require('config') + +const { features = {} } = config + +module.exports = { + execute: async ({ + models, + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + const latestRecommendation = fragmentHelper.getLatestRecommendation() + if (latestRecommendation.recommendation === 'return-to-handling-editor') { + throw new Error( + 'Cannot make decision to publish after the manuscript has been returned to Handling Editor.', + ) + } + + await fragmentHelper.addRecommendation(newRecommendation) + + let newStatus = '' + if (collectionHelper.hasEQA()) { + newStatus = 'accepted' + notification.notifyEAWhenEiCMakesFinalDecision() + notification.notifyAuthorsWhenEiCMakesDecision() + notification.notifyHEWhenEiCMakesDecision() + notification.notifyReviewersWhenEiCMakesDecision() + } else { + if (features.mts) { + await collectionHelper.sendToMTS({ + fragmentHelper, + UserModel: models.User, + FragmentModel: models.Fragment, + }) + } + + newStatus = 'inQA' + await collectionHelper.setTechnicalChecks() + notification.notifyEAWhenEiCRequestsEQAApproval() + } + + await collectionHelper.updateStatus({ newStatus }) + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReject.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReject.js new file mode 100644 index 0000000000000000000000000000000000000000..b10ff0e2a46b93c6d56e85ab0d30f2e345d7f1dd --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReject.js @@ -0,0 +1,19 @@ +module.exports = { + execute: async ({ + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + await fragmentHelper.addRecommendation(newRecommendation) + await collectionHelper.updateStatus({ newStatus: 'rejected' }) + + notification.notifyAuthorsWhenEiCMakesDecision() + if (collectionHelper.hasHandlingEditor()) { + notification.notifyHEWhenEiCMakesDecision() + } + if (fragmentHelper.hasReviewers()) { + notification.notifyReviewersWhenEiCMakesDecision() + } + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicRequestRevision.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicRequestRevision.js new file mode 100644 index 0000000000000000000000000000000000000000..0f66d8af7163765974379ceee97f12b887cbae5b --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicRequestRevision.js @@ -0,0 +1,19 @@ +module.exports = { + execute: async ({ + fragmentHelper, + collectionHelper, + newRecommendation, + notification, + }) => { + if (collectionHelper.hasHandlingEditor()) { + throw new Error( + 'Cannot make request a revision after a Handling Editor has been assigned.', + ) + } + + await fragmentHelper.addRevision() + await collectionHelper.updateStatus({ newStatus: 'revisionRequested' }) + await fragmentHelper.addRecommendation(newRecommendation) + await notification.notifySAWhenEiCRequestsRevision() + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReturnToHE.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReturnToHE.js new file mode 100644 index 0000000000000000000000000000000000000000..633d9c1b8c61065ed65841f9c43c9c250cfa7424 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/eicReturnToHE.js @@ -0,0 +1,22 @@ +module.exports = { + execute: async ({ + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + const latestRecommendation = fragmentHelper.getLatestRecommendation() + if (latestRecommendation.recommendation === 'return-to-handling-editor') { + throw new Error('Cannot return to Handling Editor again.') + } + + if (collectionHelper.hasEQA()) { + await collectionHelper.removeTechnicalChecks() + } + await collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) + + await fragmentHelper.addRecommendation(newRecommendation) + + notification.notifyHEWhenEiCReturnsToHE() + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/hePublish.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/hePublish.js new file mode 100644 index 0000000000000000000000000000000000000000..238efbc141b40fda46e5c048afb3e9031a8213fb --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/hePublish.js @@ -0,0 +1,31 @@ +module.exports = { + execute: async ({ + userId, + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + const fragments = await collectionHelper.collection.getFragments() + + if (!collectionHelper.canHEMakeRecommendation(fragments, fragmentHelper)) { + throw new Error('Cannot publish without at least one reviewer report.') + } + + const latestUserRecommendation = fragmentHelper.getLatestUserRecommendation( + userId, + ) + if ( + latestUserRecommendation && + !fragmentHelper.canHEMakeAnotherRecommendation(latestUserRecommendation) + ) { + throw new Error('Cannot make another recommendation on this version.') + } + + await fragmentHelper.addRecommendation(newRecommendation) + await collectionHelper.updateStatus({ newStatus: 'pendingApproval' }) + + notification.notifyReviewersWhenHEMakesRecommendation() + notification.notifyEiCWhenHEMakesRecommendation() + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heReject.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heReject.js new file mode 100644 index 0000000000000000000000000000000000000000..cd76a8beba735d8ef668dec8c3baef86ebbcaa41 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heReject.js @@ -0,0 +1,27 @@ +module.exports = { + execute: async ({ + userId, + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + const latestUserRecommendation = fragmentHelper.getLatestUserRecommendation( + userId, + ) + if ( + latestUserRecommendation && + !fragmentHelper.canHEMakeAnotherRecommendation(latestUserRecommendation) + ) { + throw new Error('Cannot make another recommendation on this version.') + } + + await fragmentHelper.addRecommendation(newRecommendation) + await collectionHelper.updateStatus({ newStatus: 'pendingApproval' }) + + if (fragmentHelper.hasReviewers()) { + notification.notifyReviewersWhenHEMakesRecommendation() + } + notification.notifyEiCWhenHEMakesRecommendation() + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heRequestRevision.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heRequestRevision.js new file mode 100644 index 0000000000000000000000000000000000000000..7daeb25d9e400a02de63c8693d1c99a48e02e0c5 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/heRequestRevision.js @@ -0,0 +1,30 @@ +module.exports = { + execute: async ({ + userId, + notification, + fragmentHelper, + collectionHelper, + newRecommendation, + }) => { + const latestUserRecommendation = fragmentHelper.getLatestUserRecommendation( + userId, + ) + if ( + latestUserRecommendation && + !fragmentHelper.canHEMakeAnotherRecommendation(latestUserRecommendation) + ) { + throw new Error('Cannot make another recommendation on this version.') + } + + await fragmentHelper.addRevision() + await collectionHelper.updateStatus({ newStatus: 'revisionRequested' }) + await fragmentHelper.addRecommendation(newRecommendation) + + notification.notifySAWhenHERequestsRevision() + notification.notifyEiCWhenHEMakesRecommendation() + + if (fragmentHelper.hasReviewers()) { + notification.notifyReviewersWhenHEMakesRecommendation() + } + }, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/reviewerCreateReview.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/reviewerCreateReview.js new file mode 100644 index 0000000000000000000000000000000000000000..9b8f450e33a9597a1b25b59f964392cc2befe3b9 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/strategies/reviewerCreateReview.js @@ -0,0 +1,9 @@ +module.exports = { + execute: async ({ fragmentHelper, newRecommendation, userId }) => { + if (fragmentHelper.getLatestUserRecommendation(userId)) { + throw new Error('Cannot write another review on this version.') + } + + await fragmentHelper.addRecommendation(newRecommendation) + }, +} 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 b721805c99c9b00833fcb953cc16f5a529840164..7ad0c1284762c4173c9b7e948fb2d273233eb166 100644 --- a/packages/component-manuscript-manager/src/tests/collections/get.test.js +++ b/packages/component-manuscript-manager/src/tests/collections/get.test.js @@ -36,7 +36,7 @@ describe('Get collections route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data).toHaveLength(1) + expect(data).toHaveLength(handlingEditor.teams.length) expect(data[0].type).toEqual('collection') expect(data[0]).toHaveProperty('currentVersion') expect(data[0]).toHaveProperty('visibleStatus') @@ -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 be867cbe7df7674d91c619d7837a94ed08e808e6..b6cb60fb809b12c4b399ef3b38f912ecd7c99974 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') + }) }) diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index 5638eb69ff3be3a1a164b0226377f6c38d7f0486..409e5238ef0062715a53031633e33413f27dcbf5 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -63,6 +63,7 @@ describe('Post fragments recommendations route handler', () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments + body.recommendationType = 'review' const res = await requests.sendRequest({ body, @@ -102,7 +103,7 @@ describe('Post fragments recommendations route handler', () => { expect(data.userId).toEqual(noRecommendationHE.id) }) - it('should return an error when creating a recommendation with publish as a HE when there is a single version and there are no reviews.', async () => { + it('should return an error when recommending to publish as HE when there is a single version and there are no reviews.', async () => { const { handlingEditor } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments @@ -129,52 +130,45 @@ describe('Post fragments recommendations route handler', () => { }) it('should return success when creating a recommendation as a HE after minor revision and we have at least one review on collection.', async () => { - const { handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections + const { handlingEditor: { id: userId } } = testFixtures.users const { - minorRevisionWithReview, - noInvitesFragment1, - } = testFixtures.fragments + minorRevisionCollection: { id: collectionId }, + } = testFixtures.collections + const { noInvitesFragment1: { id: fragmentId } } = testFixtures.fragments - collection.fragments = [minorRevisionWithReview.id, noInvitesFragment1.id] const res = await requests.sendRequest({ body, - userId: handlingEditor.id, + userId, models, route, path, params: { - collectionId: collection.id, - fragmentId: noInvitesFragment1.id, + collectionId, + fragmentId, }, }) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.userId).toEqual(handlingEditor.id) + expect(data.userId).toEqual(userId) }) it('should return error when creating a recommendation as a HE after minor revision and there are no reviews.', async () => { - const { handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections + const { handlingEditor: { id: userId } } = testFixtures.users const { - minorRevisionWithoutReview, - noInvitesFragment1, - } = testFixtures.fragments - - collection.fragments = [ - minorRevisionWithoutReview.id, - noInvitesFragment1.id, - ] + minorRevisionWithoutReviewCollection: { id: collectionId }, + } = testFixtures.collections + const { noInvitesFragment1: { id: fragmentId } } = testFixtures.fragments + const res = await requests.sendRequest({ body, - userId: handlingEditor.id, + userId, models, route, path, params: { - collectionId: collection.id, - fragmentId: noInvitesFragment1.id, + collectionId, + fragmentId, }, }) @@ -186,53 +180,46 @@ describe('Post fragments recommendations route handler', () => { }) it('should return success when creating a recommendation as a HE after major revision and there are least one review on fragment.', async () => { - const { handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections + const { handlingEditor: { id: userId } } = testFixtures.users const { - majorRevisionWithReview, - reviewCompletedFragment, - } = testFixtures.fragments - - reviewCompletedFragment.collectionId = collection.id - collection.fragments = [ - majorRevisionWithReview.id, - reviewCompletedFragment.id, - ] + majorRevisionCollection: { id: collectionId }, + } = testFixtures.collections + const { reviewCompletedFragment } = testFixtures.fragments + + reviewCompletedFragment.collectionId = collectionId const res = await requests.sendRequest({ body, - userId: handlingEditor.id, + userId, models, route, path, params: { - collectionId: collection.id, + collectionId, fragmentId: reviewCompletedFragment.id, }, }) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.userId).toEqual(handlingEditor.id) + expect(data.userId).toEqual(userId) }) it('should return error when creating a recommendation as a HE after major revision there are no reviews on fragment.', async () => { - const { handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections + const { handlingEditor: { id: userId } } = testFixtures.users const { - majorRevisionWithReview, - noInvitesFragment1, - } = testFixtures.fragments + majorRevisionWithoutReviewCollection: { id: collectionId }, + } = testFixtures.collections + const { noInvitesFragment1: { id: fragmentId } } = testFixtures.fragments - collection.fragments = [majorRevisionWithReview.id, noInvitesFragment1.id] const res = await requests.sendRequest({ body, - userId: handlingEditor.id, + userId, models, route, path, params: { - collectionId: collection.id, - fragmentId: noInvitesFragment1.id, + collectionId, + fragmentId, }, }) @@ -445,9 +432,6 @@ describe('Post fragments recommendations route handler', () => { body.recommendationType = 'editorRecommendation' body.comments = 'This needs more work' - delete fragment.recommendations - delete fragment.revision - delete fragment.invitations collection.technicalChecks.eqa = false const res = await requests.sendRequest({ @@ -464,7 +448,6 @@ describe('Post fragments recommendations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(collection.status).toBe('reviewCompleted') expect(collection.technicalChecks).not.toHaveProperty('token') expect(collection.technicalChecks).not.toHaveProperty('eqa') @@ -643,4 +626,55 @@ describe('Post fragments recommendations route handler', () => { 'Cannot make a recommendation on an older version.', ) }) + it('should return success when an EiC requests a revision before the Handling Editor is assigned', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'revision' + body.recommendationType = 'editorRecommendation' + delete collection.handlingEditor + + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.userId).toEqual(editorInChief.id) + expect(collection.status).toEqual('revisionRequested') + expect(fragment).toHaveProperty('revision') + }) + it('should return an error when an EiC requests a revision after a Handling Editor is assigned', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'revision' + body.recommendationType = 'editorRecommendation' + + const res = await requests.sendRequest({ + body, + userId: editorInChief.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( + 'Cannot make request a revision after a Handling Editor has been assigned.', + ) + }) }) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 5f870ecfa43fe24b4886780c6a78feaf2f0d675b..868b408304a4537d0d39affe98fabaeb3e0c3ff5 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -24,6 +24,7 @@ import ReviewerReports from './ReviewerReports' const messagesLabel = { 'return-to-handling-editor': 'Comments for Handling Editor', reject: 'Comments for Author', + revision: 'Comments for Author', } const cannotViewReviewersDetails = ['revisionRequested', 'pendingApproval'] diff --git a/packages/component-mts-package/src/MTS.js b/packages/component-mts-package/src/MTS.js index 3f9e503df78bbf5d3273f0e7e19dd4d3c764b1e1..4b9cdadfcd4ce88fcebde24378b9b5ab6c527a7d 100644 --- a/packages/component-mts-package/src/MTS.js +++ b/packages/component-mts-package/src/MTS.js @@ -38,17 +38,21 @@ module.exports = { fragment, xmlFile, isEQA, - }).then(() => { - const packageName = get(xmlFile, 'name', '').replace('.xml', '') - const filename = isEQA - ? `ACCEPTED_${packageName}.${fragment.version}.zip` - : `${packageName}.zip` + }) + .then(() => { + const packageName = get(xmlFile, 'name', '').replace('.xml', '') + const filename = isEQA + ? `ACCEPTED_${packageName}.${fragment.version}.zip` + : `${packageName}.zip` - return PackageManager.uploadFiles({ - filename, - s3Config, - config: ftpConfig, + return PackageManager.uploadFiles({ + filename, + s3Config, + config: ftpConfig, + }) + }) + .catch(e => { + throw new Error(e) }) - }) }, } diff --git a/packages/xpub-faraday/app/config/journal/recommendations.js b/packages/xpub-faraday/app/config/journal/recommendations.js index 736effd199a5801309f3fe4e1a4f622c88cc33ad..99b50aa2d2eba35e06e7a127dd19a6631eed2b69 100644 --- a/packages/xpub-faraday/app/config/journal/recommendations.js +++ b/packages/xpub-faraday/app/config/journal/recommendations.js @@ -15,4 +15,8 @@ module.exports = [ value: 'reject', label: 'Reject', }, + { + value: 'revision', + label: 'Revision', + }, ] diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 6d1ac18d402b9baa6333ed22f7cfbe5d2f566cdf..c60e5f85015904e4218550b1d78ab84567d28d33 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -126,6 +126,7 @@ module.exports = { 'reject', 'publish', 'revise', + 'revision', 'major', 'minor', 'return-to-handling-editor', diff --git a/packages/xpub-faraday/tests/config/authsome-helpers.test.js b/packages/xpub-faraday/tests/config/authsome-helpers.test.js index 41fa760afb11a7bb0cb7896549aa19151e3ce79a..10571579af13e2834aa58b9ca6a4f7fe72971397 100644 --- a/packages/xpub-faraday/tests/config/authsome-helpers.test.js +++ b/packages/xpub-faraday/tests/config/authsome-helpers.test.js @@ -167,7 +167,7 @@ describe('Authsome Helpers', () => { user: answerReviewer, }) const { recommendations } = result - expect(recommendations).toHaveLength(7) + expect(recommendations).toHaveLength(8) }) it('reviewer should not see other reviewers recommendations on latest fragment', () => { @@ -181,7 +181,7 @@ describe('Authsome Helpers', () => { user: answerReviewer, }) const { recommendations } = result - expect(recommendations).toHaveLength(6) + expect(recommendations).toHaveLength(7) }) it('reviewer should not see any reviewer recommendation on previous version if he did not submit a review on that fragment', () => {