Commit 5974e83e authored by Aanand Prasad's avatar Aanand Prasad

Merge branch 'master' into styled-components

parents 2c1af823 70a3f54d
Pipeline #4968 passed with stages
in 7 minutes and 5 seconds
{
"name": "pubsweet-component-xpub-review-backend",
"version": "0.1.0",
"main": "src",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "Collaborative Knowledge Foundation",
"license": "MIT",
"dependencies": {
"config": "^1.26.1",
"moment": "^2.18.1",
"nodemailer": "^4.0.1"
},
"devDependencies": {
"body-parser": "^1.17.2",
"jest": "^22.4.2",
"superagent": "^3.8.2",
"supertest": "^3.0.0"
},
"peerDependencies": {
"@pubsweet/logger": ">=0.0.1",
"pubsweet-component-xpub-review": "^0.0.1",
"pubsweet-server": ">=1.0.0-alpha.1"
}
}
module.exports = {
backend: () => require('./reviewBackend.js'),
}
const { pick } = require('lodash')
const config = require('config')
const logger = require('@pubsweet/logger')
const User = require('pubsweet-server/src/models/User')
const Fragment = require('pubsweet-server/src/models/Fragment')
const Collection = require('pubsweet-server/src/models/Collection')
const authsome = require('pubsweet-server/src/helpers/authsome')
const AuthorizationError = require('pubsweet-server/src/errors/AuthorizationError')
const transport = require('./transport')
module.exports = app => {
app.patch('/api/make-decision', async (req, res, next) => {
try {
const version = await Fragment.find(req.body.versionId)
const project = await Collection.find(req.body.projectId)
const authors = await Promise.all(version.owners.map(id => User.find(id)))
const canViewVersion = await authsome.can(req.user, 'GET', version)
const canPatchVersion = await authsome.can(req.user, 'PATCH', version)
if (!canPatchVersion || !canViewVersion) throw new AuthorizationError()
let versionUpdateData = { rev: version.rev, decision: req.body.decision }
if (canPatchVersion.filter) {
versionUpdateData = canPatchVersion.filter(versionUpdateData)
}
await version.updateProperties(versionUpdateData)
let nextVersionData
let projectUpdateData = { rev: project.rev }
let message
switch (version.decision.recommendation) {
case 'accept':
projectUpdateData.status = 'accepted'
message = '<p>Your manuscript has been accepted</p>'
break
case 'reject':
projectUpdateData.status = 'rejected'
message = '<p>Your manuscript has been rejected</p>'
break
case 'revise': {
projectUpdateData.status = 'revising'
message = '<p>Revisions to your manuscript have been requested</p>'
const cloned = pick(version, [
'source',
'metadata',
'declarations',
'suggestions',
'files',
'notes',
])
nextVersionData = {
fragmentType: 'version',
created: new Date(),
...cloned,
version: version.version + 1,
}
break
}
default:
throw new Error('Unknown decision type')
}
message += version.decision.note.content
let nextVersion
let canViewNextVersion
if (nextVersionData) {
const canCreateVersion = await authsome.can(
req.user,
'POST',
nextVersionData,
)
if (!canCreateVersion) throw new AuthorizationError()
if (canCreateVersion.filter) {
nextVersionData = canCreateVersion.filter(nextVersionData)
}
nextVersion = new Fragment(nextVersionData)
canViewNextVersion = await authsome.can(req.user, 'GET', nextVersion)
}
const canViewProject = await authsome.can(req.user, 'GET', project)
const canPatchProject = await authsome.can(req.user, 'PATCH', project)
if (!canPatchProject || !canViewProject) throw new AuthorizationError()
if (canPatchProject.filter) {
projectUpdateData = canPatchProject.filter(projectUpdateData)
}
await project.updateProperties(projectUpdateData)
await Promise.all([
version.save(),
project.save(),
nextVersion && nextVersion.save(),
])
const authorEmails = authors.map(user => user.email)
logger.info(`Sending decision email to ${authorEmails}`)
await transport.sendMail({
from: config.get('mailer.from'),
to: authorEmails,
subject: 'Decision made',
html: message,
})
res.send({
version: canViewVersion.filter
? canViewVersion.filter(version)
: version,
project: canViewProject.filter
? canViewProject.filter(project)
: project,
nextVersion:
canViewNextVersion && canViewNextVersion.filter
? canViewNextVersion.filter(nextVersion)
: nextVersion,
})
} catch (err) {
next(err)
}
})
}
process.env.SUPPRESS_NO_CONFIG_WARNING = true
process.env.NODE_CONFIG = '{"mailer":{"from":"sender@example.com"}}'
const express = require('express')
const supertest = require('supertest')
const bodyParser = require('body-parser')
// mocks
jest.mock('./transport', () => ({ sendMail: jest.fn() }))
jest.mock('pubsweet-server/src/models/User', () => ({
find: jest.fn(() => ({ email: 'author@example.org' })),
}))
jest.mock('pubsweet-server/src/models/Fragment', () => ({
find: jest.fn(() => ({
version: 1,
owners: [{}],
updateProperties(update) {
Object.assign(this, update)
},
save: () => {},
})),
}))
jest.mock('pubsweet-server/src/models/Collection', () => ({
find: jest.fn(() => ({
updateProperties: () => {},
save: () => {},
})),
}))
jest.mock('pubsweet-server/src/helpers/authsome', () => ({
can: jest.fn(() => true),
}))
const authsome = require('pubsweet-server/src/helpers/authsome')
const transport = require('./transport')
const component = require('./reviewBackend')
function makeApp() {
const app = express()
app.use(bodyParser.json())
component(app)
return supertest(app)
}
describe('/api/make-decision route', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('sends email on acceptance', async () => {
const app = makeApp()
const response = await app.patch('/api/make-decision').send({
decision: { recommendation: 'accept', note: { content: 'blah blah' } },
versionId: 1,
projectId: 2,
})
expect(response.body.version).toBeDefined()
expect(response.body.project).toBeDefined()
expect(response.body.nextVersion).not.toBeDefined()
expect(transport.sendMail).toHaveBeenCalledWith(
expect.objectContaining({
from: 'sender@example.com',
to: ['author@example.org'],
subject: 'Decision made',
}),
)
})
it('rejects if not authorised', async () => {
authsome.can.mockReturnValue(false)
const app = makeApp()
const response = await app.patch('/api/make-decision').send({
decision: { recommendation: 'accept', note: { content: 'blah blah' } },
versionId: 1,
projectId: 2,
})
expect(response.status).toBe(403)
expect(transport.sendMail).not.toHaveBeenCalled()
})
})
const config = require('config')
const nodemailer = require('nodemailer')
// SMTP transport options: https://nodemailer.com/smtp/
const options = config.get('mailer.transport')
module.exports = nodemailer.createTransport(options)
import { debounce, pick } from 'lodash'
import { debounce } from 'lodash'
import { compose, withProps } from 'recompose'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
......@@ -14,66 +14,6 @@ import {
import uploadFile from 'xpub-upload'
import DecisionLayout from './decision/DecisionLayout'
// TODO: this should happen on the server
const handleDecision = (project, version) => dispatch =>
dispatch(
actions.updateFragment(project, {
decision: version.decision,
id: version.id,
rev: version.rev,
}),
).then(() => {
const cloned = pick(version, [
'source',
'metadata',
'declarations',
'suggestions',
'files',
'notes',
])
switch (version.decision.recommendation) {
case 'accept':
return dispatch(
actions.updateCollection({
id: project.id,
rev: project.rev,
status: 'accepted',
}),
)
case 'reject':
return dispatch(
actions.updateCollection({
id: project.id,
rev: project.rev,
status: 'rejected',
}),
)
case 'revise':
return dispatch(
actions.updateCollection({
id: project.id,
rev: project.rev,
status: 'revising',
}),
).then(() =>
dispatch(
actions.createFragment(project, {
fragmentType: 'version',
created: new Date(), // TODO: set on server
...cloned,
version: version.version + 1,
}),
),
)
default:
throw new Error('Unknown decision type')
}
})
const onSubmit = (values, dispatch, { project, version, history }) => {
version.decision = {
...version.decision,
......@@ -82,7 +22,7 @@ const onSubmit = (values, dispatch, { project, version, history }) => {
submitted: new Date(),
}
return dispatch(handleDecision(project, version))
return dispatch(actions.makeDecision(project, version))
.then(() => {
// TODO: show "thanks for your review" message
history.push('/')
......
module.exports = {
frontend: {
components: [() => require('./components')],
actions: () => ({ makeDecision: require('./redux').makeDecision }),
reducers: {
makeDecision: () => require('./redux').default,
},
},
}
import * as api from 'pubsweet-client/src/helpers/api'
import {
GET_COLLECTION_SUCCESS,
GET_FRAGMENT_SUCCESS,
} from 'pubsweet-client/src/actions/types'
export const MAKE_DECISION_REQUEST = 'MAKE_DECISION_REQUEST'
export const MAKE_DECISION_SUCCESS = 'MAKE_DECISION_SUCCESS'
export const MAKE_DECISION_FAILURE = 'MAKE_DECISION_FAILURE'
function makeDecisionRequest(project, version) {
return {
type: MAKE_DECISION_REQUEST,
project,
version,
}
}
function makeDecisionSuccess(project, version, result) {
return {
type: MAKE_DECISION_SUCCESS,
project,
version,
result,
}
}
function makeDecisionFailure(project, version, error) {
return {
type: MAKE_DECISION_FAILURE,
project,
version,
error,
}
}
export function makeDecision(project, version) {
return dispatch => {
dispatch(makeDecisionRequest(project, version))
return api
.update('/make-decision', {
projectId: project.id,
versionId: version.id,
decision: version.decision,
})
.then(result => {
dispatch({
type: GET_COLLECTION_SUCCESS,
collection: result.project,
receivedAt: Date.now(),
})
dispatch({
type: GET_FRAGMENT_SUCCESS,
fragment: result.version,
receivedAt: Date.now(),
})
if (result.nextVersion) {
dispatch({
type: GET_FRAGMENT_SUCCESS,
fragment: result.nextVersion,
receivedAt: Date.now(),
})
}
dispatch(makeDecisionSuccess(project, version, result))
})
.catch(error => dispatch(makeDecisionFailure(project, version, error)))
}
}
const initialState = {}
export default (state = initialState, action) => {
switch (action.type) {
case MAKE_DECISION_REQUEST:
return {}
case MAKE_DECISION_SUCCESS:
return {}
case MAKE_DECISION_FAILURE:
return { error: action.error }
default:
return state
}
}
......@@ -3,6 +3,7 @@
"pubsweet-component-xpub-dashboard",
"pubsweet-component-xpub-manuscript",
"pubsweet-component-xpub-review",
"pubsweet-component-xpub-review-backend",
"pubsweet-component-xpub-submit",
"pubsweet-component-ink-backend",
"pubsweet-component-login",
......
......@@ -27,8 +27,11 @@ module.exports = {
'redux-log': false,
theme: process.env.PUBSWEET_THEME,
},
'mail-transport': {
sendmail: true,
mailer: {
from: 'dev@example.com',
transport: {
sendmail: true,
},
},
'password-reset': {
url:
......
......@@ -30,6 +30,7 @@
"pubsweet-component-xpub-find-reviewers": "^0.0.2",
"pubsweet-component-xpub-manuscript": "^0.0.2",
"pubsweet-component-xpub-review": "^0.0.2",
"pubsweet-component-xpub-review-backend": "^0.1.0",
"pubsweet-component-xpub-submit": "^0.0.2",
"pubsweet-server": "^1.0.1",
"react": "^16.2.0",
......
......@@ -1517,7 +1517,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
body-parser@1.18.2, body-parser@^1.15.2:
body-parser@1.18.2, body-parser@^1.15.2, body-parser@^1.17.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
dependencies:
......@@ -2283,7 +2283,7 @@ compare-func@^1.3.1:
array-ify "^1.0.0"
dot-prop "^3.0.0"
component-emitter@^1.2.1:
component-emitter@^1.2.0, component-emitter@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
......@@ -2531,6 +2531,10 @@ cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
cookiejar@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
......@@ -4216,6 +4220,14 @@ forever-monitor@^1.7.0:
ps-tree "0.0.x"
utile "~0.2.1"
form-data@^2.3.1, form-data@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies:
asynckit "^0.4.0"
combined-stream "1.0.6"
mime-types "^2.1.12"
form-data@~2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
......@@ -4224,13 +4236,9 @@ form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
form-data@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies:
asynckit "^0.4.0"
combined-stream "1.0.6"
mime-types "^2.1.12"
formidable@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
forwarded@~0.1.2:
version "0.1.2"
......@@ -6020,7 +6028,7 @@ jest-worker@^22.2.2:
dependencies:
merge-stream "^1.0.1"
jest@^22.1.1:
jest@^22.1.1, jest@^22.4.2:
version "22.4.2"
resolved "https://registry.yarnpkg.com/jest/-/jest-22.4.2.tgz#34012834a49bf1bdd3bc783850ab44e4499afc20"
dependencies:
......@@ -6979,7 +6987,7 @@ merge@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
methods@~1.1.2:
methods@^1.1.1, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
......@@ -7040,7 +7048,7 @@ mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
mime@^1.5.0:
mime@^1.4.1, mime@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
......@@ -7427,6 +7435,12 @@ node-sass@^4.5.3:
stdout-stream "^1.4.0"
"true-case-path" "^1.0.2"
nodemailer@^4.0.1:
version "4.6.2"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.6.2.tgz#1d0b34691d9f4b7ac5e6c240bccc1c9d025e3f67"
dependencies:
request "^2.83.0"
nomnom@~1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
......@@ -9093,7 +9107,7 @@ q@^1.1.2, q@^1.4.1, q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
qs@6.5.1, qs@~6.5.1:
qs@6.5.1, qs@^6.5.1, qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
......@@ -11024,6 +11038,28 @@ sugarss@^1.0.0:
dependencies:
postcss "^6.0.14"
superagent@^3.0.0, superagent@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403"
dependencies:
component-emitter "^1.2.0"
cookiejar "^2.1.0"
debug "^3.1.0"
extend "^3.0.0"
form-data "^2.3.1"
formidable "^1.1.1"
methods "^1.1.1"
mime "^1.4.1"
qs "^6.5.1"
readable-stream "^2.0.5"
supertest@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296"
dependencies:
methods "~1.1.2"
superagent "^3.0.0"
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment