diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js
index e88151898..bad277721 100644
--- a/backend/src/graphql-schema.js
+++ b/backend/src/graphql-schema.js
@@ -11,6 +11,7 @@ import shout from './resolvers/shout.js'
import rewards from './resolvers/rewards.js'
import socialMedia from './resolvers/socialMedia.js'
import notifications from './resolvers/notifications'
+import comments from './resolvers/comments'
export const typeDefs = fs
.readFileSync(
@@ -22,7 +23,8 @@ export const resolvers = {
Query: {
...statistics.Query,
...userManagement.Query,
- ...notifications.Query
+ ...notifications.Query,
+ ...comments.Query
},
Mutation: {
...userManagement.Mutation,
@@ -33,6 +35,7 @@ export const resolvers = {
...shout.Mutation,
...rewards.Mutation,
...socialMedia.Mutation,
- ...notifications.Mutation
+ ...notifications.Mutation,
+ ...comments.Mutation
}
}
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index 3ac43a6e2..3688aec16 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -86,7 +86,8 @@ const permissions = shield({
unshout: isAuthenticated,
changePassword: isAuthenticated,
enable: isModerator,
- disable: isModerator
+ disable: isModerator,
+ CreateComment: isAuthenticated
// CreateUser: allow,
},
User: {
diff --git a/backend/src/middleware/softDeleteMiddleware.spec.js b/backend/src/middleware/softDeleteMiddleware.spec.js
index 46005a4ff..f007888ed 100644
--- a/backend/src/middleware/softDeleteMiddleware.spec.js
+++ b/backend/src/middleware/softDeleteMiddleware.spec.js
@@ -23,21 +23,19 @@ beforeAll(async () => {
])
await Promise.all([
- factory.create('Comment', { id: 'c2', content: 'Enabled comment on public post' })
+ factory.create('Comment', { id: 'c2', postId: 'p3', content: 'Enabled comment on public post' })
])
await Promise.all([
- factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
- factory.relate('Comment', 'Post', { from: 'c2', to: 'p3' })
+ factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })
])
const asTroll = Factory()
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false })
- await asTroll.create('Comment', { id: 'c1', content: 'Disabled comment' })
+ await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
await Promise.all([
- asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' }),
- asTroll.relate('Comment', 'Post', { from: 'c1', to: 'p3' })
+ asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })
])
const asModerator = Factory()
diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js
new file mode 100644
index 000000000..b3350ec8e
--- /dev/null
+++ b/backend/src/resolvers/comments.js
@@ -0,0 +1,45 @@
+import { neo4jgraphql } from 'neo4j-graphql-js'
+
+export default {
+ Query: {
+ CommentByPost: async (object, params, context, resolveInfo) => {
+ const { postId } = params
+
+ const session = context.driver.session()
+ const transactionRes = await session.run(`
+ MATCH (comment:Comment)-[:COMMENTS]->(post:Post {id: $postId})
+ RETURN comment {.id, .contentExcerpt, .createdAt}`, {
+ postId
+ })
+
+ session.close()
+ let comments = []
+ transactionRes.records.map(record => {
+ comments.push(record.get('comment'))
+ })
+
+ return comments
+ }
+ },
+ Mutation: {
+ CreateComment: async (object, params, context, resolveInfo) => {
+ const { postId } = params
+ delete params.postId
+ const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
+
+ const session = context.driver.session()
+
+ await session.run(`
+ MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId})
+ MERGE (post)<-[:COMMENTS]-(comment)
+ RETURN comment {.id, .content}`, {
+ postId,
+ commentId: comment.id
+ }
+ )
+ session.close()
+
+ return comment
+ }
+ }
+}
diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js
new file mode 100644
index 000000000..9918038a7
--- /dev/null
+++ b/backend/src/resolvers/comments.spec.js
@@ -0,0 +1,61 @@
+import Factory from '../seed/factories'
+import { GraphQLClient } from 'graphql-request'
+import { host, login } from '../jest/helpers'
+
+const factory = Factory()
+let client
+let variables
+
+beforeEach(async () => {
+ await factory.create('User', {
+ email: 'test@example.org',
+ password: '1234'
+ })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('CreateComment', () => {
+ const mutation = `
+ mutation($postId: ID, $content: String!) {
+ CreateComment(postId: $postId, content: $content) {
+ id
+ content
+ }
+ }
+ `
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ variables = {
+ postId: 'p1',
+ content: 'I\'m not authorised to comment'
+ }
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('creates a post', async () => {
+ variables = {
+ postId: 'p1',
+ content: 'I\'m authorised to comment'
+ }
+ const expected = {
+ CreateComment: {
+ content: 'I\'m authorised to comment'
+ }
+ }
+
+ await expect(client.request(mutation, variables)).resolves.toMatchObject(expected)
+ })
+ })
+})
diff --git a/backend/src/resolvers/moderation.spec.js b/backend/src/resolvers/moderation.spec.js
index dfbcac80f..f8aa6e10b 100644
--- a/backend/src/resolvers/moderation.spec.js
+++ b/backend/src/resolvers/moderation.spec.js
@@ -109,11 +109,11 @@ describe('disable', () => {
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
await Promise.all([
factory.create('Post', { id: 'p3' }),
- factory.create('Comment', { id: 'c47' })
+ factory.create('Comment', { id: 'c47', postId: 'p3', content: 'this comment was created for this post' })
])
+
await Promise.all([
- factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }),
- factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' })
+ factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' })
])
}
})
@@ -286,8 +286,7 @@ describe('enable', () => {
factory.create('Comment', { id: 'c456' })
])
await Promise.all([
- factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }),
- factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })
+ factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' })
])
const disableMutation = `
diff --git a/backend/src/resolvers/socialMedia.js b/backend/src/resolvers/socialMedia.js
index 3adf0e2d0..310375820 100644
--- a/backend/src/resolvers/socialMedia.js
+++ b/backend/src/resolvers/socialMedia.js
@@ -3,7 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Mutation: {
CreateSocialMedia: async (object, params, context, resolveInfo) => {
- const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, true)
+ const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
await session.run(
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql
index ff8b04dfc..4638fbd0d 100644
--- a/backend/src/schema.graphql
+++ b/backend/src/schema.graphql
@@ -16,6 +16,7 @@ type Query {
LIMIT $limit
"""
)
+ CommentByPost(postId: ID!): [Comment]!
}
type Mutation {
# Get a JWT Token for the given Email and password
@@ -210,6 +211,7 @@ type Post {
type Comment {
id: ID!
activityId: String
+ postId: ID
author: User @relation(name: "WROTE", direction: "IN")
content: String!
contentExcerpt: String
diff --git a/backend/src/seed/factories/comments.js b/backend/src/seed/factories/comments.js
index 9964d0559..ba3a85840 100644
--- a/backend/src/seed/factories/comments.js
+++ b/backend/src/seed/factories/comments.js
@@ -4,6 +4,7 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
+ postId = 'p6',
content = [
faker.lorem.sentence(),
faker.lorem.sentence()
@@ -12,12 +13,12 @@ export default function (params) {
return {
mutation: `
- mutation($id: ID!, $content: String!) {
- CreateComment(id: $id, content: $content) {
+ mutation($id: ID!, $postId: ID, $content: String!) {
+ CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`,
- variables: { id, content }
+ variables: { id, postId, content }
}
}
diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js
index 149b461b1..f17c20315 100644
--- a/backend/src/seed/seed-db.js
+++ b/backend/src/seed/seed-db.js
@@ -189,45 +189,33 @@ import Factory from './factories'
])
await Promise.all([
- f.create('Comment', { id: 'c1' }),
- f.create('Comment', { id: 'c2' }),
- f.create('Comment', { id: 'c3' }),
- f.create('Comment', { id: 'c4' }),
- f.create('Comment', { id: 'c5' }),
- f.create('Comment', { id: 'c6' }),
- f.create('Comment', { id: 'c7' }),
- f.create('Comment', { id: 'c8' }),
- f.create('Comment', { id: 'c9' }),
- f.create('Comment', { id: 'c10' }),
- f.create('Comment', { id: 'c11' }),
- f.create('Comment', { id: 'c12' })
+ f.create('Comment', { id: 'c1', postId: 'p1' }),
+ f.create('Comment', { id: 'c2', postId: 'p1' }),
+ f.create('Comment', { id: 'c3', postId: 'p3' }),
+ f.create('Comment', { id: 'c4', postId: 'p2' }),
+ f.create('Comment', { id: 'c5', postId: 'p3' }),
+ f.create('Comment', { id: 'c6', postId: 'p4' }),
+ f.create('Comment', { id: 'c7', postId: 'p2' }),
+ f.create('Comment', { id: 'c8', postId: 'p15' }),
+ f.create('Comment', { id: 'c9', postId: 'p15' }),
+ f.create('Comment', { id: 'c10', postId: 'p15' }),
+ f.create('Comment', { id: 'c11', postId: 'p15' }),
+ f.create('Comment', { id: 'c12', postId: 'p15' })
])
await Promise.all([
f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }),
- f.relate('Comment', 'Post', { from: 'c1', to: 'p1' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
- f.relate('Comment', 'Post', { from: 'c2', to: 'p1' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }),
- f.relate('Comment', 'Post', { from: 'c3', to: 'p3' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }),
- f.relate('Comment', 'Post', { from: 'c4', to: 'p2' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }),
- f.relate('Comment', 'Post', { from: 'c5', to: 'p3' }),
f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }),
- f.relate('Comment', 'Post', { from: 'c6', to: 'p4' }),
f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }),
- f.relate('Comment', 'Post', { from: 'c7', to: 'p2' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }),
- f.relate('Comment', 'Post', { from: 'c8', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }),
- f.relate('Comment', 'Post', { from: 'c9', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }),
- f.relate('Comment', 'Post', { from: 'c10', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }),
- f.relate('Comment', 'Post', { from: 'c11', to: 'p15' }),
- f.relate('Comment', 'Author', { from: 'u6', to: 'c12' }),
- f.relate('Comment', 'Post', { from: 'c12', to: 'p15' })
+ f.relate('Comment', 'Author', { from: 'u6', to: 'c12' })
])
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js
new file mode 100644
index 000000000..01601437c
--- /dev/null
+++ b/cypress/integration/common/post.js
@@ -0,0 +1,23 @@
+import { When, Then } from 'cypress-cucumber-preprocessor/steps'
+
+When('I should be able to post a comment', () => {
+ cy.get('.ProseMirror')
+ .type('This is a comment')
+ .get('.ds-form')
+ .submit()
+ .get('button')
+ .contains('Submit Comment')
+ .click()
+ .get('.iziToast-message')
+ .contains('Comment Submitted')
+ })
+
+Then('I should see my comment', () => {
+ cy.get('div.comment p')
+ .should('contain', 'This is a comment')
+})
+
+Then('the editor should be cleared', () => {
+ cy.get('.ProseMirror p')
+ .should('have.class', 'is-empty')
+})
diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature
new file mode 100644
index 000000000..9290d7e21
--- /dev/null
+++ b/cypress/integration/post/Comment.feature
@@ -0,0 +1,17 @@
+Feature: Post Comment
+ As a user
+ I want to comment on contributions of others
+ To be able to express my thoughts and emotions about these, discuss, and add give further information.
+
+ Background:
+ Given we have the following posts in our database:
+ | id | title | slug |
+ | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
+ And I have a user account
+ And I am logged in
+
+ Scenario: Comment creation
+ Given I visit "post/bWBjpkTKZp/101-essays"
+ Then I should be able to post a comment
+ And I should see my comment
+ And the editor should be cleared
diff --git a/webapp/graphql/CommentQuery.js b/webapp/graphql/CommentQuery.js
new file mode 100644
index 000000000..1c3f9be20
--- /dev/null
+++ b/webapp/graphql/CommentQuery.js
@@ -0,0 +1,13 @@
+import gql from 'graphql-tag'
+
+export default app => {
+ return gql(`
+ query CommentByPost($postId: ID!) {
+ CommentByPost(postId: $postId, orderBy: createdAt_desc) {
+ id
+ contentExcerpt
+ createdAt
+ }
+ }
+ `)
+}
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index b0f06952b..c74cbed52 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -111,7 +111,9 @@
},
"takeAction": {
"name": "Take action"
- }
+ },
+ "submitComment": "Submit Comment",
+ "commentSubmitted": "Comment Submitted"
},
"quotes": {
"african": {
diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue
index b72faa850..0ecd98c4e 100644
--- a/webapp/pages/post/_id/_slug/index.vue
+++ b/webapp/pages/post/_id/_slug/index.vue
@@ -96,26 +96,72 @@
-
-
-
- {{ post.commentsCount }} Comments
-
-
+
+
+
+
+
+ {{ post.commentsCount }} Comments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('actions.cancel') }}
+
+
+
+
+ {{ $t('post.submitComment') }}
+
+
+
+
+
+
+
+