diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js index 9f9e2aaa75309afd25084e94a65cbcf65e2c646b..e2197d20c406eeff827b6b4051f48062d7e7493e 100644 --- a/packages/component-faraday-ui/src/ReviewersTable.js +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -41,9 +41,9 @@ const ReviewersTable = ({ invitation, 'person.lastName', )}`}</Text> - {invitation.isAccepted && ( + {invitation.reviewerNumber && ( <Text customId ml={1}> - {renderAcceptedLabel(index)} + Reviewer {invitation.reviewerNumber} </Text> )} </td> @@ -102,12 +102,7 @@ export default compose( withProps(({ invitations = [] }) => ({ invitations: orderBy(invitations, orderInvitations), })), - withProps(({ invitations = [] }) => ({ - firstAccepted: invitations.findIndex(i => i.hasAnswer && i.isAccepted), - })), withHandlers({ - renderAcceptedLabel: ({ firstAccepted, invitations }) => index => - `Reviewer ${index - firstAccepted + 1}`, getInvitationStatus: () => ({ hasAnswer, isAccepted }) => { if (!hasAnswer) return 'PENDING' if (isAccepted) return 'ACCEPTED' diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index d8a41b821abe0639bbb6f9cfbe6ffe17b9f75189..d1a9cbcffcb1e50ee2f2eda2aab33b283e9a8557 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -13,6 +13,7 @@ import { ContextualBox, ReviewersTable, PublonsTable, + indexReviewers, ReviewerReport, InviteReviewers, ReviewerBreakdown, @@ -118,14 +119,14 @@ const ReviewerDetails = ({ {reports.length === 0 && ( <Text align="center">No reports submitted yet.</Text> )} - {reports.map((report, index) => ( + {reports.map(report => ( <ReviewerReport journal={journal} key={report.id} onDownload={downloadFile} onPreview={previewFile} report={report} - reviewerIndex={index + 1} + reviewerIndex={report.reviewerNumber} showOwner /> ))} @@ -154,7 +155,10 @@ export default compose( ...i, review: reviewerReports.find(r => r.userId === i.userId), })), - reports: reviewerReports.filter(r => r.submittedOn), + reports: indexReviewers( + reviewerReports.filter(r => r.submittedOn), + invitations, + ), }), ), withProps(({ currentUser }) => ({ diff --git a/packages/component-faraday-ui/src/helpers/utils.js b/packages/component-faraday-ui/src/helpers/utils.js index 898fd3ebc62282763a1331fd359f9d8da85a732c..8b798e34e7b73272c922ab6201019b5b4b221fb9 100644 --- a/packages/component-faraday-ui/src/helpers/utils.js +++ b/packages/component-faraday-ui/src/helpers/utils.js @@ -1,4 +1,4 @@ -import { get, chain } from 'lodash' +import { get, chain, find } from 'lodash' export const handleError = fn => e => { fn(get(JSON.parse(e.response), 'error', 'Oops! Something went wrong!')) @@ -10,3 +10,14 @@ export const getReportComments = ({ report, isPublic = false }) => .find(c => c.public === isPublic) .get('content') .value() + +export const indexReviewers = (reports = [], invitations = []) => { + reports.forEach(report => { + report.reviewerNumber = get( + find(invitations, ['userId', report.userId]), + 'reviewerNumber', + 0, + ) + }) + return reports +} diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js index e66bb71a232758a7de0746033ba2493745157250..a964c2c498c452599e68dd3457a24eac4c0a3b49 100644 --- a/packages/component-fixture-manager/src/fixtures/collectionIDs.js +++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js @@ -7,5 +7,6 @@ module.exports = { collectionReviewCompletedID: chance.guid(), collectionNoInvitesID: chance.guid(), twoVersionsCollectionId: chance.guid(), + oneReviewedFragmentCollectionID: chance.guid(), noEditorRecomedationCollectionID: chance.guid(), } diff --git a/packages/component-fixture-manager/src/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js index 7d1df81e4f6379f9bb4d21bc243a2ed97a9e9e2d..0fbff658ca84fc9266a31674461679e769a1ff13 100644 --- a/packages/component-fixture-manager/src/fixtures/collections.js +++ b/packages/component-fixture-manager/src/fixtures/collections.js @@ -17,6 +17,7 @@ const { collectionReviewCompletedID, collectionNoInvitesID, twoVersionsCollectionId, + oneReviewedFragmentCollectionID, noEditorRecomedationCollectionID, } = require('./collectionIDs') @@ -30,6 +31,7 @@ const collections = { fragments: [fragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment]), invitations: [ { id: chance.guid(), @@ -73,6 +75,7 @@ const collections = { fragments: [fragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment]), invitations: [ { id: chance.guid(), @@ -115,6 +118,7 @@ const collections = { fragments: [fragment1.id, noInvitesFragment.id], owners: [user.id], save: jest.fn(() => collections.collection2), + getFragments: jest.fn(() => [fragment1, noInvitesFragment]), invitations: [ { id: chance.guid(), @@ -159,6 +163,7 @@ const collections = { created: chance.timestamp(), customId: '0000001', fragments: [reviewCompletedFragment.id], + getFragments: jest.fn(() => [reviewCompletedFragment]), invitations: [ { id: chance.guid(), @@ -189,6 +194,7 @@ const collections = { fragments: [fragment.id, reviewCompletedFragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment, reviewCompletedFragment]), invitations: [ { id: chance.guid(), @@ -219,6 +225,7 @@ const collections = { fragments: [], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => []), customId: chance.natural({ min: 999999, max: 9999999 }), }, noEditorRecomedationCollection: { @@ -228,6 +235,7 @@ const collections = { fragments: [noEditorRecomedationFragment.id], owners: [user.id], save: jest.fn(() => collections.noEditorRecomedationCollection), + getFragments: jest.fn(() => [noEditorRecomedationFragment]), invitations: [ { id: chance.guid(), @@ -263,6 +271,37 @@ const collections = { }, status: 'reviewCompleted', }, + oneReviewedFragmentCollection: { + id: oneReviewedFragmentCollectionID, + title: chance.sentence(), + type: 'collection', + fragments: [reviewCompletedFragment.id, noInvitesFragment.id], + owners: [user.id], + save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [reviewCompletedFragment, noInvitesFragment]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: false, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: false, + isAccepted: false, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: null, + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + status: 'revisionRequested', + 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 e61b8b432acfb7dfda7254189f0fd7861135ae45..337c0d943c7e6fd87e47fd3e31ff3458c0fb567d 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -99,6 +99,69 @@ const fragments = { createdOn: chance.timestamp(), updatedOn: chance.timestamp(), }, + { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: handlingEditor.id, + createdOn: 1542361074012, + updatedOn: chance.timestamp(), + }, + { + recommendation: 'return-to-handling-editor', + 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: 1542361115749, + updatedOn: chance.timestamp(), + }, + { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: chance.bool(), + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: handlingEditor.id, + createdOn: 1542361115750, + updatedOn: chance.timestamp(), + }, { recommendation: 'publish', recommendationType: 'editorRecommendation', @@ -117,7 +180,7 @@ const fragments = { ], id: chance.guid(), userId: admin.id, - createdOn: chance.timestamp(), + createdOn: 1542361115751, updatedOn: chance.timestamp(), }, ], @@ -277,6 +340,7 @@ const fragments = { invitedOn: chance.timestamp(), isAccepted: true, respondedOn: chance.timestamp(), + reviewerNumber: 2, }, { id: chance.guid(), @@ -457,6 +521,48 @@ const fragments = { updatedOn: chance.timestamp(), submittedOn: chance.timestamp(), }, + { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: handlingEditor.id, + createdOn: 1542361074012, + updatedOn: chance.timestamp(), + }, + { + recommendation: 'return-to-handling-editor', + 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: 1542361115749, + updatedOn: chance.timestamp(), + }, ], authors: [ { @@ -490,17 +596,9 @@ fragments.noInvitesFragment = { invites: [], id: chance.guid(), } -fragments.noInvitesFragment = { - ...fragments.fragment1, - recommendations: [], - invites: [], - id: chance.guid(), -} - fragments.noInvitesFragment1 = { ...fragments.fragment, recommendations: [], - invites: [], id: chance.guid(), } fragments.minorRevisionWithoutReview = { diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 70efac8ded420595e62bc16fa271b90a74fb741d..0db955323c5e15fe008bf2ef90152788d078df46 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -1,4 +1,4 @@ -const { findLast, get } = require('lodash') +const { findLast, isEmpty, maxBy, get, flatMap } = require('lodash') const Fragment = require('./Fragment') @@ -10,6 +10,7 @@ class Collection { async updateStatusByRecommendation({ recommendation, isHandlingEditor = false, + fragments, }) { let newStatus if (isHandlingEditor) { @@ -19,7 +20,9 @@ class Collection { } } else { if (recommendation === 'minor') { - newStatus = 'reviewCompleted' + newStatus = this.hasAtLeastOneReviewReport(fragments) + ? 'reviewCompleted' + : 'heAssigned' } if (recommendation === 'major') { @@ -27,7 +30,7 @@ class Collection { } } - this.updateStatus({ newStatus }) + return this.updateStatus({ newStatus }) } async updateFinalStatusByRecommendation({ recommendation }) { @@ -89,18 +92,16 @@ class Collection { async updateStatusOnRecommendation({ isEditorInChief, recommendation }) { if (isEditorInChief) { if (recommendation === 'return-to-handling-editor') { - this.updateStatus({ newStatus: 'reviewCompleted' }) - } else { - this.updateFinalStatusByRecommendation({ - recommendation, - }) + return this.updateStatus({ newStatus: 'reviewCompleted' }) } - } else { - this.updateStatusByRecommendation({ + return this.updateFinalStatusByRecommendation({ recommendation, - isHandlingEditor: true, }) } + return this.updateStatusByRecommendation({ + recommendation, + isHandlingEditor: true, + }) } getHELastName() { @@ -108,6 +109,27 @@ class Collection { return lastName || firstName } + async getReviewerNumber({ userId }) { + const allCollectionFragments = await this.collection.getFragments() + const allCollectionInvitations = flatMap( + allCollectionFragments, + fragment => fragment.invitations, + ) + const allNumberedInvitationsForUser = allCollectionInvitations + .filter(invite => invite.userId === userId) + .filter(invite => invite.reviewerNumber) + + if (isEmpty(allNumberedInvitationsForUser)) { + const maxReviewerNumber = get( + maxBy(allCollectionInvitations, 'reviewerNumber'), + 'reviewerNumber', + 0, + ) + return maxReviewerNumber + 1 + } + return allNumberedInvitationsForUser[0].reviewerNumber + } + // eslint-disable-next-line class-methods-use-this hasAtLeastOneReviewReport(fragments) { return fragments.some(fragment => @@ -137,6 +159,14 @@ class Collection { return fragmentHelper.hasReviewReport() } } + + async getAllFragments({ FragmentModel }) { + return Promise.all( + this.collection.fragments.map(async fragment => + FragmentModel.find(fragment), + ), + ) + } } module.exports = Collection diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index ba56f7a41f116f412842fa721f2336cf6430f169..7a0f27ae03d656269c6e5c757c094dd083f1ddb8 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -1,4 +1,4 @@ -const { get, remove } = require('lodash') +const { get, remove, findLast, last } = require('lodash') const config = require('config') const User = require('./User') @@ -145,11 +145,21 @@ class Fragment { hasReviewReport() { const { fragment: { recommendations = [] } } = this - return recommendations.find( + return recommendations.some( rec => rec.recommendationType === 'review' && rec.submittedOn, ) } + canHEMakeAnotherRecommendation(currentUserRecommendations) { + const lastHERecommendation = last(currentUserRecommendations) + 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 = [], diff --git a/packages/component-helper-service/src/tests/collection.test.js b/packages/component-helper-service/src/tests/collection.test.js new file mode 100644 index 0000000000000000000000000000000000000000..167153af8c03fc7149c36a9e8c149de351c0f7db --- /dev/null +++ b/packages/component-helper-service/src/tests/collection.test.js @@ -0,0 +1,234 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const { cloneDeep } = require('lodash') +const fixturesService = require('pubsweet-component-fixture-service') + +const { fixtures, Model } = fixturesService +const { Collection, Fragment } = require('../Helper') + +describe('Collection helper', () => { + let testFixtures = {} + let models + + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + models = Model.build(testFixtures) + }) + + describe('getReviewerNumber', () => { + it('should assign reviewer number 1 on invitation if no other reviewer numbers exist', async () => { + const { collection } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ collection }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(1) + }) + it('should assign next reviewer number on invitation if another reviewer numbers exist', async () => { + const { collectionReviewCompleted } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: collectionReviewCompleted, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(3) + }) + it('should keep reviewer number across fragment versions', async () => { + const { oneReviewedFragmentCollection } = testFixtures.collections + const { answerReviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: oneReviewedFragmentCollection, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: answerReviewer.id, + }) + + expect(reviewerNumber).toBe(2) + }) + it('should assign next reviewer number across fragment versions', async () => { + const { oneReviewedFragmentCollection } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: oneReviewedFragmentCollection, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(3) + }) + }) + + describe('hasAtLeastOneReviewReport', () => { + it('should return true if collection has at least one report from reviewers.', async () => { + const { collection } = testFixtures.collections + const collectionHelper = new Collection({ collection }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const hasReviewReport = await collectionHelper.hasAtLeastOneReviewReport( + fragments, + ) + expect(hasReviewReport).toBe(true) + }) + it('should return false if collection has at least one report from reviewers.', async () => { + const { noInvitesFragment } = testFixtures.fragments + const { collection } = testFixtures.collections + collection.fragments = [noInvitesFragment.id] + const collectionHelper = new Collection({ collection }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const hasReviewReport = await collectionHelper.hasAtLeastOneReviewReport( + fragments, + ) + expect(hasReviewReport).toBe(false) + }) + }) + + describe('canHEMakeRecommendation', () => { + it('should return true when creating a recommendation as a HE when there is a single version with at least one review.', async () => { + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ fragment }) + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(true) + }) + it('should return false when creating a recommendation with publish as a HE when there is a single version and there are no reviews.', async () => { + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + fragment.recommendations = undefined + + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ fragment }) + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(false) + }) + it('should return true when creating a recommendation as a HE after minor revision and we have at least one review on collection.', async () => { + const { collection } = testFixtures.collections + const { + minorRevisionWithReview, + noInvitesFragment1, + } = testFixtures.fragments + collection.fragments = [minorRevisionWithReview.id, noInvitesFragment1.id] + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ fragment: noInvitesFragment1 }) + + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(true) + }) + it('should return false when creating a recommendation as a HE after minor revision and there are no reviews.', async () => { + const { collection } = testFixtures.collections + const { + minorRevisionWithoutReview, + noInvitesFragment1, + } = testFixtures.fragments + collection.fragments = [ + minorRevisionWithoutReview.id, + noInvitesFragment1.id, + ] + + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ noInvitesFragment1 }) + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(false) + }) + it('should return true when creating a recommendation as a HE after major revision and there are least one review on fragment.', async () => { + const { collection } = testFixtures.collections + const { + majorRevisionWithReview, + reviewCompletedFragment, + } = testFixtures.fragments + + reviewCompletedFragment.collectionId = collection.id + collection.fragments = [ + majorRevisionWithReview.id, + reviewCompletedFragment.id, + ] + + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ fragment: reviewCompletedFragment }) + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(true) + }) + it('should return false when creating a recommendation as a HE after major revision there are no reviews on fragment.', async () => { + const { collection } = testFixtures.collections + const { + majorRevisionWithReview, + noInvitesFragment1, + } = testFixtures.fragments + collection.fragments = [majorRevisionWithReview.id, noInvitesFragment1.id] + const collectionHelper = new Collection({ + collection, + }) + const FragmentModel = models.Fragment + const fragments = await collectionHelper.getAllFragments({ + FragmentModel, + }) + const fragmentHelper = new Fragment({ fragment: noInvitesFragment1 }) + const canHEMakeRecommendation = await collectionHelper.canHEMakeRecommendation( + fragments, + fragmentHelper, + ) + expect(canHEMakeRecommendation).toBe(false) + }) + }) +}) diff --git a/packages/component-helper-service/src/tests/fragment.test.js b/packages/component-helper-service/src/tests/fragment.test.js index 26598cfe8bd73fc40a8966a03ab51aa3552f3668..c5623dc18a8d7cf4812d3695189319f48b7e2202 100644 --- a/packages/component-helper-service/src/tests/fragment.test.js +++ b/packages/component-helper-service/src/tests/fragment.test.js @@ -15,6 +15,8 @@ const { recommendations: configRecommendations } = config const acceptedReviewerId = chance.guid() const submittedReviewerId1 = chance.guid() const submittedReviewerId2 = chance.guid() +const handlingEditorId = chance.guid() +const editorInChiefId = chance.guid() const fragment = { invitations: [ { @@ -281,4 +283,93 @@ describe('Fragment helper', () => { } }) }) + describe('canHEMakeAnotherRecommendation', () => { + it('should return true when He makes a recommendation after EIC decision was to return to HE', async () => { + testFragment.recommendations = [ + { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: handlingEditorId, + createdOn: 1542361074012, + updatedOn: chance.timestamp(), + }, + { + recommendation: 'return-to-handling-editor', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: editorInChiefId, + createdOn: 1542361115749, + updatedOn: chance.timestamp(), + }, + ] + const currentUserRecommendations = testFragment.recommendations.filter( + r => r.userId === handlingEditorId, + ) + const fragmentHelper = new Fragment({ fragment: testFragment }) + const canHEMakeAnotherRecommendation = await fragmentHelper.canHEMakeAnotherRecommendation( + currentUserRecommendations, + ) + expect(canHEMakeAnotherRecommendation).toBe(true) + }) + it('should return false when He makes another recommendation', async () => { + testFragment.recommendations = [ + { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + comments: [ + { + content: chance.paragraph(), + public: true, + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: handlingEditorId, + createdOn: 1542361074012, + updatedOn: chance.timestamp(), + }, + ] + const currentUserRecommendations = testFragment.recommendations.filter( + r => r.userId === handlingEditorId, + ) + const fragmentHelper = new Fragment({ fragment: testFragment }) + const canHEMakeAnotherRecommendation = await fragmentHelper.canHEMakeAnotherRecommendation( + currentUserRecommendations, + ) + expect(canHEMakeAnotherRecommendation).toBe(false) + }) + }) }) diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js index 1a441531473daef14c295e408fc86e7d57fed96d..0e45b83faac29aef7b05b2ca588cf07b6dc7fb0e 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -4,6 +4,7 @@ const { services, Collection, Invitation, + Fragment, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') @@ -116,7 +117,12 @@ module.exports = models => async (req, res) => { }) } - if (collection.status === 'heAssigned') { + const fragmentHelper = new Fragment({ fragment }) + if ( + collection.status === 'heAssigned' || + (collection.status === 'reviewCompleted' && + !fragmentHelper.hasReviewReport()) + ) { collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) } diff --git a/packages/component-manuscript-manager/src/routes/fragments/patch.js b/packages/component-manuscript-manager/src/routes/fragments/patch.js index 396672ff07542005238642045ebe226e45d8fa3c..acd037254ddb9ada6417d99980a2a30588799d08 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js @@ -103,8 +103,13 @@ module.exports = models => async (req, res) => { await authorsTeam.save() } - collectionHelper.updateStatusByRecommendation({ + const fragments = await collectionHelper.getAllFragments({ + FragmentModel: models.Fragment, + }) + + await collectionHelper.updateStatusByRecommendation({ recommendation: heRecommendation.recommendation, + fragments, }) newFragment.submitted = Date.now() diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index 7846722ed2f83db255a59ff80f95c142ccf2d3ec..7cd5b2ae49984d23fff26a53ae1bfb38a19f8244 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -1,3 +1,4 @@ +const { find } = require('lodash') const { services, authsome: authsomeHelper, @@ -8,6 +9,7 @@ const Notification = require('../../notifications/notification') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params + const userId = req.user let collection, fragment try { collection = await models.Collection.find(collectionId) @@ -25,7 +27,7 @@ module.exports = models => async (req, res) => { if (!recommendation) return res.status(404).json({ error: 'Recommendation not found.' }) - if (recommendation.userId !== req.user) + if (recommendation.userId !== userId) return res.status(403).json({ error: 'Unauthorized.', }) @@ -35,14 +37,14 @@ module.exports = models => async (req, res) => { fragment, path: req.route.path, } - const canPatch = await authsome.can(req.user, 'PATCH', target) + const canPatch = await authsome.can(userId, 'PATCH', target) if (!canPatch) return res.status(403).json({ error: 'Unauthorized.', }) const UserModel = models.User - const reviewer = await UserModel.find(req.user) + const reviewer = await UserModel.find(userId) Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() @@ -62,6 +64,16 @@ module.exports = models => async (req, res) => { const collectionHelper = new Collection({ collection }) collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) } + + const collectionHelper = new Collection({ collection }) + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId, + }) + + find(fragment.invitations, [ + 'userId', + userId, + ]).reviewerNumber = reviewerNumber } fragment.save() diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index db2336e8eebebb019f2eecf603381fa7973a5b6e..b47234c1f1a017149269ba6e415ee49d5af6b159 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -1,6 +1,5 @@ -/* eslint-disable no-return-await */ const uuid = require('uuid') -const { pick, get, set, has, isEmpty, last } = require('lodash') +const { pick, get, set, has, isEmpty, last, chain } = require('lodash') const config = require('config') const { v4 } = require('uuid') const logger = require('@pubsweet/logger') @@ -45,11 +44,9 @@ module.exports = models => async (req, res) => { const collectionHelper = new Collection({ collection }) try { - fragments = await Promise.all( - collection.fragments.map( - async fragment => await models.Fragment.find(fragment), - ), - ) + fragments = await collectionHelper.getAllFragments({ + FragmentModel: models.Fragment, + }) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') fragments = [] @@ -57,9 +54,16 @@ module.exports = models => async (req, res) => { error: notFoundError.message, }) } - const currentUserRecommendation = get(fragment, 'recommendations', []).filter( - r => r.userId === req.user, - ) + 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 = { @@ -92,16 +96,32 @@ module.exports = models => async (req, res) => { } if ( last(collection.fragments) === fragmentId && - !isEmpty(currentUserRecommendation) + !isEmpty(currentUserRecommendations) ) { if (recommendationType === recommendations.type.review) { return res .status(400) .json({ error: 'Cannot write another review on this version.' }) } - return res - .status(400) - .json({ error: 'Cannot make another recommendation 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 ( @@ -130,7 +150,7 @@ module.exports = models => async (req, res) => { newRecommendation.comments = comments || undefined if (recommendationType === 'editorRecommendation') { - collectionHelper.updateStatusOnRecommendation({ + await collectionHelper.updateStatusOnRecommendation({ isEditorInChief, recommendation, }) 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 fb88e5658444b5c3bad2f2cf5ac7760bf9900537..aa4a1cfec4a3e9de92809e07e056a81b2aaede1f 100644 --- a/packages/component-manuscript-manager/src/tests/collections/get.test.js +++ b/packages/component-manuscript-manager/src/tests/collections/get.test.js @@ -61,7 +61,7 @@ describe('Get collections route handler', () => { expect(data).toHaveLength(2) expect(data[0].type).toEqual('collection') - expect(data[0].currentVersion.recommendations).toHaveLength(3) + 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/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index df52effe144daf17b1a9ec16e546c4f189b62187..5638eb69ff3be3a1a164b0226377f6c38d7f0486 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -571,6 +571,52 @@ describe('Post fragments recommendations route handler', () => { expect(data.error).toEqual('Cannot write another review on this version.') }) + it('should return success when creating another recommendation as a HE on the same version when EiC returned manuscript to He ', async () => { + const { noRecommendationHE } = testFixtures.users + const { noEditorRecomedationCollection } = testFixtures.collections + const { noEditorRecomedationFragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: noRecommendationHE.id, + models, + route, + path, + params: { + collectionId: noEditorRecomedationCollection.id, + fragmentId: noEditorRecomedationFragment.id, + }, + }) + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.userId).toEqual(noRecommendationHE.id) + }) + + it('should return an error when creating another recommendation as a HE on the same version after EiC made decision to publish', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'publish' + body.recommendationType = 'editorRecommendation' + + const res = await requests.sendRequest({ + body, + userId: handlingEditor.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 another recommendation on this version.', + ) + }) + it('should return an error when an EiC makes a decision on an older version of a manuscript', async () => { const { editorInChief } = testFixtures.users const { twoVersionsCollection } = testFixtures.collections