mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1797 from Human-Connection/1707-reporting-with-specific-information
🍰 Reporting with specific information
This commit is contained in:
commit
a705f6a017
@ -122,7 +122,7 @@ const permissions = shield(
|
||||
embed: allow,
|
||||
Category: allow,
|
||||
Tag: allow,
|
||||
Report: isModerator,
|
||||
reports: isModerator,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Post: or(onlyEnabledContent, isModerator),
|
||||
|
||||
@ -57,11 +57,37 @@ const validateUpdatePost = async (resolve, root, args, context, info) => {
|
||||
return validatePost(resolve, root, args, context, info)
|
||||
}
|
||||
|
||||
const validateReport = async (resolve, root, args, context, info) => {
|
||||
const { resourceId } = args
|
||||
const { user, driver } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot report yourself!')
|
||||
const session = driver.session()
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId})
|
||||
RETURN labels(resource)[0] as label
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
const [existingReportedResource] = reportQueryRes.records.map(record => {
|
||||
return {
|
||||
label: record.get('label'),
|
||||
}
|
||||
})
|
||||
|
||||
if (existingReportedResource) throw new Error(`${existingReportedResource.label}`)
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCommentCreation,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
report: validateReport,
|
||||
},
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ function clean(dirty) {
|
||||
return dirty
|
||||
}
|
||||
|
||||
const fields = ['content', 'contentExcerpt']
|
||||
const fields = ['content', 'contentExcerpt', 'reasonDescription']
|
||||
|
||||
export default {
|
||||
Mutation: async (resolve, root, args, context, info) => {
|
||||
|
||||
@ -23,6 +23,7 @@ export default applyScalars(
|
||||
'Location',
|
||||
'SocialMedia',
|
||||
'NOTIFIED',
|
||||
'REPORTED',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
@ -35,7 +36,6 @@ export default applyScalars(
|
||||
'Notfication',
|
||||
'Post',
|
||||
'Comment',
|
||||
'Report',
|
||||
'Statistics',
|
||||
'LoggedInUser',
|
||||
'Location',
|
||||
@ -43,6 +43,7 @@ export default applyScalars(
|
||||
'User',
|
||||
'EMOTED',
|
||||
'NOTIFIED',
|
||||
'REPORTED',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
|
||||
@ -1,55 +1,76 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
report: async (parent, { id, description }, { driver, req, user }, resolveInfo) => {
|
||||
const reportId = uuid()
|
||||
report: async (_parent, params, { driver, user }, _resolveInfo) => {
|
||||
let createdRelationshipWithNestedAttributes
|
||||
const { resourceId, reasonCategory, reasonDescription } = params
|
||||
const session = driver.session()
|
||||
const reportData = {
|
||||
id: reportId,
|
||||
createdAt: new Date().toISOString(),
|
||||
description: description,
|
||||
}
|
||||
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId})
|
||||
return labels(resource)[0] as label
|
||||
`,
|
||||
{
|
||||
resourceId: id,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
const [rep] = reportQueryRes.records.map(record => {
|
||||
return {
|
||||
label: record.get('label'),
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const reportRelationshipTransactionResponse = await txc.run(
|
||||
`
|
||||
MATCH (submitter:User {id: $submitterId})
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Comment OR resource:Post
|
||||
CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
submitterId: user.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
reasonCategory,
|
||||
reasonDescription,
|
||||
},
|
||||
)
|
||||
return reportRelationshipTransactionResponse.records.map(record => ({
|
||||
report: record.get('report'),
|
||||
submitter: record.get('submitter'),
|
||||
resource: record.get('resource').properties,
|
||||
type: record.get('type'),
|
||||
}))
|
||||
})
|
||||
|
||||
if (rep) {
|
||||
throw new Error(rep.label)
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
if (!txResult[0]) return null
|
||||
const { report, submitter, resource, type } = txResult[0]
|
||||
createdRelationshipWithNestedAttributes = {
|
||||
...report.properties,
|
||||
post: null,
|
||||
comment: null,
|
||||
user: null,
|
||||
submitter: submitter.properties,
|
||||
type,
|
||||
}
|
||||
switch (type) {
|
||||
case 'Post':
|
||||
createdRelationshipWithNestedAttributes.post = resource
|
||||
break
|
||||
case 'Comment':
|
||||
createdRelationshipWithNestedAttributes.comment = resource
|
||||
break
|
||||
case 'User':
|
||||
createdRelationshipWithNestedAttributes.user = resource
|
||||
break
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return createdRelationshipWithNestedAttributes
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
reports: async (_parent, _params, { driver }, _resolveInfo) => {
|
||||
const session = driver.session()
|
||||
const res = await session.run(
|
||||
`
|
||||
MATCH (submitter:User {id: $userId})
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Comment OR resource:Post
|
||||
MERGE (report:Report {id: {reportData}.id })
|
||||
MERGE (resource)<-[:REPORTED]-(report)
|
||||
MERGE (report)<-[:REPORTED]-(submitter)
|
||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
||||
MATCH (submitter:User)-[report:REPORTED]->(resource)
|
||||
WHERE resource:User OR resource:Comment OR resource:Post
|
||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
||||
`,
|
||||
{
|
||||
resourceId: id,
|
||||
userId: user.id,
|
||||
reportData,
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
const [dbResponse] = res.records.map(r => {
|
||||
const dbResponse = res.records.map(r => {
|
||||
return {
|
||||
report: r.get('report'),
|
||||
submitter: r.get('submitter'),
|
||||
@ -58,27 +79,33 @@ export default {
|
||||
}
|
||||
})
|
||||
if (!dbResponse) return null
|
||||
const { report, submitter, resource, type } = dbResponse
|
||||
|
||||
const response = {
|
||||
...report.properties,
|
||||
post: null,
|
||||
comment: null,
|
||||
user: null,
|
||||
submitter: submitter.properties,
|
||||
type,
|
||||
}
|
||||
switch (type) {
|
||||
case 'Post':
|
||||
response.post = resource.properties
|
||||
break
|
||||
case 'Comment':
|
||||
response.comment = resource.properties
|
||||
break
|
||||
case 'User':
|
||||
response.user = resource.properties
|
||||
break
|
||||
}
|
||||
const response = []
|
||||
dbResponse.forEach(ele => {
|
||||
const { report, submitter, resource, type } = ele
|
||||
|
||||
const responseEle = {
|
||||
...report.properties,
|
||||
post: null,
|
||||
comment: null,
|
||||
user: null,
|
||||
submitter: submitter.properties,
|
||||
type,
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'Post':
|
||||
responseEle.post = resource.properties
|
||||
break
|
||||
case 'Comment':
|
||||
responseEle.comment = resource.properties
|
||||
break
|
||||
case 'User':
|
||||
responseEle.user = resource.properties
|
||||
break
|
||||
}
|
||||
response.push(responseEle)
|
||||
})
|
||||
|
||||
return response
|
||||
},
|
||||
|
||||
@ -1,35 +1,73 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { host, login, gql } from '../../jest/helpers'
|
||||
import { getDriver, neode } from '../../bootstrap/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../.././server'
|
||||
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
const driver = getDriver()
|
||||
|
||||
describe('report', () => {
|
||||
let mutation
|
||||
describe('report mutation', () => {
|
||||
let reportMutation
|
||||
let headers
|
||||
let returnedObject
|
||||
let client
|
||||
let variables
|
||||
let createPostVariables
|
||||
let user
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const action = () => {
|
||||
reportMutation = gql`
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
createdAt
|
||||
reasonCategory
|
||||
reasonDescription
|
||||
type
|
||||
submitter {
|
||||
email
|
||||
}
|
||||
user {
|
||||
name
|
||||
}
|
||||
post {
|
||||
title
|
||||
}
|
||||
comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
return client.request(reportMutation, variables)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
returnedObject = '{ description }'
|
||||
variables = {
|
||||
id: 'whatever',
|
||||
resourceId: 'whatever',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'Violates code of conduct !!!',
|
||||
}
|
||||
headers = {}
|
||||
user = await factory.create('User', {
|
||||
id: 'u1',
|
||||
role: 'user',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
id: 'u1',
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
name: 'abusive-user',
|
||||
role: 'user',
|
||||
name: 'abusive-user',
|
||||
email: 'abusive-user@example.org',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
@ -43,59 +81,57 @@ describe('report', () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
let client
|
||||
const action = () => {
|
||||
mutation = `
|
||||
mutation($id: ID!) {
|
||||
report(
|
||||
id: $id,
|
||||
description: "Violates code of conduct"
|
||||
) ${returnedObject}
|
||||
}
|
||||
`
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid resource id', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid resource id', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid resource id', () => {
|
||||
describe('valid resource id', () => {
|
||||
describe('reported resource is a user', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
id: 'u2',
|
||||
...variables,
|
||||
resourceId: 'u2',
|
||||
}
|
||||
})
|
||||
/*
|
||||
it('creates a report', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
type: null,
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
it('returns type "User"', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
type: 'User',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns resource in user attribute', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
user: {
|
||||
name: 'abusive-user',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the submitter', async () => {
|
||||
returnedObject = '{ submitter { email } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
submitter: {
|
||||
email: 'test@example.org',
|
||||
@ -104,138 +140,382 @@ describe('report', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('reported resource is a user', () => {
|
||||
it('returns type "User"', async () => {
|
||||
returnedObject = '{ type }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
type: 'User',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns resource in user attribute', async () => {
|
||||
returnedObject = '{ user { name } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
user: {
|
||||
name: 'abusive-user',
|
||||
},
|
||||
},
|
||||
})
|
||||
it('returns a date', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('reported resource is a post', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p23',
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
categoryIds,
|
||||
})
|
||||
variables = {
|
||||
id: 'p23',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns type "Post"', async () => {
|
||||
returnedObject = '{ type }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
type: 'Post',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns resource in post attribute', async () => {
|
||||
returnedObject = '{ post { title } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
post: {
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null in user attribute', async () => {
|
||||
returnedObject = '{ user { name } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
user: null,
|
||||
},
|
||||
})
|
||||
it('returns the reason category', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||
}
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
reasonCategory: 'my_category',
|
||||
}
|
||||
await expect(action()).rejects.toThrow(
|
||||
'got invalid value "my_category"; Expected type ReasonCategory',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the reason description', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
reasonDescription: 'My reason!',
|
||||
}
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
reasonDescription: 'My reason!',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitize the reason description', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
reasonDescription: 'My reason <sanitize></sanitize>!',
|
||||
}
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
reasonDescription: 'My reason !',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reported resource is a post', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p23',
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
categoryIds,
|
||||
})
|
||||
variables = {
|
||||
...variables,
|
||||
resourceId: 'p23',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns type "Post"', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
type: 'Post',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns resource in post attribute', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
post: {
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null in user attribute', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
user: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the p23 again, but this time there must be an error message. */
|
||||
|
||||
describe('reported resource is a comment', () => {
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
id: 'p1',
|
||||
title: 'post to comment on',
|
||||
content: 'please comment on me',
|
||||
categoryIds,
|
||||
}
|
||||
await factory.create('Post', { ...createPostVariables, author: user })
|
||||
await factory.create('Comment', {
|
||||
author: user,
|
||||
postId: 'p1',
|
||||
id: 'c34',
|
||||
content: 'Robert getting tired.',
|
||||
})
|
||||
variables = {
|
||||
id: 'c34',
|
||||
}
|
||||
describe('reported resource is a comment', () => {
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
id: 'p1',
|
||||
title: 'post to comment on',
|
||||
content: 'please comment on me',
|
||||
categoryIds,
|
||||
}
|
||||
await factory.create('Post', { ...createPostVariables, author: user })
|
||||
await factory.create('Comment', {
|
||||
author: user,
|
||||
postId: 'p1',
|
||||
id: 'c34',
|
||||
content: 'Robert getting tired.',
|
||||
})
|
||||
variables = {
|
||||
...variables,
|
||||
resourceId: 'c34',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns type "Comment"', async () => {
|
||||
returnedObject = '{ type }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
type: 'Comment',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns resource in comment attribute', async () => {
|
||||
returnedObject = '{ comment { content } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: {
|
||||
comment: {
|
||||
content: 'Robert getting tired.',
|
||||
},
|
||||
},
|
||||
})
|
||||
it('returns type "Comment"', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
type: 'Comment',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
it('returns resource in comment attribute', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: {
|
||||
comment: {
|
||||
content: 'Robert getting tired.',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the c34 again, but this time there must be an error message. */
|
||||
|
||||
describe('reported resource is a tag', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Tag', {
|
||||
id: 't23',
|
||||
})
|
||||
variables = {
|
||||
id: 't23',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns null', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: null,
|
||||
})
|
||||
describe('reported resource is a tag', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Tag', {
|
||||
id: 't23',
|
||||
})
|
||||
variables = {
|
||||
...variables,
|
||||
resourceId: 't23',
|
||||
}
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the t23 again, but this time there must be an error message. */
|
||||
it('returns null', async () => {
|
||||
await expect(action()).resolves.toMatchObject({
|
||||
report: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the t23 again, but this time there must be an error message. */
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reports query', () => {
|
||||
let query, mutate, authenticatedUser, moderator, user, author
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const reportMutation = gql`
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
type
|
||||
}
|
||||
}
|
||||
`
|
||||
const reportsQuery = gql`
|
||||
query {
|
||||
reports(orderBy: createdAt_desc) {
|
||||
createdAt
|
||||
reasonCategory
|
||||
reasonDescription
|
||||
submitter {
|
||||
id
|
||||
}
|
||||
type
|
||||
user {
|
||||
id
|
||||
}
|
||||
post {
|
||||
id
|
||||
}
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = null
|
||||
|
||||
moderator = await factory.create('User', {
|
||||
id: 'mod1',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
user = await factory.create('User', {
|
||||
id: 'user1',
|
||||
role: 'user',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
author = await factory.create('User', {
|
||||
id: 'auth1',
|
||||
role: 'user',
|
||||
name: 'abusive-user',
|
||||
email: 'abusive-user@example.org',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
factory.create('Post', {
|
||||
author,
|
||||
id: 'p1',
|
||||
categoryIds,
|
||||
content: 'Interesting Knowledge',
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: moderator,
|
||||
id: 'p2',
|
||||
categoryIds,
|
||||
content: 'More things to do …',
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: user,
|
||||
id: 'p3',
|
||||
categoryIds,
|
||||
content: 'I am at school …',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
author: user,
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await user.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
resourceId: 'p1',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
resourceId: 'c1',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
resourceId: 'auth1',
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
},
|
||||
}),
|
||||
])
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('role "user" gets no reports', async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('role "moderator" gets reports', async () => {
|
||||
const expected = {
|
||||
// to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ
|
||||
reports: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
submitter: expect.objectContaining({
|
||||
id: 'user1',
|
||||
}),
|
||||
type: 'User',
|
||||
user: expect.objectContaining({
|
||||
id: 'auth1',
|
||||
}),
|
||||
post: null,
|
||||
comment: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
submitter: expect.objectContaining({
|
||||
id: 'user1',
|
||||
}),
|
||||
type: 'Post',
|
||||
user: null,
|
||||
post: expect.objectContaining({
|
||||
id: 'p1',
|
||||
}),
|
||||
comment: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
submitter: expect.objectContaining({
|
||||
id: 'user1',
|
||||
}),
|
||||
type: 'Comment',
|
||||
user: null,
|
||||
post: null,
|
||||
comment: expect.objectContaining({
|
||||
id: 'c1',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}
|
||||
|
||||
authenticatedUser = await moderator.toJson()
|
||||
const { data } = await query({ query: reportsQuery })
|
||||
expect(data).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -24,7 +24,6 @@ type Mutation {
|
||||
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
||||
report(id: ID!, description: String): Report
|
||||
disable(id: ID!): ID
|
||||
enable(id: ID!): ID
|
||||
# Shout the given Type and ID
|
||||
@ -35,18 +34,6 @@ type Mutation {
|
||||
unfollowUser(id: ID!): User
|
||||
}
|
||||
|
||||
type Report {
|
||||
id: ID!
|
||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
||||
description: String
|
||||
type: String!
|
||||
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
|
||||
createdAt: String
|
||||
comment: Comment @relation(name: "REPORTED", direction: "OUT")
|
||||
post: Post @relation(name: "REPORTED", direction: "OUT")
|
||||
user: User @relation(name: "REPORTED", direction: "OUT")
|
||||
}
|
||||
|
||||
enum Deletable {
|
||||
Post
|
||||
Comment
|
||||
|
||||
42
backend/src/schema/types/type/REPORTED.gql
Normal file
42
backend/src/schema/types/type/REPORTED.gql
Normal file
@ -0,0 +1,42 @@
|
||||
type REPORTED {
|
||||
createdAt: String
|
||||
reasonCategory: ReasonCategory
|
||||
reasonDescription: String
|
||||
submitter: User
|
||||
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user")
|
||||
# not yet supported
|
||||
# resource: ReportResource
|
||||
# @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource")
|
||||
type: String
|
||||
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]")
|
||||
user: User
|
||||
post: Post
|
||||
comment: Comment
|
||||
}
|
||||
|
||||
# this list equals the strings of an array in file "webapp/constants/modals.js"
|
||||
enum ReasonCategory {
|
||||
other
|
||||
discrimination_etc
|
||||
pornographic_content_links
|
||||
glorific_trivia_of_cruel_inhuman_acts
|
||||
doxing
|
||||
intentional_intimidation_stalking_persecution
|
||||
advert_products_services_commercial
|
||||
criminal_behavior_violation_german_law
|
||||
}
|
||||
|
||||
# not yet supported
|
||||
# union ReportResource = User | Post | Comment
|
||||
|
||||
enum ReportOrdering {
|
||||
createdAt_desc
|
||||
}
|
||||
|
||||
type Query {
|
||||
reports(orderBy: ReportOrdering): [REPORTED]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED
|
||||
}
|
||||
@ -647,10 +647,15 @@ import { gql } from '../jest/helpers'
|
||||
])
|
||||
authenticatedUser = null
|
||||
|
||||
// There is no error logged or the 'try' fails if this mutation is wrong. Why?
|
||||
const reportMutation = gql`
|
||||
mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
type
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -659,22 +664,25 @@ import { gql } from '../jest/helpers'
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: 'This comment is bigoted',
|
||||
id: 'c1',
|
||||
resourceId: 'c1',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: 'This post is bigoted',
|
||||
id: 'p1',
|
||||
resourceId: 'p1',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: 'This user is harassing me with bigoted remarks',
|
||||
id: 'u1',
|
||||
resourceId: 'u1',
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@ -107,6 +107,11 @@ Then(`I can't see the moderation menu item`, () => {
|
||||
When(/^I confirm the reporting dialog .*:$/, message => {
|
||||
cy.contains(message) // wait for element to become visible
|
||||
cy.get('.ds-modal').within(() => {
|
||||
cy.get('.ds-radio-option-label')
|
||||
.first()
|
||||
.click({
|
||||
force: true
|
||||
})
|
||||
cy.get('button')
|
||||
.contains('Report')
|
||||
.click()
|
||||
@ -114,21 +119,22 @@ When(/^I confirm the reporting dialog .*:$/, message => {
|
||||
})
|
||||
|
||||
Given('somebody reported the following posts:', table => {
|
||||
table.hashes().forEach(({ id }) => {
|
||||
table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
|
||||
const submitter = {
|
||||
email: `submitter${id}@example.org`,
|
||||
email: submitterEmail,
|
||||
password: '1234'
|
||||
}
|
||||
cy.factory()
|
||||
.create('User', submitter)
|
||||
.authenticateAs(submitter)
|
||||
.mutate(`mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||
type
|
||||
}
|
||||
}`, {
|
||||
id,
|
||||
description: 'Offensive content'
|
||||
resourceId,
|
||||
reasonCategory,
|
||||
reasonDescription
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -50,8 +50,8 @@ Feature: Report and Moderate
|
||||
|
||||
Scenario: Review reported content
|
||||
Given somebody reported the following posts:
|
||||
| id |
|
||||
| p1 |
|
||||
| submitterEmail | resourceId | reasonCategory | reasonDescription |
|
||||
| p1.submitter@example.org | p1 | discrimination_etc | Offensive content |
|
||||
And I am logged in with a "moderator" role
|
||||
When I click on the avatar menu in the top right corner
|
||||
And I click on "Moderation"
|
||||
@ -60,8 +60,8 @@ Feature: Report and Moderate
|
||||
|
||||
Scenario: Review reported posts of a user who's blocked a moderator
|
||||
Given somebody reported the following posts:
|
||||
| id |
|
||||
| p2 |
|
||||
| submitterEmail | resourceId | reasonCategory | reasonDescription |
|
||||
| p2.submitter@example.org | p2 | other | Offensive content |
|
||||
And my user account has the role "moderator"
|
||||
And there is an annoying user who has blocked me
|
||||
And I am logged in
|
||||
|
||||
26
neo4j/change_report_node_to_relationship.sh
Executable file
26
neo4j/change_report_node_to_relationship.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ENV_FILE=$(dirname "$0")/.env
|
||||
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
|
||||
|
||||
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
|
||||
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
|
||||
echo "Database manipulation is not possible without connecting to the database."
|
||||
echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container"
|
||||
fi
|
||||
|
||||
until echo 'RETURN "Connection successful" as info;' | cypher-shell
|
||||
do
|
||||
echo "Connecting to neo4j failed, trying again..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "
|
||||
MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource)
|
||||
DETACH DELETE report
|
||||
CREATE (submitter)-[reported:REPORTED]->(resource)
|
||||
SET reported.createdAt = toString(datetime())
|
||||
SET reported.reasonCategory = 'other'
|
||||
SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! createdAt is when the database manipulation happened.'
|
||||
RETURN reported;
|
||||
" | cypher-shell
|
||||
@ -1 +1 @@
|
||||
Subproject commit d46fc1570c6bcea328ae4cc3a4892745edea7319
|
||||
Subproject commit 808b3c5a9523505cb80b20b50348d29ba9932845
|
||||
@ -20,7 +20,7 @@ describe('ReportModal.vue', () => {
|
||||
id: 'c43',
|
||||
}
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$t: jest.fn(a => a),
|
||||
$filters: {
|
||||
truncate: a => a,
|
||||
},
|
||||
@ -29,7 +29,9 @@ describe('ReportModal.vue', () => {
|
||||
error: () => {},
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue(),
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
data: {},
|
||||
}),
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -154,6 +156,7 @@ describe('ReportModal.vue', () => {
|
||||
|
||||
describe('click confirm button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.ds-radio-option-label').trigger('click')
|
||||
wrapper.find('button.confirm').trigger('click')
|
||||
})
|
||||
|
||||
|
||||
@ -8,14 +8,36 @@
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template slot="footer">
|
||||
<ds-button class="cancel" icon="close" @click="cancel">{{ $t('report.cancel') }}</ds-button>
|
||||
<ds-radio
|
||||
v-model="form.reasonCategory"
|
||||
:schema="formSchema.reasonCategory"
|
||||
:label="$t('report.reason.category.label')"
|
||||
:options="form.reasonCategoryOptions"
|
||||
labelProp="label"
|
||||
/>
|
||||
<ds-input
|
||||
class="reason-description"
|
||||
v-model="form.reasonDescription"
|
||||
:schema="formSchema.reasonDescription"
|
||||
:label="$t('report.reason.description.label')"
|
||||
:placeholder="$t('report.reason.description.placeholder')"
|
||||
type="textarea"
|
||||
rows="5"
|
||||
/>
|
||||
<small class="smallTag">
|
||||
{{ form.reasonDescription.length }}/{{ formSchema.reasonDescription.max }}
|
||||
</small>
|
||||
<ds-space />
|
||||
<template #footer>
|
||||
<ds-button class="cancel" icon="close" @click="cancel">
|
||||
{{ $t('report.cancel') }}
|
||||
</ds-button>
|
||||
|
||||
<ds-button
|
||||
danger
|
||||
class="confirm"
|
||||
icon="exclamation-circle"
|
||||
:disabled="!form.reasonCategory"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
@ -26,8 +48,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
import { reportMutation } from '~/graphql/Moderation.js'
|
||||
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
|
||||
import validReport from '~/components/utils/ReportModal'
|
||||
|
||||
export default {
|
||||
name: 'ReportModal',
|
||||
@ -44,8 +68,21 @@ export default {
|
||||
isOpen: true,
|
||||
success: false,
|
||||
loading: false,
|
||||
form: {
|
||||
reasonCategory: null,
|
||||
reasonCategoryOptions: [],
|
||||
reasonDescription: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.form.reasonCategoryOptions = valuesReasonCategoryOptions.map(reasonCategory => {
|
||||
return {
|
||||
label: this.$t('report.reason.category.options.' + reasonCategory),
|
||||
value: reasonCategory,
|
||||
}
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.$t(`report.${this.type}.title`)
|
||||
@ -54,6 +91,12 @@ export default {
|
||||
const name = this.$filters.truncate(this.name, 30)
|
||||
return this.$t(`report.${this.type}.message`, { name })
|
||||
},
|
||||
formSchema() {
|
||||
const validReportSchema = validReport({ translate: this.$t })
|
||||
return {
|
||||
...validReportSchema.formSchema,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async cancel() {
|
||||
@ -65,52 +108,74 @@ export default {
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
const { reasonCategory, reasonDescription } = this.form
|
||||
this.loading = true
|
||||
try {
|
||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($id: ID!) {
|
||||
report(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: this.id },
|
||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: reportMutation(),
|
||||
variables: {
|
||||
resourceId: this.id,
|
||||
reasonCategory: reasonCategory.value,
|
||||
reasonDescription,
|
||||
},
|
||||
})
|
||||
this.success = true
|
||||
this.$toast.success(this.$t('report.success'))
|
||||
setTimeout(() => {
|
||||
this.isOpen = false
|
||||
.then(({ _data }) => {
|
||||
this.success = true
|
||||
this.$toast.success(this.$t('report.success'))
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
this.$emit('close')
|
||||
}, 500)
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
this.$emit('close')
|
||||
this.success = false
|
||||
switch (err.message) {
|
||||
case 'GraphQL error: User':
|
||||
this.$toast.error(this.$t('report.user.error'))
|
||||
break
|
||||
case 'GraphQL error: Post':
|
||||
this.$toast.error(this.$t('report.contribution.error'))
|
||||
break
|
||||
case 'GraphQL error: Comment':
|
||||
this.$toast.error(this.$t('report.comment.error'))
|
||||
break
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
this.$emit('close')
|
||||
}, 500)
|
||||
}, 1500)
|
||||
this.loading = false
|
||||
})
|
||||
.catch(err => {
|
||||
this.$emit('close')
|
||||
this.success = false
|
||||
switch (err.message) {
|
||||
case 'GraphQL error: User':
|
||||
this.$toast.error(this.$t('report.user.error'))
|
||||
break
|
||||
case 'GraphQL error: Post':
|
||||
this.$toast.error(this.$t('report.contribution.error'))
|
||||
break
|
||||
case 'GraphQL error: Comment':
|
||||
this.$toast.error(this.$t('report.comment.error'))
|
||||
break
|
||||
default:
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ds-modal {
|
||||
max-width: 600px !important;
|
||||
}
|
||||
.ds-radio-option:not(.ds-button) {
|
||||
width: 100% !important;
|
||||
}
|
||||
.ds-radio-option-label {
|
||||
margin: 5px 20px 5px 5px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.reason-description {
|
||||
margin-top: $space-x-small !important;
|
||||
margin-bottom: $space-xx-small !important;
|
||||
}
|
||||
.smallTag {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
left: 90%;
|
||||
}
|
||||
.hc-modal-success {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
||||
25
webapp/components/utils/ReportModal.js
Normal file
25
webapp/components/utils/ReportModal.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
|
||||
|
||||
export default function validReport({ translate }) {
|
||||
return {
|
||||
formSchema: {
|
||||
reasonCategory: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
fields: {
|
||||
value: {
|
||||
type: 'enum',
|
||||
enum: valuesReasonCategoryOptions,
|
||||
required: true,
|
||||
message: translate('report.reason.category.invalid'),
|
||||
},
|
||||
},
|
||||
},
|
||||
reasonDescription: {
|
||||
type: 'string',
|
||||
min: 0,
|
||||
max: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
36
webapp/components/utils/ReportModal.spec.js
Normal file
36
webapp/components/utils/ReportModal.spec.js
Normal file
@ -0,0 +1,36 @@
|
||||
import validReport from './ReportModal'
|
||||
import Schema from 'async-validator'
|
||||
|
||||
let translate
|
||||
|
||||
beforeEach(() => {
|
||||
translate = jest.fn(() => 'Validation error')
|
||||
})
|
||||
|
||||
describe('validReport', () => {
|
||||
let validate = object => {
|
||||
const { formSchema } = validReport({ translate })
|
||||
const validator = new Schema(formSchema)
|
||||
return validator.validate(object, { suppressWarning: true }).catch(({ errors }) => {
|
||||
throw new Error(errors[0].message)
|
||||
})
|
||||
}
|
||||
|
||||
describe('reasonCategory', () => {
|
||||
describe('invalid enum', () => {
|
||||
it('rejects', async () => {
|
||||
await expect(validate({ reasonCategory: { value: 'invalid_enum' } })).rejects.toThrow(
|
||||
'Validation error',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid enum', () => {
|
||||
it('resolves', async () => {
|
||||
await expect(
|
||||
validate({ reasonCategory: { value: 'discrimination_etc' } }),
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
11
webapp/constants/modals.js
Normal file
11
webapp/constants/modals.js
Normal file
@ -0,0 +1,11 @@
|
||||
// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/REPORTED.gql"
|
||||
export const valuesReasonCategoryOptions = [
|
||||
'discrimination_etc',
|
||||
'pornographic_content_links',
|
||||
'glorific_trivia_of_cruel_inhuman_acts',
|
||||
'doxing',
|
||||
'intentional_intimidation_stalking_persecution',
|
||||
'advert_products_services_commercial',
|
||||
'criminal_behavior_violation_german_law',
|
||||
'other',
|
||||
]
|
||||
@ -1,13 +1,14 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default app => {
|
||||
export const reportListQuery = () => {
|
||||
// no limit vor the moment like before: "reports(first: 20, orderBy: createdAt_desc)"
|
||||
return gql`
|
||||
query {
|
||||
Report(first: 20, orderBy: createdAt_desc) {
|
||||
id
|
||||
description
|
||||
type
|
||||
reports(orderBy: createdAt_desc) {
|
||||
createdAt
|
||||
reasonCategory
|
||||
reasonDescription
|
||||
type
|
||||
submitter {
|
||||
id
|
||||
slug
|
||||
@ -79,3 +80,17 @@ export default app => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const reportMutation = () => {
|
||||
return gql`
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
type
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -432,8 +432,11 @@
|
||||
"reports": {
|
||||
"empty": "Glückwunsch, es gibt nichts zu moderieren.",
|
||||
"name": "Meldungen",
|
||||
"submitter": "gemeldet von",
|
||||
"disabledBy": "deaktiviert von"
|
||||
"reasonCategory": "Kategorie",
|
||||
"reasonDescription": "Beschreibung",
|
||||
"createdAt": "Datum",
|
||||
"submitter": "Gemeldet von",
|
||||
"disabledBy": "Deaktiviert von"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
@ -493,6 +496,27 @@
|
||||
"type": "Kommentar",
|
||||
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?",
|
||||
"error": "Du hast den Kommentar bereits gemeldet!"
|
||||
},
|
||||
"reason": {
|
||||
"category": {
|
||||
"label": "Wähle eine Kategorie:",
|
||||
"placeholder": "Kategorie …",
|
||||
"options": {
|
||||
"discrimination_etc": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen.",
|
||||
"pornographic_content_links": "Das Posten oder Verlinken eindeutig pornografischen Materials.",
|
||||
"glorific_trivia_of_cruel_inhuman_acts": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
|
||||
"doxing": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen (\"Doxing\").",
|
||||
"intentional_intimidation_stalking_persecution": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||
"other": "Andere …"
|
||||
},
|
||||
"invalid": "Bitte wähle eine gültige Kategorie aus"
|
||||
},
|
||||
"description": {
|
||||
"label": "Bitte erkläre: Warum möchtest du dies melden?",
|
||||
"placeholder": "Zusätzliche Information …"
|
||||
}
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
|
||||
@ -433,8 +433,11 @@
|
||||
"reports": {
|
||||
"empty": "Congratulations, nothing to moderate.",
|
||||
"name": "Reports",
|
||||
"submitter": "reported by",
|
||||
"disabledBy": "disabled by"
|
||||
"reasonCategory": "Category",
|
||||
"reasonDescription": "Description",
|
||||
"createdAt": "Date",
|
||||
"submitter": "Reported by",
|
||||
"disabledBy": "Disabled by"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
@ -494,6 +497,27 @@
|
||||
"type": "Comment",
|
||||
"message": "Do you really want to report the comment from \"<b>{name}</b>\"?",
|
||||
"error": "You have already reported the comment!"
|
||||
},
|
||||
"reason": {
|
||||
"category": {
|
||||
"label": "Select a category:",
|
||||
"placeholder": "Category …",
|
||||
"options": {
|
||||
"discrimination_etc": "Discriminatory posts, comments, utterances or insults.",
|
||||
"pornographic_content_links": "Posting or linking of clearly pornographic material.",
|
||||
"glorific_trivia_of_cruel_inhuman_acts": "Glorification or trivialization of cruel or inhuman acts of violence.",
|
||||
"doxing": "The disclosure of others' personal information without their consent or threat there of (\"doxing\").",
|
||||
"intentional_intimidation_stalking_persecution": "Intentional intimidation, stalking or persecution.",
|
||||
"advert_products_services_commercial": "Advertising products and services with commercial intent.",
|
||||
"criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.",
|
||||
"other": "Other …"
|
||||
},
|
||||
"invalid": "Please select a valid category"
|
||||
},
|
||||
"description": {
|
||||
"label": "Please explain: Why you like to report this?",
|
||||
"placeholder": "Additional information …"
|
||||
}
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
|
||||
@ -1,8 +1,29 @@
|
||||
<template>
|
||||
<ds-card space="small">
|
||||
<ds-heading tag="h3">{{ $t('moderation.reports.name') }}</ds-heading>
|
||||
<ds-table v-if="Report && Report.length" :data="Report" :fields="fields" condensed>
|
||||
<template slot="name" slot-scope="scope">
|
||||
<ds-table v-if="reports && reports.length" :data="reports" :fields="fields" condensed>
|
||||
<!-- Icon -->
|
||||
<template slot="type" slot-scope="scope">
|
||||
<ds-text color="soft">
|
||||
<ds-icon
|
||||
v-if="scope.row.type === 'Post'"
|
||||
v-tooltip="{ content: $t('report.contribution.type'), placement: 'right' }"
|
||||
name="bookmark"
|
||||
/>
|
||||
<ds-icon
|
||||
v-else-if="scope.row.type === 'Comment'"
|
||||
v-tooltip="{ content: $t('report.comment.type'), placement: 'right' }"
|
||||
name="comments"
|
||||
/>
|
||||
<ds-icon
|
||||
v-else-if="scope.row.type === 'User'"
|
||||
v-tooltip="{ content: $t('report.user.type'), placement: 'right' }"
|
||||
name="user"
|
||||
/>
|
||||
</ds-text>
|
||||
</template>
|
||||
<!-- reported user or content -->
|
||||
<template slot="reportedUserContent" slot-scope="scope">
|
||||
<div v-if="scope.row.type === 'Post'">
|
||||
<nuxt-link
|
||||
:to="{
|
||||
@ -42,25 +63,15 @@
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="type" slot-scope="scope">
|
||||
<ds-text color="soft">
|
||||
<ds-icon
|
||||
v-if="scope.row.type === 'Post'"
|
||||
v-tooltip="{ content: $t('report.contribution.type'), placement: 'right' }"
|
||||
name="bookmark"
|
||||
/>
|
||||
<ds-icon
|
||||
v-else-if="scope.row.type === 'Comment'"
|
||||
v-tooltip="{ content: $t('report.comment.type'), placement: 'right' }"
|
||||
name="comments"
|
||||
/>
|
||||
<ds-icon
|
||||
v-else-if="scope.row.type === 'User'"
|
||||
v-tooltip="{ content: $t('report.user.type'), placement: 'right' }"
|
||||
name="user"
|
||||
/>
|
||||
</ds-text>
|
||||
<!-- reasonCategory -->
|
||||
<template slot="reasonCategory" slot-scope="scope">
|
||||
{{ $t('report.reason.category.options.' + scope.row.reasonCategory) }}
|
||||
</template>
|
||||
<!-- reasonDescription -->
|
||||
<template slot="reasonDescription" slot-scope="scope">
|
||||
{{ scope.row.reasonDescription }}
|
||||
</template>
|
||||
<!-- submitter -->
|
||||
<template slot="submitter" slot-scope="scope">
|
||||
<nuxt-link
|
||||
:to="{
|
||||
@ -71,6 +82,15 @@
|
||||
{{ scope.row.submitter.name }}
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<!-- createdAt -->
|
||||
<template slot="createdAt" slot-scope="scope">
|
||||
<ds-text size="small">
|
||||
<client-only>
|
||||
<hc-relative-date-time :date-time="scope.row.createdAt" />
|
||||
</client-only>
|
||||
</ds-text>
|
||||
</template>
|
||||
<!-- disabledBy -->
|
||||
<template slot="disabledBy" slot-scope="scope">
|
||||
<nuxt-link
|
||||
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
|
||||
@ -102,6 +122,7 @@
|
||||
>
|
||||
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
|
||||
</nuxt-link>
|
||||
<b v-else>—</b>
|
||||
</template>
|
||||
</ds-table>
|
||||
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
||||
@ -110,31 +131,36 @@
|
||||
|
||||
<script>
|
||||
import HcEmpty from '~/components/Empty.vue'
|
||||
import query from '~/graphql/ModerationListQuery.js'
|
||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||
import { reportListQuery } from '~/graphql/Moderation.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HcEmpty,
|
||||
HcRelativeDateTime,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Report: [],
|
||||
reports: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return {
|
||||
type: ' ',
|
||||
name: ' ',
|
||||
reportedUserContent: ' ',
|
||||
reasonCategory: this.$t('moderation.reports.reasonCategory'),
|
||||
reasonDescription: this.$t('moderation.reports.reasonDescription'),
|
||||
submitter: this.$t('moderation.reports.submitter'),
|
||||
createdAt: this.$t('moderation.reports.createdAt'),
|
||||
disabledBy: this.$t('moderation.reports.disabledBy'),
|
||||
// actions: ' '
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
Report: {
|
||||
query,
|
||||
reports: {
|
||||
query: reportListQuery(),
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user