diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js index 7241792473c594738d65210ccfe98b4e0f08e91f..15d06ecb278aac32ee6928c6eced4c1bf74bc2aa 100644 --- a/packages/component-helper-service/src/services/Email.js +++ b/packages/component-helper-service/src/services/Email.js @@ -347,6 +347,30 @@ class Email { }, }) } + + async setupManuscriptSubmittedEmail() { + const { + baseUrl, + UserModel, + collection, + parsedFragment: { id, title }, + authors: { submittingAuthor: { firstName = '', lastName = '' } }, + } = this + + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + + mailService.sendSimpleEmail({ + toEmail: eic.email, + emailType: 'manuscript-submitted', + dashboardUrl: baseUrl, + meta: { + collection, + fragment: { id, authorName: `${firstName} ${lastName}`, title }, + eicName: `${eic.firstName} ${eic.lastName}`, + }, + }) + } } const getSubject = recommendation => diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index d40d99d1f2803362450ef95ec51552948ce6e791..39cff7d046e3704ae6abdb3a3cc728672bb1fe38 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -7,10 +7,10 @@ const resetPath = config.get('invite-reset-password.url') module.exports = { sendSimpleEmail: async ({ - toEmail, - user, - emailType, - dashboardUrl, + toEmail = '', + user = {}, + emailType = '', + dashboardUrl = '', meta = {}, }) => { let subject, textBody @@ -117,6 +117,26 @@ module.exports = { textBody = `${replacements.headline}` emailTemplate = 'noCTA' break + case 'manuscript-submitted': + subject = `${meta.collection.customId}: Manuscript Submitted` + replacements.previewText = 'A new manuscript has been submitted' + replacements.headline = `A new manuscript has been submitted.` + replacements.paragraph = `You can view the full manuscript titled "${ + meta.fragment.title + }" by ${ + meta.fragment.authorName + } and take further actions by clicking on the following link:` + replacements.buttonText = 'MANUSCRIPT DETAILS' + replacements.url = helpers.createUrl( + dashboardUrl, + `/projects/${meta.collection.id}/versions/${ + meta.fragment.id + }/details`, + ) + textBody = `${replacements.headline} ${replacements.paragraph} ${ + replacements.url + } ${replacements.buttonText}` + break default: subject = 'Welcome to Hindawi!' break diff --git a/packages/component-manuscript-manager/src/Fragments.js b/packages/component-manuscript-manager/src/Fragments.js index 9e3569166d7dd7e7e76f9082412ab3bc8219c16a..01e11a93582bdc5ff37497d2bb7a7c6a980b68c0 100644 --- a/packages/component-manuscript-manager/src/Fragments.js +++ b/packages/component-manuscript-manager/src/Fragments.js @@ -9,7 +9,7 @@ const Fragments = app => { }) /** * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a revision for a manuscript - * @apiGroup FragmentsRecommendations + * @apiGroup Fragments * @apiParam {collectionId} collectionId Collection id * @apiParam {fragmentId} fragmentId Fragment id * @apiSuccessExample {json} Success @@ -28,6 +28,27 @@ const Fragments = app => { authBearer, require(`${routePath}/patch`)(app.locals.models), ) + /** + * @api {post} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a manuscript + * @apiGroup Fragments + * @apiParam {collectionId} collectionId Collection id + * @apiParam {fragmentId} fragmentId Fragment id + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * + * } + * @apiErrorExample {json} Invite user errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.post( + `${basePath}`, + authBearer, + require(`${routePath}/post`)(app.locals.models), + ) } module.exports = Fragments diff --git a/packages/component-manuscript-manager/src/routes/fragments/post.js b/packages/component-manuscript-manager/src/routes/fragments/post.js new file mode 100644 index 0000000000000000000000000000000000000000..d0b5b18f6d4592f681bcd14298c8cedd5c194c0d --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/post.js @@ -0,0 +1,58 @@ +const { + Email, + Fragment, + services, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { collectionId, fragmentId } = req.params + let collection, fragment + + try { + collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Collection and fragment do not match.`, + }) + fragment = await models.Fragment.find(fragmentId) + + const authsome = authsomeHelper.getAuthsome(models) + const target = { + fragment, + path: req.route.path, + } + const canPost = await authsome.can(req.user, 'POST', target) + if (!canPost) + return res.status(403).json({ + error: 'Unauthorized.', + }) + + fragment.submitted = Date.now() + fragment = await fragment.save() + + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const authors = await fragmentHelper.getAuthorData({ + UserModel: models.User, + }) + + const email = new Email({ + authors, + collection, + parsedFragment, + UserModel: models.User, + baseUrl: services.getBaseUrl(req), + }) + email.setupManuscriptSubmittedEmail() + + return res.status(200).json(fragment) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'Item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-manuscript-manager/src/tests/fragments/post.test.js b/packages/component-manuscript-manager/src/tests/fragments/post.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1da5c72f731cdc11512706c2709d014dd2ed1930 --- /dev/null +++ b/packages/component-manuscript-manager/src/tests/fragments/post.test.js @@ -0,0 +1,86 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') + +const { Model, fixtures } = fixturesService +jest.mock('pubsweet-component-mail-service', () => ({ + sendNotificationEmail: jest.fn(), +})) +const reqBody = {} + +const path = '../routes/fragments/post' +const route = { + path: '/api/collections/:collectionId/fragments/:fragmentId/submit', +} +describe('Post fragments route handler', () => { + let testFixtures = {} + let body = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + body = cloneDeep(reqBody) + models = Model.build(testFixtures) + }) + it('should return success when the parameters are correct', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + }) + it('should return an error when the fragmentId does not match the collectionId', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { noParentFragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: noParentFragment.id, + }, + }) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Collection and fragment do not match.') + }) + it('should return an error when the collection does not exist', async () => { + const { user } = testFixtures.users + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: 'invalid-id', + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Item not found') + }) +}) diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 960e0bede30325e9d0dec6bc33e6762880b77e8e..043e5e8c87b8c4c89929611852dab47bbe350e86 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -192,6 +192,14 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { roles: ['reviewer', 'handlingEditor'], }) } + + // allow owner to submit a manuscript + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/submit' + ) { + return helpers.isOwner({ user, object: object.fragment }) + } } if (operation === 'PATCH') {