mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #1954 from Human-Connection/1710-list-and-protocol-moderation
List and protocol moderation
This commit is contained in:
commit
4010635f2b
@ -112,7 +112,7 @@ export default shield(
|
|||||||
CreatePost: isAuthenticated,
|
CreatePost: isAuthenticated,
|
||||||
UpdatePost: isAuthor,
|
UpdatePost: isAuthor,
|
||||||
DeletePost: isAuthor,
|
DeletePost: isAuthor,
|
||||||
report: isAuthenticated,
|
fileReport: isAuthenticated,
|
||||||
CreateSocialMedia: isAuthenticated,
|
CreateSocialMedia: isAuthenticated,
|
||||||
UpdateSocialMedia: isMySocialMedia,
|
UpdateSocialMedia: isMySocialMedia,
|
||||||
DeleteSocialMedia: isMySocialMedia,
|
DeleteSocialMedia: isMySocialMedia,
|
||||||
@ -125,8 +125,7 @@ export default shield(
|
|||||||
shout: isAuthenticated,
|
shout: isAuthenticated,
|
||||||
unshout: isAuthenticated,
|
unshout: isAuthenticated,
|
||||||
changePassword: isAuthenticated,
|
changePassword: isAuthenticated,
|
||||||
enable: isModerator,
|
review: isModerator,
|
||||||
disable: isModerator,
|
|
||||||
CreateComment: isAuthenticated,
|
CreateComment: isAuthenticated,
|
||||||
UpdateComment: isAuthor,
|
UpdateComment: isAuthor,
|
||||||
DeleteComment: isAuthor,
|
DeleteComment: isAuthor,
|
||||||
|
|||||||
@ -8,14 +8,8 @@ const factory = Factory()
|
|||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|
||||||
let query
|
|
||||||
let mutate
|
|
||||||
let graphqlQuery
|
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
let authenticatedUser
|
let query, graphqlQuery, authenticatedUser, user, moderator, troll
|
||||||
let user
|
|
||||||
let moderator
|
|
||||||
let troll
|
|
||||||
|
|
||||||
const action = () => {
|
const action = () => {
|
||||||
return query({ query: graphqlQuery })
|
return query({ query: graphqlQuery })
|
||||||
@ -38,18 +32,17 @@ beforeAll(async () => {
|
|||||||
avatar: '/some/offensive/avatar.jpg',
|
avatar: '/some/offensive/avatar.jpg',
|
||||||
about: 'This self description is very offensive',
|
about: 'This self description is very offensive',
|
||||||
}),
|
}),
|
||||||
|
neode.create('Category', {
|
||||||
|
id: 'cat9',
|
||||||
|
name: 'Democracy & Politics',
|
||||||
|
icon: 'university',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
user = users[0]
|
user = users[0]
|
||||||
moderator = users[1]
|
moderator = users[1]
|
||||||
troll = users[2]
|
troll = users[2]
|
||||||
|
|
||||||
await neode.create('Category', {
|
|
||||||
id: 'cat9',
|
|
||||||
name: 'Democracy & Politics',
|
|
||||||
icon: 'university',
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
user.relateTo(troll, 'following'),
|
user.relateTo(troll, 'following'),
|
||||||
factory.create('Post', {
|
factory.create('Post', {
|
||||||
@ -70,33 +63,32 @@ beforeAll(async () => {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
const resources = await Promise.all([
|
||||||
factory.create('Comment', {
|
factory.create('Comment', {
|
||||||
author: user,
|
author: user,
|
||||||
id: 'c2',
|
id: 'c2',
|
||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
content: 'Enabled comment on public post',
|
content: 'Enabled comment on public post',
|
||||||
}),
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
id: 'p2',
|
||||||
|
author: troll,
|
||||||
|
title: 'Disabled post',
|
||||||
|
content: 'This is an offensive post content',
|
||||||
|
contentExcerpt: 'This is an offensive post content',
|
||||||
|
image: '/some/offensive/image.jpg',
|
||||||
|
deleted: false,
|
||||||
|
categoryIds,
|
||||||
|
}),
|
||||||
|
factory.create('Comment', {
|
||||||
|
id: 'c1',
|
||||||
|
author: troll,
|
||||||
|
postId: 'p3',
|
||||||
|
content: 'Disabled comment',
|
||||||
|
contentExcerpt: 'Disabled comment',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await factory.create('Post', {
|
|
||||||
id: 'p2',
|
|
||||||
author: troll,
|
|
||||||
title: 'Disabled post',
|
|
||||||
content: 'This is an offensive post content',
|
|
||||||
contentExcerpt: 'This is an offensive post content',
|
|
||||||
image: '/some/offensive/image.jpg',
|
|
||||||
deleted: false,
|
|
||||||
categoryIds,
|
|
||||||
})
|
|
||||||
await factory.create('Comment', {
|
|
||||||
id: 'c1',
|
|
||||||
author: troll,
|
|
||||||
postId: 'p3',
|
|
||||||
content: 'Disabled comment',
|
|
||||||
contentExcerpt: 'Disabled comment',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { server } = createServer({
|
const { server } = createServer({
|
||||||
context: () => {
|
context: () => {
|
||||||
return {
|
return {
|
||||||
@ -108,20 +100,57 @@ beforeAll(async () => {
|
|||||||
})
|
})
|
||||||
const client = createTestClient(server)
|
const client = createTestClient(server)
|
||||||
query = client.query
|
query = client.query
|
||||||
mutate = client.mutate
|
|
||||||
|
|
||||||
authenticatedUser = await moderator.toJson()
|
const trollingPost = resources[1]
|
||||||
const disableMutation = gql`
|
const trollingComment = resources[2]
|
||||||
mutation($id: ID!) {
|
|
||||||
disable(id: $id)
|
const reports = await Promise.all([
|
||||||
}
|
factory.create('Report'),
|
||||||
`
|
factory.create('Report'),
|
||||||
await Promise.all([
|
factory.create('Report'),
|
||||||
mutate({ mutation: disableMutation, variables: { id: 'c1' } }),
|
])
|
||||||
mutate({ mutation: disableMutation, variables: { id: 'u2' } }),
|
const reportAgainstTroll = reports[0]
|
||||||
mutate({ mutation: disableMutation, variables: { id: 'p2' } }),
|
const reportAgainstTrollingPost = reports[1]
|
||||||
|
const reportAgainstTrollingComment = reports[2]
|
||||||
|
|
||||||
|
const reportVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'I am what I am !!!',
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTroll.relateTo(user, 'filed', { ...reportVariables, resourceId: 'u2' }),
|
||||||
|
reportAgainstTroll.relateTo(troll, 'belongsTo'),
|
||||||
|
reportAgainstTrollingPost.relateTo(user, 'filed', { ...reportVariables, resourceId: 'p2' }),
|
||||||
|
reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'),
|
||||||
|
reportAgainstTrollingComment.relateTo(moderator, 'filed', {
|
||||||
|
...reportVariables,
|
||||||
|
resourceId: 'c1',
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const disableVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
disable: true,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTroll.relateTo(moderator, 'reviewed', { ...disableVariables, resourceId: 'u2' }),
|
||||||
|
troll.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
reportAgainstTrollingPost.relateTo(moderator, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'p2',
|
||||||
|
}),
|
||||||
|
trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
reportAgainstTrollingComment.relateTo(moderator, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'c1',
|
||||||
|
}),
|
||||||
|
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
])
|
])
|
||||||
authenticatedUser = null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@ -61,31 +61,58 @@ const validateUpdatePost = async (resolve, root, args, context, info) => {
|
|||||||
|
|
||||||
const validateReport = async (resolve, root, args, context, info) => {
|
const validateReport = async (resolve, root, args, context, info) => {
|
||||||
const { resourceId } = args
|
const { resourceId } = args
|
||||||
const { user, driver } = context
|
const { user } = context
|
||||||
if (resourceId === user.id) throw new Error('You cannot report yourself!')
|
if (resourceId === user.id) throw new Error('You cannot report yourself!')
|
||||||
|
return resolve(root, args, context, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateReview = async (resolve, root, args, context, info) => {
|
||||||
|
const { resourceId } = args
|
||||||
|
let existingReportedResource
|
||||||
|
const { user, driver } = context
|
||||||
|
if (resourceId === user.id) throw new Error('You cannot review yourself!')
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
const reportReadTxPromise = session.writeTransaction(async txc => {
|
||||||
const reportQueryRes = await session.run(
|
const validateReviewTransactionResponse = await txc.run(
|
||||||
`
|
`
|
||||||
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId})
|
MATCH (resource {id: $resourceId})
|
||||||
RETURN labels(resource)[0] as label
|
WHERE resource:User OR resource:Post OR resource:Comment
|
||||||
`,
|
OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource)
|
||||||
|
OPTIONAL MATCH (resource)<-[:WROTE]-(author:User)
|
||||||
|
RETURN labels(resource)[0] AS label, author, filed
|
||||||
|
`,
|
||||||
{
|
{
|
||||||
resourceId,
|
resourceId,
|
||||||
submitterId: user.id,
|
submitterId: user.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const [existingReportedResource] = reportQueryRes.records.map(record => {
|
return validateReviewTransactionResponse.records.map(record => ({
|
||||||
return {
|
label: record.get('label'),
|
||||||
label: record.get('label'),
|
author: record.get('author'),
|
||||||
}
|
filed: record.get('filed'),
|
||||||
})
|
}))
|
||||||
|
})
|
||||||
if (existingReportedResource) throw new Error(`${existingReportedResource.label}`)
|
try {
|
||||||
return resolve(root, args, context, info)
|
const txResult = await reportReadTxPromise
|
||||||
|
existingReportedResource = txResult
|
||||||
|
if (!existingReportedResource || !existingReportedResource.length)
|
||||||
|
throw new Error(`Resource not found or is not a Post|Comment|User!`)
|
||||||
|
existingReportedResource = existingReportedResource[0]
|
||||||
|
if (!existingReportedResource.filed)
|
||||||
|
throw new Error(
|
||||||
|
`Before starting the review process, please report the ${existingReportedResource.label}!`,
|
||||||
|
)
|
||||||
|
const authorId =
|
||||||
|
existingReportedResource.label !== 'User' && existingReportedResource.author
|
||||||
|
? existingReportedResource.author.properties.id
|
||||||
|
: null
|
||||||
|
if (authorId && authorId === user.id)
|
||||||
|
throw new Error(`You cannot review your own ${existingReportedResource.label}!`)
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resolve(root, args, context, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -94,6 +121,7 @@ export default {
|
|||||||
UpdateComment: validateUpdateComment,
|
UpdateComment: validateUpdateComment,
|
||||||
CreatePost: validatePost,
|
CreatePost: validatePost,
|
||||||
UpdatePost: validateUpdatePost,
|
UpdatePost: validateUpdatePost,
|
||||||
report: validateReport,
|
fileReport: validateReport,
|
||||||
|
review: validateReview,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,16 @@ import createServer from '../../server'
|
|||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
let mutate, authenticatedUser, user
|
let authenticatedUser,
|
||||||
|
mutate,
|
||||||
|
users,
|
||||||
|
offensivePost,
|
||||||
|
reportVariables,
|
||||||
|
disableVariables,
|
||||||
|
reportingUser,
|
||||||
|
moderatingUser,
|
||||||
|
commentingUser
|
||||||
|
|
||||||
const createCommentMutation = gql`
|
const createCommentMutation = gql`
|
||||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
@ -23,8 +32,14 @@ const updateCommentMutation = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
const createPostMutation = gql`
|
const createPostMutation = gql`
|
||||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
|
||||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
CreatePost(
|
||||||
|
id: $id
|
||||||
|
title: $title
|
||||||
|
content: $content
|
||||||
|
language: $language
|
||||||
|
categoryIds: $categoryIds
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,7 +52,25 @@ const updatePostMutation = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const reportMutation = gql`
|
||||||
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
|
fileReport(
|
||||||
|
resourceId: $resourceId
|
||||||
|
reasonCategory: $reasonCategory
|
||||||
|
reasonDescription: $reasonDescription
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const reviewMutation = gql`
|
||||||
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const { server } = createServer({
|
const { server } = createServer({
|
||||||
context: () => {
|
context: () => {
|
||||||
@ -52,13 +85,42 @@ beforeAll(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await factory.create('User', {
|
users = await Promise.all([
|
||||||
id: 'user-id',
|
factory.create('User', {
|
||||||
})
|
id: 'reporting-user',
|
||||||
await factory.create('Post', {
|
}),
|
||||||
id: 'post-4-commenting',
|
factory.create('User', {
|
||||||
authorId: 'user-id',
|
id: 'moderating-user',
|
||||||
})
|
role: 'moderator',
|
||||||
|
}),
|
||||||
|
factory.create('User', {
|
||||||
|
id: 'commenting-user',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
reportVariables = {
|
||||||
|
resourceId: 'whatever',
|
||||||
|
reasonCategory: 'other',
|
||||||
|
reasonDescription: 'Violates code of conduct !!!',
|
||||||
|
}
|
||||||
|
disableVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
disable: true,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
reportingUser = users[0]
|
||||||
|
moderatingUser = users[1]
|
||||||
|
commentingUser = users[2]
|
||||||
|
const posts = await Promise.all([
|
||||||
|
factory.create('Post', {
|
||||||
|
id: 'offensive-post',
|
||||||
|
authorId: 'moderating-user',
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
id: 'post-4-commenting',
|
||||||
|
authorId: 'commenting-user',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
offensivePost = posts[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -72,7 +134,7 @@ describe('validateCreateComment', () => {
|
|||||||
postId: 'whatever',
|
postId: 'whatever',
|
||||||
content: '',
|
content: '',
|
||||||
}
|
}
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await commentingUser.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if content is empty', async () => {
|
it('throws an error if content is empty', async () => {
|
||||||
@ -114,13 +176,13 @@ describe('validateCreateComment', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await factory.create('Comment', {
|
await factory.create('Comment', {
|
||||||
id: 'comment-id',
|
id: 'comment-id',
|
||||||
authorId: 'user-id',
|
authorId: 'commenting-user',
|
||||||
})
|
})
|
||||||
updateCommentVariables = {
|
updateCommentVariables = {
|
||||||
id: 'whatever',
|
id: 'whatever',
|
||||||
content: '',
|
content: '',
|
||||||
}
|
}
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await commentingUser.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if content is empty', async () => {
|
it('throws an error if content is empty', async () => {
|
||||||
@ -151,7 +213,7 @@ describe('validateCreateComment', () => {
|
|||||||
title: 'I am a title',
|
title: 'I am a title',
|
||||||
content: 'Some content',
|
content: 'Some content',
|
||||||
}
|
}
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await commentingUser.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('categories', () => {
|
describe('categories', () => {
|
||||||
@ -242,3 +304,97 @@ describe('validateCreateComment', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('validateReport', () => {
|
||||||
|
it('throws an error if a user tries to report themself', async () => {
|
||||||
|
authenticatedUser = await reportingUser.toJson()
|
||||||
|
reportVariables = { ...reportVariables, resourceId: 'reporting-user' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reportMutation, variables: reportVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { fileReport: null },
|
||||||
|
errors: [{ message: 'You cannot report yourself!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateReview', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const reportAgainstModerator = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstModerator.relateTo(reportingUser, 'filed', {
|
||||||
|
...reportVariables,
|
||||||
|
resourceId: 'moderating-user',
|
||||||
|
}),
|
||||||
|
reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'),
|
||||||
|
])
|
||||||
|
authenticatedUser = await moderatingUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if a user tries to review a report against them', async () => {
|
||||||
|
disableVariables = { ...disableVariables, resourceId: 'moderating-user' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: null },
|
||||||
|
errors: [{ message: 'You cannot review yourself!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error for invaild resource', async () => {
|
||||||
|
disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: null },
|
||||||
|
errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if no report exists', async () => {
|
||||||
|
disableVariables = { ...disableVariables, resourceId: 'offensive-post' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: null },
|
||||||
|
errors: [{ message: 'Before starting the review process, please report the Post!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => {
|
||||||
|
const reportAgainstOffensivePost = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstOffensivePost.relateTo(reportingUser, 'filed', {
|
||||||
|
...reportVariables,
|
||||||
|
resourceId: 'offensive-post',
|
||||||
|
}),
|
||||||
|
reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'),
|
||||||
|
])
|
||||||
|
disableVariables = { ...disableVariables, resourceId: 'offensive-post' }
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: null },
|
||||||
|
errors: [{ message: 'You cannot review your own Post!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moderate a resource that is not a (Comment|Post|User) ', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all([factory.create('Tag', { id: 'tag-id' })])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null', async () => {
|
||||||
|
disableVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'tag-id',
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: null },
|
||||||
|
errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -25,12 +25,6 @@ module.exports = {
|
|||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
disabledBy: {
|
|
||||||
type: 'relationship',
|
|
||||||
relationship: 'DISABLED',
|
|
||||||
target: 'User',
|
|
||||||
direction: 'in',
|
|
||||||
},
|
|
||||||
notified: {
|
notified: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'NOTIFIED',
|
relationship: 'NOTIFIED',
|
||||||
|
|||||||
@ -17,12 +17,6 @@ module.exports = {
|
|||||||
image: { type: 'string', allow: [null] },
|
image: { type: 'string', allow: [null] },
|
||||||
deleted: { type: 'boolean', default: false },
|
deleted: { type: 'boolean', default: false },
|
||||||
disabled: { type: 'boolean', default: false },
|
disabled: { type: 'boolean', default: false },
|
||||||
disabledBy: {
|
|
||||||
type: 'relationship',
|
|
||||||
relationship: 'DISABLED',
|
|
||||||
target: 'User',
|
|
||||||
direction: 'in',
|
|
||||||
},
|
|
||||||
notified: {
|
notified: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'NOTIFIED',
|
relationship: 'NOTIFIED',
|
||||||
|
|||||||
53
backend/src/models/Report.js
Normal file
53
backend/src/models/Report.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
rule: { type: 'string', default: 'latestReviewUpdatedAtRules' },
|
||||||
|
disable: { type: 'boolean', default: false },
|
||||||
|
closed: { type: 'boolean', default: false },
|
||||||
|
belongsTo: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'BELONGS_TO',
|
||||||
|
target: ['User', 'Comment', 'Post'],
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
filed: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'FILED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
properties: {
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
resourceId: { type: 'string', primary: true, default: uuid },
|
||||||
|
reasonCategory: {
|
||||||
|
type: 'string',
|
||||||
|
valid: [
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
invalid: [null],
|
||||||
|
},
|
||||||
|
reasonDescription: { type: 'string', allow: [null] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reviewed: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'REVIEWED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
properties: {
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
disable: { type: 'boolean', default: false },
|
||||||
|
closed: { type: 'boolean', default: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -42,12 +42,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' },
|
friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' },
|
||||||
disabledBy: {
|
|
||||||
type: 'relationship',
|
|
||||||
relationship: 'DISABLED',
|
|
||||||
target: 'User',
|
|
||||||
direction: 'in',
|
|
||||||
},
|
|
||||||
rewarded: {
|
rewarded: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'REWARDED',
|
relationship: 'REWARDED',
|
||||||
|
|||||||
@ -12,4 +12,5 @@ export default {
|
|||||||
Tag: require('./Tag.js'),
|
Tag: require('./Tag.js'),
|
||||||
Location: require('./Location.js'),
|
Location: require('./Location.js'),
|
||||||
Donations: require('./Donations.js'),
|
Donations: require('./Donations.js'),
|
||||||
|
Report: require('./Report.js'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,9 @@ export default makeAugmentedSchema({
|
|||||||
'Location',
|
'Location',
|
||||||
'SocialMedia',
|
'SocialMedia',
|
||||||
'NOTIFIED',
|
'NOTIFIED',
|
||||||
'REPORTED',
|
'FILED',
|
||||||
|
'REVIEWED',
|
||||||
|
'Report',
|
||||||
'Donations',
|
'Donations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -78,7 +78,6 @@ export default {
|
|||||||
hasOne: {
|
hasOne: {
|
||||||
author: '<-[:WROTE]-(related:User)',
|
author: '<-[:WROTE]-(related:User)',
|
||||||
post: '-[:COMMENTS]->(related:Post)',
|
post: '-[:COMMENTS]->(related:Post)',
|
||||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,47 +1,51 @@
|
|||||||
|
const transformReturnType = record => {
|
||||||
|
return {
|
||||||
|
...record.get('review').properties,
|
||||||
|
report: record.get('report').properties,
|
||||||
|
resource: {
|
||||||
|
__typename: record.get('type'),
|
||||||
|
...record.get('resource').properties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
disable: async (object, params, { user, driver }) => {
|
review: async (_object, params, context, _resolveInfo) => {
|
||||||
const { id } = params
|
const { user: moderator, driver } = context
|
||||||
const { id: userId } = user
|
|
||||||
const cypher = `
|
let createdRelationshipWithNestedAttributes = null // return value
|
||||||
MATCH (u:User {id: $userId})
|
|
||||||
MATCH (resource {id: $id})
|
|
||||||
WHERE resource:User OR resource:Comment OR resource:Post
|
|
||||||
SET resource.disabled = true
|
|
||||||
MERGE (resource)<-[:DISABLED]-(u)
|
|
||||||
RETURN resource {.id}
|
|
||||||
`
|
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
const res = await session.run(cypher, { id, userId })
|
const cypher = `
|
||||||
const [resource] = res.records.map(record => {
|
MATCH (moderator:User {id: $moderatorId})
|
||||||
return record.get('resource')
|
MATCH (resource {id: $params.resourceId})<-[:BELONGS_TO]-(report:Report {closed: false})
|
||||||
|
WHERE resource:User OR resource:Post OR resource:Comment
|
||||||
|
MERGE (report)<-[review:REVIEWED]-(moderator)
|
||||||
|
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
||||||
|
ON MATCH SET review.updatedAt = $dateTime
|
||||||
|
SET review.disable = $params.disable
|
||||||
|
SET report.updatedAt = $dateTime, report.closed = $params.closed
|
||||||
|
SET resource.disabled = review.disable
|
||||||
|
|
||||||
|
RETURN review, report, resource, labels(resource)[0] AS type
|
||||||
|
`
|
||||||
|
const reviewWriteTxResultPromise = session.writeTransaction(async txc => {
|
||||||
|
const reviewTransactionResponse = await txc.run(cypher, {
|
||||||
|
params,
|
||||||
|
moderatorId: moderator.id,
|
||||||
|
dateTime: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
return reviewTransactionResponse.records.map(transformReturnType)
|
||||||
})
|
})
|
||||||
if (!resource) return null
|
const txResult = await reviewWriteTxResultPromise
|
||||||
return resource.id
|
if (!txResult[0]) return null
|
||||||
} finally {
|
createdRelationshipWithNestedAttributes = txResult[0]
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enable: async (object, params, { user, driver }) => {
|
|
||||||
const { id } = params
|
|
||||||
const cypher = `
|
|
||||||
MATCH (resource {id: $id})<-[d:DISABLED]-()
|
|
||||||
SET resource.disabled = false
|
|
||||||
DELETE d
|
|
||||||
RETURN resource {.id}
|
|
||||||
`
|
|
||||||
const session = driver.session()
|
|
||||||
try {
|
|
||||||
const res = await session.run(cypher, { id })
|
|
||||||
const [resource] = res.records.map(record => {
|
|
||||||
return record.get('resource')
|
|
||||||
})
|
|
||||||
if (!resource) return null
|
|
||||||
return resource.id
|
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return createdRelationshipWithNestedAttributes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,45 +8,53 @@ const factory = Factory()
|
|||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|
||||||
let query, mutate, authenticatedUser, variables, moderator, nonModerator
|
let mutate,
|
||||||
|
authenticatedUser,
|
||||||
|
disableVariables,
|
||||||
|
enableVariables,
|
||||||
|
moderator,
|
||||||
|
nonModerator,
|
||||||
|
closeReportVariables
|
||||||
|
|
||||||
const disableMutation = gql`
|
const reviewMutation = gql`
|
||||||
mutation($id: ID!) {
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
disable(id: $id)
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
}
|
createdAt
|
||||||
`
|
updatedAt
|
||||||
const enableMutation = gql`
|
resource {
|
||||||
mutation($id: ID!) {
|
__typename
|
||||||
enable(id: $id)
|
... on User {
|
||||||
}
|
id
|
||||||
`
|
disabled
|
||||||
|
}
|
||||||
const commentQuery = gql`
|
... on Post {
|
||||||
query($id: ID!) {
|
id
|
||||||
Comment(id: $id) {
|
disabled
|
||||||
id
|
}
|
||||||
disabled
|
... on Comment {
|
||||||
disabledBy {
|
id
|
||||||
id
|
disabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
report {
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const postQuery = gql`
|
|
||||||
query($id: ID) {
|
|
||||||
Post(id: $id) {
|
|
||||||
id
|
|
||||||
disabled
|
|
||||||
disabledBy {
|
|
||||||
id
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
closed
|
||||||
|
reviewed {
|
||||||
|
createdAt
|
||||||
|
moderator {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
describe('moderate resources', () => {
|
describe('moderate resources', () => {
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
authenticatedUser = undefined
|
authenticatedUser = undefined
|
||||||
const { server } = createServer({
|
const { server } = createServer({
|
||||||
context: () => {
|
context: () => {
|
||||||
@ -58,11 +66,19 @@ describe('moderate resources', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
mutate = createTestClient(server).mutate
|
mutate = createTestClient(server).mutate
|
||||||
query = createTestClient(server).query
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
variables = {}
|
disableVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
disable: true,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
enableVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
disable: false,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
moderator = await factory.create('User', {
|
moderator = await factory.create('User', {
|
||||||
id: 'moderator-id',
|
id: 'moderator-id',
|
||||||
@ -71,155 +87,392 @@ describe('moderate resources', () => {
|
|||||||
password: '1234',
|
password: '1234',
|
||||||
role: 'moderator',
|
role: 'moderator',
|
||||||
})
|
})
|
||||||
|
nonModerator = await factory.create('User', {
|
||||||
|
id: 'non-moderator',
|
||||||
|
name: 'Non Moderator',
|
||||||
|
email: 'non.moderator@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('disable', () => {
|
describe('review to close report, leaving resource enabled', () => {
|
||||||
beforeEach(() => {
|
|
||||||
variables = {
|
|
||||||
id: 'some-resource',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
describe('non moderator', () => {
|
beforeEach(async () => {
|
||||||
beforeEach(async () => {
|
authenticatedUser = await nonModerator.toJson()
|
||||||
nonModerator = await factory.create('User', {
|
})
|
||||||
id: 'non-moderator',
|
|
||||||
name: 'Non Moderator',
|
it('non-moderator receives an authorization error', async () => {
|
||||||
email: 'non.moderator@example.org',
|
await expect(
|
||||||
password: '1234',
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
})
|
).resolves.toMatchObject({
|
||||||
authenticatedUser = await nonModerator.toJson()
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
it('throws authorization error', async () => {
|
})
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
})
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
|
||||||
|
describe('moderator', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await moderator.toJson()
|
||||||
|
const questionablePost = await factory.create('Post', {
|
||||||
|
id: 'should-i-be-disabled',
|
||||||
|
})
|
||||||
|
const reportAgainstQuestionablePost = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstQuestionablePost.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'should-i-be-disabled',
|
||||||
|
reasonCategory: 'doxing',
|
||||||
|
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
||||||
|
}),
|
||||||
|
reportAgainstQuestionablePost.relateTo(questionablePost, 'belongsTo'),
|
||||||
|
])
|
||||||
|
closeReportVariables = {
|
||||||
|
resourceId: 'should-i-be-disabled',
|
||||||
|
disable: false,
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('report can be closed without disabling resource', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: closeReportVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'Post', id: 'should-i-be-disabled', disabled: false },
|
||||||
|
report: { id: expect.any(String), closed: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates only one review for multiple reviews by the same moderator on same resource', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
mutate({
|
||||||
|
mutation: reviewMutation,
|
||||||
|
variables: { ...disableVariables, resourceId: 'should-i-be-disabled' },
|
||||||
|
}),
|
||||||
|
mutate({
|
||||||
|
mutation: reviewMutation,
|
||||||
|
variables: { ...enableVariables, resourceId: 'should-i-be-disabled' },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const cypher =
|
||||||
|
'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review'
|
||||||
|
const reviews = await neode.cypher(cypher)
|
||||||
|
expect(reviews.records).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates the updatedAt attribute', async () => {
|
||||||
|
const [firstReview, secondReview] = await Promise.all([
|
||||||
|
mutate({
|
||||||
|
mutation: reviewMutation,
|
||||||
|
variables: { ...disableVariables, resourceId: 'should-i-be-disabled' },
|
||||||
|
}),
|
||||||
|
mutate({
|
||||||
|
mutation: reviewMutation,
|
||||||
|
variables: { ...enableVariables, resourceId: 'should-i-be-disabled' },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
expect(firstReview.data.review.updatedAt).toBeTruthy()
|
||||||
|
expect(Date.parse(firstReview.data.review.updatedAt)).toEqual(expect.any(Number))
|
||||||
|
expect(secondReview.data.review.updatedAt).toBeTruthy()
|
||||||
|
expect(Date.parse(secondReview.data.review.updatedAt)).toEqual(expect.any(Number))
|
||||||
|
expect(firstReview.data.review.updatedAt).not.toEqual(secondReview.data.review.updatedAt)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('review to disable', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await nonModerator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('non-moderator receives an authorization error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moderator', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await moderator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moderate a comment', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const trollingComment = await factory.create('Comment', {
|
||||||
|
id: 'comment-id',
|
||||||
|
})
|
||||||
|
const reportAgainstTrollingComment = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingComment.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
reasonCategory: 'other',
|
||||||
|
reasonDescription: 'This comment is bigoted',
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||||
|
])
|
||||||
|
disableVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns disabled resource id', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns .reviewed', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'Comment', id: 'comment-id' },
|
||||||
|
report: {
|
||||||
|
id: expect.any(String),
|
||||||
|
reviewed: expect.arrayContaining([
|
||||||
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates .disabled on comment', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: true } },
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be closed with one review', async () => {
|
||||||
|
closeReportVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: closeReportVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'Comment', id: 'comment-id' },
|
||||||
|
report: { id: expect.any(String), closed: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('moderator', () => {
|
describe('moderate a post', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await moderator.toJson()
|
const trollingPost = await factory.create('Post', {
|
||||||
|
id: 'post-id',
|
||||||
|
})
|
||||||
|
const reportAgainstTrollingPost = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingPost.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'post-id',
|
||||||
|
reasonCategory: 'doxing',
|
||||||
|
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'),
|
||||||
|
])
|
||||||
|
disableVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'post-id',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('moderate a resource that is not a (Comment|Post|User) ', () => {
|
it('returns disabled resource id', async () => {
|
||||||
beforeEach(async () => {
|
await expect(
|
||||||
variables = {
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
id: 'sample-tag-id',
|
).resolves.toMatchObject({
|
||||||
}
|
data: {
|
||||||
await factory.create('Tag', { id: 'sample-tag-id' })
|
review: {
|
||||||
})
|
resource: { __typename: 'Post', id: 'post-id' },
|
||||||
|
},
|
||||||
it('returns null', async () => {
|
},
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
errors: undefined,
|
||||||
data: { disable: null },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('moderate a comment', () => {
|
it('returns .reviewed', async () => {
|
||||||
beforeEach(async () => {
|
await expect(
|
||||||
variables = {}
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
await factory.create('Comment', {
|
).resolves.toMatchObject({
|
||||||
id: 'comment-id',
|
data: {
|
||||||
})
|
review: {
|
||||||
})
|
resource: { __typename: 'Post', id: 'post-id' },
|
||||||
|
report: {
|
||||||
it('returns disabled resource id', async () => {
|
id: expect.any(String),
|
||||||
variables = { id: 'comment-id' }
|
reviewed: expect.arrayContaining([
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
data: { disable: 'comment-id' },
|
]),
|
||||||
errors: undefined,
|
},
|
||||||
})
|
},
|
||||||
})
|
},
|
||||||
|
errors: undefined,
|
||||||
it('changes .disabledBy', async () => {
|
|
||||||
variables = { id: 'comment-id' }
|
|
||||||
const before = { data: { Comment: [{ id: 'comment-id', disabledBy: null }] } }
|
|
||||||
const expected = {
|
|
||||||
data: { Comment: [{ id: 'comment-id', disabledBy: { id: 'moderator-id' } }] },
|
|
||||||
}
|
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before)
|
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { disable: 'comment-id' },
|
|
||||||
})
|
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates .disabled on comment', async () => {
|
|
||||||
variables = { id: 'comment-id' }
|
|
||||||
const before = { data: { Comment: [{ id: 'comment-id', disabled: false }] } }
|
|
||||||
const expected = { data: { Comment: [{ id: 'comment-id', disabled: true }] } }
|
|
||||||
|
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before)
|
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { disable: 'comment-id' },
|
|
||||||
})
|
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('moderate a post', () => {
|
it('updates .disabled on post', async () => {
|
||||||
beforeEach(async () => {
|
await expect(
|
||||||
variables = {}
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
await factory.create('Post', {
|
).resolves.toMatchObject({
|
||||||
id: 'sample-post-id',
|
data: { review: { resource: { __typename: 'Post', id: 'post-id', disabled: true } } },
|
||||||
})
|
errors: undefined,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('returns disabled resource id', async () => {
|
it('can be closed with one review', async () => {
|
||||||
variables = { id: 'sample-post-id' }
|
closeReportVariables = {
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
...disableVariables,
|
||||||
data: { disable: 'sample-post-id' },
|
closed: true,
|
||||||
})
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: closeReportVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'Post', id: 'post-id' },
|
||||||
|
report: { id: expect.any(String), closed: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('changes .disabledBy', async () => {
|
describe('moderate a user', () => {
|
||||||
variables = { id: 'sample-post-id' }
|
beforeEach(async () => {
|
||||||
const before = { data: { Post: [{ id: 'sample-post-id', disabledBy: null }] } }
|
const troll = await factory.create('User', {
|
||||||
const expected = {
|
id: 'user-id',
|
||||||
data: { Post: [{ id: 'sample-post-id', disabledBy: { id: 'moderator-id' } }] },
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before)
|
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { disable: 'sample-post-id' },
|
|
||||||
})
|
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
|
const reportAgainstTroll = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTroll.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'user-id',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||||
|
}),
|
||||||
|
reportAgainstTroll.relateTo(troll, 'belongsTo'),
|
||||||
|
])
|
||||||
|
disableVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'user-id',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('updates .disabled on post', async () => {
|
it('returns disabled resource id', async () => {
|
||||||
const before = { data: { Post: [{ id: 'sample-post-id', disabled: false }] } }
|
await expect(
|
||||||
const expected = { data: { Post: [{ id: 'sample-post-id', disabled: true }] } }
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
variables = { id: 'sample-post-id' }
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'User', id: 'user-id' } } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before)
|
it('returns .reviewed', async () => {
|
||||||
await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({
|
await expect(
|
||||||
data: { disable: 'sample-post-id' },
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
})
|
).resolves.toMatchObject({
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected)
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'User', id: 'user-id' },
|
||||||
|
report: {
|
||||||
|
id: expect.any(String),
|
||||||
|
reviewed: expect.arrayContaining([
|
||||||
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates .disabled on user', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'User', id: 'user-id', disabled: true } } },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be closed with one review', async () => {
|
||||||
|
closeReportVariables = {
|
||||||
|
...disableVariables,
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: closeReportVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'User', id: 'user-id' },
|
||||||
|
report: { id: expect.any(String), closed: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('enable', () => {
|
describe('review to re-enable after disabled', () => {
|
||||||
describe('unautenticated user', () => {
|
describe('unautenticated user', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
variables = { id: 'sample-post-id' }
|
enableVariables = {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
...enableVariables,
|
||||||
|
resourceId: 'post-id',
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -228,17 +481,17 @@ describe('moderate resources', () => {
|
|||||||
describe('authenticated user', () => {
|
describe('authenticated user', () => {
|
||||||
describe('non moderator', () => {
|
describe('non moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
nonModerator = await factory.create('User', {
|
|
||||||
id: 'non-moderator',
|
|
||||||
name: 'Non Moderator',
|
|
||||||
email: 'non.moderator@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
authenticatedUser = await nonModerator.toJson()
|
authenticatedUser = await nonModerator.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
variables = { id: 'sample-post-id' }
|
enableVariables = {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
...enableVariables,
|
||||||
|
resourceId: 'post-id',
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -248,101 +501,197 @@ describe('moderate resources', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await moderator.toJson()
|
authenticatedUser = await moderator.toJson()
|
||||||
})
|
})
|
||||||
describe('moderate a resource that is not a (Comment|Post|User) ', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await Promise.all([factory.create('Tag', { id: 'sample-tag-id' })])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns null', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({ mutation: enableMutation, variables: { id: 'sample-tag-id' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { enable: null },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('moderate a comment', () => {
|
describe('moderate a comment', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
variables = { id: 'comment-id' }
|
const trollingComment = await factory.create('Comment', {
|
||||||
await factory.create('Comment', {
|
|
||||||
id: 'comment-id',
|
id: 'comment-id',
|
||||||
})
|
})
|
||||||
await mutate({ mutation: disableMutation, variables })
|
const reportAgainstTrollingComment = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingComment.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
reasonCategory: 'other',
|
||||||
|
reasonDescription: 'This comment is bigoted',
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||||
|
])
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingComment.relateTo(moderator, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
}),
|
||||||
|
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
])
|
||||||
|
enableVariables = {
|
||||||
|
...enableVariables,
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns enabled resource id', async () => {
|
it('returns enabled resource id', async () => {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
await expect(
|
||||||
data: { enable: 'comment-id' },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changes .disabledBy', async () => {
|
it('returns .reviewed', async () => {
|
||||||
const expected = {
|
await expect(
|
||||||
data: { Comment: [{ id: 'comment-id', disabledBy: null }] },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
}
|
data: {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
review: {
|
||||||
data: { enable: 'comment-id' },
|
resource: { __typename: 'Comment', id: 'comment-id' },
|
||||||
errors: undefined,
|
report: {
|
||||||
|
id: expect.any(String),
|
||||||
|
reviewed: expect.arrayContaining([
|
||||||
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates .disabled on comment', async () => {
|
it('updates .disabled on comment', async () => {
|
||||||
const expected = {
|
await expect(
|
||||||
data: { Comment: [{ id: 'comment-id', disabled: false }] },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
}
|
data: {
|
||||||
|
review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: false } },
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
},
|
||||||
data: { enable: 'comment-id' },
|
|
||||||
errors: undefined,
|
|
||||||
})
|
})
|
||||||
await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('moderate a post', () => {
|
describe('moderate a post', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
variables = { id: 'post-id' }
|
const trollingPost = await factory.create('Post', {
|
||||||
await factory.create('Post', {
|
|
||||||
id: 'post-id',
|
id: 'post-id',
|
||||||
})
|
})
|
||||||
await mutate({ mutation: disableMutation, variables })
|
const reportAgainstTrollingPost = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingPost.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'post-id',
|
||||||
|
reasonCategory: 'doxing',
|
||||||
|
reasonDescription:
|
||||||
|
"This shouldn't be shown to anybody else! It's my private thing!",
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'),
|
||||||
|
])
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTrollingPost.relateTo(moderator, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
}),
|
||||||
|
trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
])
|
||||||
|
enableVariables = {
|
||||||
|
...enableVariables,
|
||||||
|
resourceId: 'post-id',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns enabled resource id', async () => {
|
it('returns enabled resource id', async () => {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
await expect(
|
||||||
data: { enable: 'post-id' },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'Post', id: 'post-id' } } },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changes .disabledBy', async () => {
|
it('returns .reviewed', async () => {
|
||||||
const expected = {
|
await expect(
|
||||||
data: { Post: [{ id: 'post-id', disabledBy: null }] },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
}
|
data: {
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
review: {
|
||||||
data: { enable: 'post-id' },
|
resource: { __typename: 'Post', id: 'post-id' },
|
||||||
errors: undefined,
|
report: {
|
||||||
|
id: expect.any(String),
|
||||||
|
reviewed: expect.arrayContaining([
|
||||||
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates .disabled on post', async () => {
|
it('updates .disabled on post', async () => {
|
||||||
const expected = {
|
await expect(
|
||||||
data: { Post: [{ id: 'post-id', disabled: false }] },
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
errors: undefined,
|
).resolves.toMatchObject({
|
||||||
}
|
data: {
|
||||||
|
review: { resource: { __typename: 'Post', id: 'post-id', disabled: false } },
|
||||||
await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({
|
},
|
||||||
data: { enable: 'post-id' },
|
})
|
||||||
errors: undefined,
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moderate a user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const troll = await factory.create('User', {
|
||||||
|
id: 'user-id',
|
||||||
|
})
|
||||||
|
const reportAgainstTroll = await factory.create('Report')
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTroll.relateTo(nonModerator, 'filed', {
|
||||||
|
resourceId: 'user-id',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||||
|
}),
|
||||||
|
reportAgainstTroll.relateTo(troll, 'belongsTo'),
|
||||||
|
])
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstTroll.relateTo(moderator, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'comment-id',
|
||||||
|
}),
|
||||||
|
troll.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
])
|
||||||
|
enableVariables = {
|
||||||
|
...enableVariables,
|
||||||
|
resourceId: 'user-id',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns enabled resource id', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { review: { resource: { __typename: 'User', id: 'user-id' } } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns .reviewed', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: {
|
||||||
|
resource: { __typename: 'User', id: 'user-id' },
|
||||||
|
report: {
|
||||||
|
id: expect.any(String),
|
||||||
|
reviewed: expect.arrayContaining([
|
||||||
|
{ createdAt: expect.any(String), moderator: { id: 'moderator-id' } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates .disabled on user', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
review: { resource: { __typename: 'User', id: 'user-id', disabled: false } },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getBlockedUsers, getBlockedByUsers } from './users.js'
|
|||||||
import { mergeWith, isArray, isEmpty } from 'lodash'
|
import { mergeWith, isArray, isEmpty } from 'lodash'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
const filterForBlockedUsers = async (params, context) => {
|
const filterForBlockedUsers = async (params, context) => {
|
||||||
if (!context.user) return params
|
if (!context.user) return params
|
||||||
const [blockedUsers, blockedByUsers] = await Promise.all([
|
const [blockedUsers, blockedByUsers] = await Promise.all([
|
||||||
@ -318,7 +319,6 @@ export default {
|
|||||||
},
|
},
|
||||||
hasOne: {
|
hasOne: {
|
||||||
author: '<-[:WROTE]-(related:User)',
|
author: '<-[:WROTE]-(related:User)',
|
||||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
|
||||||
pinnedBy: '<-[:PINNED]-(related:User)',
|
pinnedBy: '<-[:PINNED]-(related:User)',
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
|
|||||||
@ -1,57 +1,47 @@
|
|||||||
|
const transformReturnType = record => {
|
||||||
|
return {
|
||||||
|
...record.get('report').properties,
|
||||||
|
resource: {
|
||||||
|
__typename: record.get('type'),
|
||||||
|
...record.get('resource').properties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
report: async (_parent, params, context, _resolveInfo) => {
|
fileReport: async (_parent, params, context, _resolveInfo) => {
|
||||||
let createdRelationshipWithNestedAttributes
|
let createdRelationshipWithNestedAttributes
|
||||||
const { resourceId, reasonCategory, reasonDescription } = params
|
const { resourceId, reasonCategory, reasonDescription } = params
|
||||||
const { driver, user } = context
|
const { driver, user } = context
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
const reportWriteTxResultPromise = session.writeTransaction(async txc => {
|
||||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
const reportTransactionResponse = await txc.run(
|
||||||
const reportRelationshipTransactionResponse = await txc.run(
|
`
|
||||||
`
|
|
||||||
MATCH (submitter:User {id: $submitterId})
|
MATCH (submitter:User {id: $submitterId})
|
||||||
MATCH (resource {id: $resourceId})
|
MATCH (resource {id: $resourceId})
|
||||||
WHERE resource:User OR resource:Comment OR resource:Post
|
WHERE resource:User OR resource:Post OR resource:Comment
|
||||||
CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
MERGE (resource)<-[:BELONGS_TO]-(report:Report {closed: false})
|
||||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
ON CREATE SET report.id = randomUUID(), report.createdAt = $createdAt, report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.disable = resource.disabled, report.closed = false
|
||||||
|
WITH submitter, resource, report
|
||||||
|
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
||||||
|
|
||||||
|
RETURN report, resource, labels(resource)[0] AS type
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
resourceId,
|
resourceId,
|
||||||
submitterId: user.id,
|
submitterId: user.id,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
reasonCategory,
|
reasonCategory,
|
||||||
reasonDescription,
|
reasonDescription,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return reportRelationshipTransactionResponse.records.map(record => ({
|
return reportTransactionResponse.records.map(transformReturnType)
|
||||||
report: record.get('report'),
|
})
|
||||||
submitter: record.get('submitter'),
|
try {
|
||||||
resource: record.get('resource').properties,
|
const txResult = await reportWriteTxResultPromise
|
||||||
type: record.get('type'),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
const txResult = await writeTxResultPromise
|
|
||||||
if (!txResult[0]) return null
|
if (!txResult[0]) return null
|
||||||
const { report, submitter, resource, type } = txResult[0]
|
createdRelationshipWithNestedAttributes = 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 {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
@ -61,8 +51,8 @@ export default {
|
|||||||
Query: {
|
Query: {
|
||||||
reports: async (_parent, params, context, _resolveInfo) => {
|
reports: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { driver } = context
|
const { driver } = context
|
||||||
let response
|
const session = driver.session()
|
||||||
let orderByClause
|
let reports, orderByClause
|
||||||
switch (params.orderBy) {
|
switch (params.orderBy) {
|
||||||
case 'createdAt_asc':
|
case 'createdAt_asc':
|
||||||
orderByClause = 'ORDER BY report.createdAt ASC'
|
orderByClause = 'ORDER BY report.createdAt ASC'
|
||||||
@ -73,56 +63,97 @@ export default {
|
|||||||
default:
|
default:
|
||||||
orderByClause = ''
|
orderByClause = ''
|
||||||
}
|
}
|
||||||
const session = driver.session()
|
const reportReadTxPromise = session.readTransaction(async tx => {
|
||||||
try {
|
const allReportsTransactionResponse = await tx.run(
|
||||||
const cypher = `
|
`
|
||||||
MATCH (submitter:User)-[report:REPORTED]->(resource)
|
MATCH (submitter:User)-[filed:FILED]->(report:Report)-[:BELONGS_TO]->(resource)
|
||||||
WHERE resource:User OR resource:Comment OR resource:Post
|
WHERE resource:User OR resource:Post OR resource:Comment
|
||||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
RETURN DISTINCT report, resource, labels(resource)[0] as type
|
||||||
${orderByClause}
|
${orderByClause}
|
||||||
`
|
`,
|
||||||
const result = await session.run(cypher, {})
|
{},
|
||||||
const dbResponse = result.records.map(r => {
|
)
|
||||||
return {
|
return allReportsTransactionResponse.records.map(transformReturnType)
|
||||||
report: r.get('report'),
|
})
|
||||||
submitter: r.get('submitter'),
|
try {
|
||||||
resource: r.get('resource'),
|
const txResult = await reportReadTxPromise
|
||||||
type: r.get('type'),
|
if (!txResult[0]) return null
|
||||||
|
reports = txResult
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return reports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Report: {
|
||||||
|
filed: async (parent, _params, context, _resolveInfo) => {
|
||||||
|
if (typeof parent.filed !== 'undefined') return parent.filed
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { id } = parent
|
||||||
|
let filed
|
||||||
|
const readTxPromise = session.readTransaction(async tx => {
|
||||||
|
const allReportsTransactionResponse = await tx.run(
|
||||||
|
`
|
||||||
|
MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id})
|
||||||
|
RETURN filed, submitter
|
||||||
|
`,
|
||||||
|
{ id },
|
||||||
|
)
|
||||||
|
return allReportsTransactionResponse.records.map(record => ({
|
||||||
|
submitter: record.get('submitter').properties,
|
||||||
|
filed: record.get('filed').properties,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const txResult = await readTxPromise
|
||||||
|
if (!txResult[0]) return null
|
||||||
|
filed = txResult.map(reportedRecord => {
|
||||||
|
const { submitter, filed } = reportedRecord
|
||||||
|
const relationshipWithNestedAttributes = {
|
||||||
|
...filed,
|
||||||
|
submitter,
|
||||||
}
|
}
|
||||||
})
|
return relationshipWithNestedAttributes
|
||||||
if (!dbResponse) return null
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
return filed
|
||||||
return response
|
},
|
||||||
|
reviewed: async (parent, _params, context, _resolveInfo) => {
|
||||||
|
if (typeof parent.reviewed !== 'undefined') return parent.reviewed
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { id } = parent
|
||||||
|
let reviewed
|
||||||
|
const readTxPromise = session.readTransaction(async tx => {
|
||||||
|
const allReportsTransactionResponse = await tx.run(
|
||||||
|
`
|
||||||
|
MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User)
|
||||||
|
RETURN moderator, review
|
||||||
|
ORDER BY report.updatedAt DESC, review.updatedAt DESC
|
||||||
|
`,
|
||||||
|
{ id },
|
||||||
|
)
|
||||||
|
return allReportsTransactionResponse.records.map(record => ({
|
||||||
|
review: record.get('review').properties,
|
||||||
|
moderator: record.get('moderator').properties,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const txResult = await readTxPromise
|
||||||
|
if (!txResult[0]) return null
|
||||||
|
reviewed = txResult.map(reportedRecord => {
|
||||||
|
const { review, moderator } = reportedRecord
|
||||||
|
const relationshipWithNestedAttributes = {
|
||||||
|
...review,
|
||||||
|
moderator,
|
||||||
|
}
|
||||||
|
return relationshipWithNestedAttributes
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return reviewed
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,31 +8,41 @@ const factory = Factory()
|
|||||||
const instance = getNeode()
|
const instance = getNeode()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|
||||||
describe('report resources', () => {
|
describe('file a report on a resource', () => {
|
||||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser
|
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
const reportMutation = gql`
|
const reportMutation = gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
report(
|
fileReport(
|
||||||
resourceId: $resourceId
|
resourceId: $resourceId
|
||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
|
id
|
||||||
createdAt
|
createdAt
|
||||||
reasonCategory
|
updatedAt
|
||||||
reasonDescription
|
disable
|
||||||
type
|
closed
|
||||||
submitter {
|
rule
|
||||||
email
|
resource {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
... on Post {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user {
|
filed {
|
||||||
name
|
submitter {
|
||||||
}
|
id
|
||||||
post {
|
}
|
||||||
title
|
createdAt
|
||||||
}
|
reasonCategory
|
||||||
comment {
|
reasonDescription
|
||||||
content
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +77,7 @@ describe('report resources', () => {
|
|||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { report: null },
|
data: { fileReport: null },
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -81,6 +91,12 @@ describe('report resources', () => {
|
|||||||
email: 'test@example.org',
|
email: 'test@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
})
|
})
|
||||||
|
otherReportingUser = await factory.create('User', {
|
||||||
|
id: 'other-reporting-user-id',
|
||||||
|
role: 'user',
|
||||||
|
email: 'reporting@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
await factory.create('User', {
|
await factory.create('User', {
|
||||||
id: 'abusive-user-id',
|
id: 'abusive-user-id',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -99,15 +115,15 @@ describe('report resources', () => {
|
|||||||
describe('invalid resource id', () => {
|
describe('invalid resource id', () => {
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { report: null },
|
data: { fileReport: null },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('valid resource', () => {
|
describe('valid resource', () => {
|
||||||
describe('reported resource is a user', () => {
|
describe('creates report', () => {
|
||||||
it('returns type "User"', async () => {
|
it('which belongs to resource', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: reportMutation,
|
||||||
@ -115,15 +131,28 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
type: 'User',
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns resource in user attribute', async () => {
|
it('creates only one report for multiple reports on the same resource', async () => {
|
||||||
|
const firstReport = await mutate({
|
||||||
|
mutation: reportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
})
|
||||||
|
authenticatedUser = await otherReportingUser.toJson()
|
||||||
|
const secondReport = await mutate({
|
||||||
|
mutation: reportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
})
|
||||||
|
expect(firstReport.data.fileReport.id).toEqual(secondReport.data.fileReport.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the rule for how the report was decided', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: reportMutation,
|
||||||
@ -131,8 +160,46 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
user: {
|
rule: 'latestReviewUpdatedAtRules',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it.todo('creates multiple filed reports')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('reported resource is a user', () => {
|
||||||
|
it('returns __typename "User"', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: reportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
fileReport: {
|
||||||
|
resource: {
|
||||||
|
__typename: 'User',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns user attribute info', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: reportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
fileReport: {
|
||||||
|
resource: {
|
||||||
|
__typename: 'User',
|
||||||
name: 'abusive-user',
|
name: 'abusive-user',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -149,10 +216,14 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
submitter: {
|
filed: [
|
||||||
email: 'test@example.org',
|
{
|
||||||
},
|
submitter: {
|
||||||
|
id: 'current-user-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -167,7 +238,7 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -187,8 +258,12 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
filed: [
|
||||||
|
{
|
||||||
|
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -228,15 +303,19 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
reasonDescription: 'My reason!',
|
filed: [
|
||||||
|
{
|
||||||
|
reasonDescription: 'My reason!',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sanitize the reason description', async () => {
|
it('sanitizes the reason description', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: reportMutation,
|
||||||
@ -248,8 +327,12 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
reasonDescription: 'My reason !',
|
filed: [
|
||||||
|
{
|
||||||
|
reasonDescription: 'My reason !',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -278,8 +361,10 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
type: 'Post',
|
resource: {
|
||||||
|
__typename: 'Post',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -297,8 +382,9 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
post: {
|
resource: {
|
||||||
|
__typename: 'Post',
|
||||||
title: 'This is a post that is going to be reported',
|
title: 'This is a post that is going to be reported',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -306,25 +392,6 @@ describe('report resources', () => {
|
|||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns null in user attribute', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: reportMutation,
|
|
||||||
variables: {
|
|
||||||
...variables,
|
|
||||||
resourceId: 'post-to-report-id',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
report: {
|
|
||||||
user: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('reported resource is a comment', () => {
|
describe('reported resource is a comment', () => {
|
||||||
@ -356,8 +423,10 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
type: 'Comment',
|
resource: {
|
||||||
|
__typename: 'Comment',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -375,8 +444,9 @@ describe('report resources', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
report: {
|
fileReport: {
|
||||||
comment: {
|
resource: {
|
||||||
|
__typename: 'Comment',
|
||||||
content: 'Post comment to be reported.',
|
content: 'Post comment to be reported.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -403,7 +473,7 @@ describe('report resources', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: { report: null },
|
data: { fileReport: null },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -411,25 +481,35 @@ describe('report resources', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('query for reported resource', () => {
|
describe('query for reported resource', () => {
|
||||||
const reportsQuery = gql`
|
const reportsQuery = gql`
|
||||||
query {
|
query {
|
||||||
reports(orderBy: createdAt_desc) {
|
reports(orderBy: createdAt_desc) {
|
||||||
|
id
|
||||||
createdAt
|
createdAt
|
||||||
reasonCategory
|
updatedAt
|
||||||
reasonDescription
|
disable
|
||||||
submitter {
|
closed
|
||||||
id
|
resource {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type
|
filed {
|
||||||
user {
|
submitter {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
post {
|
createdAt
|
||||||
id
|
reasonCategory
|
||||||
}
|
reasonDescription
|
||||||
comment {
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -437,7 +517,6 @@ describe('report resources', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
|
|
||||||
moderator = await factory.create('User', {
|
moderator = await factory.create('User', {
|
||||||
id: 'moderator-1',
|
id: 'moderator-1',
|
||||||
role: 'moderator',
|
role: 'moderator',
|
||||||
@ -518,6 +597,7 @@ describe('report resources', () => {
|
|||||||
])
|
])
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
@ -527,6 +607,7 @@ describe('report resources', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
it('role "user" gets no reports', async () => {
|
it('role "user" gets no reports', async () => {
|
||||||
authenticatedUser = await currentUser.toJson()
|
authenticatedUser = await currentUser.toJson()
|
||||||
@ -538,49 +619,69 @@ describe('report resources', () => {
|
|||||||
|
|
||||||
it('role "moderator" gets reports', async () => {
|
it('role "moderator" gets reports', async () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
// to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ
|
|
||||||
reports: expect.arrayContaining([
|
reports: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
reasonCategory: 'doxing',
|
updatedAt: expect.any(String),
|
||||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
disable: false,
|
||||||
submitter: expect.objectContaining({
|
closed: false,
|
||||||
id: 'current-user-id',
|
resource: {
|
||||||
}),
|
__typename: 'User',
|
||||||
type: 'User',
|
|
||||||
user: expect.objectContaining({
|
|
||||||
id: 'abusive-user-1',
|
id: 'abusive-user-1',
|
||||||
}),
|
},
|
||||||
post: null,
|
filed: expect.arrayContaining([
|
||||||
comment: null,
|
expect.objectContaining({
|
||||||
|
submitter: expect.objectContaining({
|
||||||
|
id: 'current-user-id',
|
||||||
|
}),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
reasonCategory: 'doxing',
|
||||||
|
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
reasonCategory: 'other',
|
updatedAt: expect.any(String),
|
||||||
reasonDescription: 'This comment is bigoted',
|
disable: false,
|
||||||
submitter: expect.objectContaining({
|
closed: false,
|
||||||
id: 'current-user-id',
|
resource: {
|
||||||
}),
|
__typename: 'Post',
|
||||||
type: 'Post',
|
|
||||||
user: null,
|
|
||||||
post: expect.objectContaining({
|
|
||||||
id: 'abusive-post-1',
|
id: 'abusive-post-1',
|
||||||
}),
|
},
|
||||||
comment: null,
|
filed: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
submitter: expect.objectContaining({
|
||||||
|
id: 'current-user-id',
|
||||||
|
}),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
reasonCategory: 'other',
|
||||||
|
reasonDescription: 'This comment is bigoted',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
reasonCategory: 'discrimination_etc',
|
updatedAt: expect.any(String),
|
||||||
reasonDescription: 'This post is bigoted',
|
disable: false,
|
||||||
submitter: expect.objectContaining({
|
closed: false,
|
||||||
id: 'current-user-id',
|
resource: {
|
||||||
}),
|
__typename: 'Comment',
|
||||||
type: 'Comment',
|
|
||||||
user: null,
|
|
||||||
post: null,
|
|
||||||
comment: expect.objectContaining({
|
|
||||||
id: 'abusive-comment-1',
|
id: 'abusive-comment-1',
|
||||||
}),
|
},
|
||||||
|
filed: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
submitter: expect.objectContaining({
|
||||||
|
id: 'current-user-id',
|
||||||
|
}),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'This post is bigoted',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,25 +9,25 @@ import { neode as getNeode } from '../../bootstrap/neo4j'
|
|||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
let query
|
let query, mutate, variables, req, user
|
||||||
let mutate
|
|
||||||
let variables
|
|
||||||
let req
|
|
||||||
let user
|
|
||||||
|
|
||||||
const disable = async id => {
|
const disable = async id => {
|
||||||
await factory.create('User', { id: 'u2', role: 'moderator' })
|
const moderator = await factory.create('User', { id: 'u2', role: 'moderator' })
|
||||||
const moderatorBearerToken = encode({ id: 'u2' })
|
const user = await neode.find('User', id)
|
||||||
req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } }
|
const reportAgainstUser = await factory.create('Report')
|
||||||
await mutate({
|
await Promise.all([
|
||||||
mutation: gql`
|
reportAgainstUser.relateTo(moderator, 'filed', {
|
||||||
mutation($id: ID!) {
|
resourceId: id,
|
||||||
disable(id: $id)
|
reasonCategory: 'discrimination_etc',
|
||||||
}
|
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||||
`,
|
}),
|
||||||
variables: { id },
|
reportAgainstUser.relateTo(user, 'belongsTo'),
|
||||||
})
|
])
|
||||||
req = { headers: {} }
|
const disableVariables = { resourceId: user.id, disable: true, closed: false }
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstUser.relateTo(moderator, 'reviewed', disableVariables),
|
||||||
|
user.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -212,7 +212,6 @@ export default {
|
|||||||
},
|
},
|
||||||
hasOne: {
|
hasOne: {
|
||||||
invitedBy: '<-[:INVITED]-(related:User)',
|
invitedBy: '<-[:INVITED]-(related:User)',
|
||||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
|
||||||
location: '-[:IS_IN]->(related:Location)',
|
location: '-[:IS_IN]->(related:Location)',
|
||||||
},
|
},
|
||||||
hasMany: {
|
hasMany: {
|
||||||
|
|||||||
@ -24,8 +24,6 @@ type Mutation {
|
|||||||
changePassword(oldPassword: String!, newPassword: String!): String!
|
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||||
requestPasswordReset(email: String!): Boolean!
|
requestPasswordReset(email: String!): Boolean!
|
||||||
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
||||||
disable(id: ID!): ID
|
|
||||||
enable(id: ID!): ID
|
|
||||||
# Shout the given Type and ID
|
# Shout the given Type and ID
|
||||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||||
# Unshout the given Type and ID
|
# Unshout the given Type and ID
|
||||||
|
|||||||
@ -47,7 +47,6 @@ type Comment {
|
|||||||
updatedAt: String
|
updatedAt: String
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|||||||
23
backend/src/schema/types/type/FILED.gql
Normal file
23
backend/src/schema/types/type/FILED.gql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
type FILED {
|
||||||
|
createdAt: String!
|
||||||
|
reasonCategory: ReasonCategory!
|
||||||
|
reasonDescription: String!
|
||||||
|
submitter: User
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReportOrdering {
|
||||||
|
createdAt_asc
|
||||||
|
createdAt_desc
|
||||||
|
}
|
||||||
@ -26,7 +26,7 @@ enum NotificationReason {
|
|||||||
type Query {
|
type Query {
|
||||||
notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED]
|
notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
markAsRead(id: ID!): NOTIFIED
|
markAsRead(id: ID!): NOTIFIED
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,7 @@ type Post {
|
|||||||
objectId: String
|
objectId: String
|
||||||
author: User @relation(name: "WROTE", direction: "IN")
|
author: User @relation(name: "WROTE", direction: "IN")
|
||||||
title: String!
|
title: String!
|
||||||
slug: String
|
slug: String!
|
||||||
content: String!
|
content: String!
|
||||||
contentExcerpt: String
|
contentExcerpt: String
|
||||||
image: String
|
image: String
|
||||||
@ -123,7 +123,6 @@ type Post {
|
|||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
pinned: Boolean
|
pinned: Boolean
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
language: String
|
language: String
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
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_asc
|
|
||||||
createdAt_desc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
reports(orderBy: ReportOrdering): [REPORTED]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED
|
|
||||||
}
|
|
||||||
15
backend/src/schema/types/type/REVIEWED.gql
Normal file
15
backend/src/schema/types/type/REVIEWED.gql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
type REVIEWED {
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
disable: Boolean!
|
||||||
|
closed: Boolean!
|
||||||
|
report: Report
|
||||||
|
# @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report")
|
||||||
|
moderator: User
|
||||||
|
resource: ReviewedResource
|
||||||
|
}
|
||||||
|
union ReviewedResource = User | Post | Comment
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
review(resourceId: ID!, disable: Boolean, closed: Boolean): REVIEWED
|
||||||
|
}
|
||||||
25
backend/src/schema/types/type/Report.gql
Normal file
25
backend/src/schema/types/type/Report.gql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
type Report {
|
||||||
|
id: ID!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
rule: ReportRule!
|
||||||
|
disable: Boolean!
|
||||||
|
closed: Boolean!
|
||||||
|
filed: [FILED]
|
||||||
|
reviewed: [REVIEWED]
|
||||||
|
resource: ReportedResource
|
||||||
|
}
|
||||||
|
|
||||||
|
union ReportedResource = User | Post | Comment
|
||||||
|
|
||||||
|
enum ReportRule {
|
||||||
|
latestReviewUpdatedAtRules
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
reports(orderBy: ReportOrdering): [Report]
|
||||||
|
}
|
||||||
@ -33,7 +33,6 @@ type User {
|
|||||||
coverImg: String
|
coverImg: String
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
role: UserGroup!
|
role: UserGroup!
|
||||||
publicKey: String
|
publicKey: String
|
||||||
invitedBy: User @relation(name: "INVITED", direction: "IN")
|
invitedBy: User @relation(name: "INVITED", direction: "IN")
|
||||||
@ -44,8 +43,6 @@ type User {
|
|||||||
about: String
|
about: String
|
||||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
||||||
|
|
||||||
# createdAt: DateTime
|
|
||||||
# updatedAt: DateTime
|
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import createLocation from './locations.js'
|
|||||||
import createEmailAddress from './emailAddresses.js'
|
import createEmailAddress from './emailAddresses.js'
|
||||||
import createDonations from './donations.js'
|
import createDonations from './donations.js'
|
||||||
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
|
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
|
||||||
|
import createReport from './reports.js'
|
||||||
|
|
||||||
const factories = {
|
const factories = {
|
||||||
Badge: createBadge,
|
Badge: createBadge,
|
||||||
@ -23,6 +24,7 @@ const factories = {
|
|||||||
EmailAddress: createEmailAddress,
|
EmailAddress: createEmailAddress,
|
||||||
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
|
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
|
||||||
Donations: createDonations,
|
Donations: createDonations,
|
||||||
|
Report: createReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options = {}) => {
|
||||||
|
|||||||
7
backend/src/seed/factories/reports.js
Normal file
7
backend/src/seed/factories/reports.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function create() {
|
||||||
|
return {
|
||||||
|
factory: async ({ args, neodeInstance }) => {
|
||||||
|
return neodeInstance.create('Report', args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -524,7 +524,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
])
|
])
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
|
|
||||||
await Promise.all([
|
const comments = await Promise.all([
|
||||||
factory.create('Comment', {
|
factory.create('Comment', {
|
||||||
author: jennyRostock,
|
author: jennyRostock,
|
||||||
id: 'c1',
|
id: 'c1',
|
||||||
@ -541,7 +541,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
}),
|
}),
|
||||||
factory.create('Comment', {
|
factory.create('Comment', {
|
||||||
author: bobDerBaumeister,
|
author: jennyRostock,
|
||||||
id: 'c5',
|
id: 'c5',
|
||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
}),
|
}),
|
||||||
@ -581,6 +581,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
const trollingComment = comments[0]
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
democracy.relateTo(p3, 'post'),
|
democracy.relateTo(p3, 'post'),
|
||||||
@ -644,68 +645,107 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
louie.relateTo(p10, 'shouted'),
|
louie.relateTo(p10, 'shouted'),
|
||||||
])
|
])
|
||||||
|
|
||||||
const disableMutation = gql`
|
const reports = await Promise.all([
|
||||||
mutation($id: ID!) {
|
factory.create('Report'),
|
||||||
disable(id: $id)
|
factory.create('Report'),
|
||||||
}
|
factory.create('Report'),
|
||||||
`
|
|
||||||
authenticatedUser = await bobDerBaumeister.toJson()
|
|
||||||
await Promise.all([
|
|
||||||
mutate({
|
|
||||||
mutation: disableMutation,
|
|
||||||
variables: {
|
|
||||||
id: 'p11',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
mutate({
|
|
||||||
mutation: disableMutation,
|
|
||||||
variables: {
|
|
||||||
id: 'c5',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
authenticatedUser = null
|
const reportAgainstDagobert = reports[0]
|
||||||
|
const reportAgainstTrollingPost = reports[1]
|
||||||
|
const reportAgainstTrollingComment = reports[2]
|
||||||
|
|
||||||
// There is no error logged or the 'try' fails if this mutation is wrong. Why?
|
// report resource first time
|
||||||
const reportMutation = gql`
|
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
|
||||||
report(
|
|
||||||
resourceId: $resourceId
|
|
||||||
reasonCategory: $reasonCategory
|
|
||||||
reasonDescription: $reasonDescription
|
|
||||||
) {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
authenticatedUser = await huey.toJson()
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mutate({
|
reportAgainstDagobert.relateTo(jennyRostock, 'filed', {
|
||||||
mutation: reportMutation,
|
resourceId: 'u7',
|
||||||
variables: {
|
reasonCategory: 'discrimination_etc',
|
||||||
resourceId: 'c1',
|
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||||
reasonCategory: 'other',
|
|
||||||
reasonDescription: 'This comment is bigoted',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
mutate({
|
reportAgainstDagobert.relateTo(dagobert, 'belongsTo'),
|
||||||
mutation: reportMutation,
|
reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', {
|
||||||
variables: {
|
resourceId: 'p2',
|
||||||
resourceId: 'p1',
|
reasonCategory: 'doxing',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
||||||
reasonDescription: 'This post is bigoted',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
mutate({
|
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
|
||||||
mutation: reportMutation,
|
reportAgainstTrollingComment.relateTo(huey, 'filed', {
|
||||||
variables: {
|
resourceId: 'c1',
|
||||||
resourceId: 'u1',
|
reasonCategory: 'other',
|
||||||
reasonCategory: 'doxing',
|
reasonDescription: 'This comment is bigoted',
|
||||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||||
|
])
|
||||||
|
|
||||||
|
// report resource a second time
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstDagobert.relateTo(louie, 'filed', {
|
||||||
|
resourceId: 'u7',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'this user is attacking me for who I am!',
|
||||||
|
}),
|
||||||
|
reportAgainstDagobert.relateTo(dagobert, 'belongsTo'),
|
||||||
|
reportAgainstTrollingPost.relateTo(peterLustig, 'filed', {
|
||||||
|
resourceId: 'p2',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'This post is bigoted',
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
|
||||||
|
|
||||||
|
reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', {
|
||||||
|
resourceId: 'c1',
|
||||||
|
reasonCategory: 'pornographic_content_links',
|
||||||
|
reasonDescription: 'This comment is porno!!!',
|
||||||
|
}),
|
||||||
|
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const disableVariables = {
|
||||||
|
resourceId: 'undefined-resource',
|
||||||
|
disable: true,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// review resource first time
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'u7',
|
||||||
|
}),
|
||||||
|
dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'p2',
|
||||||
|
}),
|
||||||
|
p2.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'c1',
|
||||||
|
}),
|
||||||
|
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||||
|
])
|
||||||
|
|
||||||
|
// second review of resource and close report
|
||||||
|
await Promise.all([
|
||||||
|
reportAgainstDagobert.relateTo(peterLustig, 'reviewed', {
|
||||||
|
resourceId: 'u7',
|
||||||
|
disable: false,
|
||||||
|
closed: true,
|
||||||
|
}),
|
||||||
|
dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }),
|
||||||
|
reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', {
|
||||||
|
resourceId: 'p2',
|
||||||
|
disable: true,
|
||||||
|
closed: true,
|
||||||
|
}),
|
||||||
|
p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }),
|
||||||
|
reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', {
|
||||||
|
...disableVariables,
|
||||||
|
resourceId: 'c1',
|
||||||
|
disable: true,
|
||||||
|
closed: true,
|
||||||
|
}),
|
||||||
|
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }),
|
||||||
])
|
])
|
||||||
authenticatedUser = null
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...Array(30).keys()].map(i => {
|
[...Array(30).keys()].map(i => {
|
||||||
|
|||||||
@ -129,8 +129,8 @@ Given('somebody reported the following posts:', table => {
|
|||||||
.create('User', submitter)
|
.create('User', submitter)
|
||||||
.authenticateAs(submitter)
|
.authenticateAs(submitter)
|
||||||
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||||
type
|
id
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
resourceId,
|
resourceId,
|
||||||
|
|||||||
55
neo4j/change_disabled_relationship_to_report_node.sh
Executable file
55
neo4j/change_disabled_relationship_to_report_node.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/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 "
|
||||||
|
// convert old DISABLED to new REVIEWED-Report-BELONGS_TO structure
|
||||||
|
MATCH (moderator:User)-[disabled:DISABLED]->(disabledResource)
|
||||||
|
WHERE disabledResource:User OR disabledResource:Comment OR disabledResource:Post
|
||||||
|
DELETE disabled
|
||||||
|
CREATE (moderator)-[review:REVIEWED]->(report:Report)-[:BELONGS_TO]->(disabledResource)
|
||||||
|
SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true
|
||||||
|
SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false
|
||||||
|
|
||||||
|
// if disabledResource has no filed report, then create a moderators default filed report
|
||||||
|
WITH moderator, disabledResource, report
|
||||||
|
OPTIONAL MATCH (disabledResourceReporter:User)-[existingFiledReport:FILED]->(disabledResource)
|
||||||
|
FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NULL THEN [1] ELSE [] END |
|
||||||
|
CREATE (moderator)-[addModeratorReport:FILED]->(report)
|
||||||
|
SET addModeratorReport.createdAt = toString(datetime()), addModeratorReport.reasonCategory = 'other', addModeratorReport.reasonDescription = 'Old DISABLED relations didn't enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.'
|
||||||
|
)
|
||||||
|
FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NOT NULL THEN [1] ELSE [] END |
|
||||||
|
CREATE (disabledResourceReporter)-[moveModeratorReport:FILED]->(report)
|
||||||
|
SET moveModeratorReport = existingFiledReport
|
||||||
|
DELETE existingFiledReport
|
||||||
|
)
|
||||||
|
|
||||||
|
RETURN disabledResource {.id};
|
||||||
|
" | cypher-shell
|
||||||
|
|
||||||
|
echo "
|
||||||
|
// for FILED resources without DISABLED relation which are handled above, create new FILED-Report-BELONGS_TO structure
|
||||||
|
MATCH (reporter:User)-[oldReport:REPORTED]->(notDisabledResource)
|
||||||
|
WHERE notDisabledResource:User OR notDisabledResource:Comment OR notDisabledResource:Post
|
||||||
|
MERGE (report:Report)-[:BELONGS_TO]->(notDisabledResource)
|
||||||
|
ON CREATE SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false
|
||||||
|
CREATE (reporter)-[filed:FILED]->(report)
|
||||||
|
SET report = oldReport
|
||||||
|
DELETE oldReport
|
||||||
|
|
||||||
|
RETURN notDisabledResource {.id};
|
||||||
|
" | cypher-shell
|
||||||
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
#!/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! Creation date is when the database manipulation happened.'
|
|
||||||
RETURN reported;
|
|
||||||
" | cypher-shell
|
|
||||||
@ -1,19 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="comments">
|
<div id="comments">
|
||||||
<h3 style="margin-top: -10px;">
|
<h3 style="margin-top: -10px;">
|
||||||
<span>
|
<counter-icon icon="comments" :count="post.comments.length">
|
||||||
<base-icon name="comments" />
|
{{ $t('common.comment', null, 0) }}
|
||||||
<ds-tag
|
</counter-icon>
|
||||||
v-if="post.comments.length"
|
|
||||||
style="margin-top: -4px; margin-left: -12px; position: absolute;"
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
round
|
|
||||||
>
|
|
||||||
{{ post.comments.length }}
|
|
||||||
</ds-tag>
|
|
||||||
<span class="list-title">{{ $t('common.comment', null, 0) }}</span>
|
|
||||||
</span>
|
|
||||||
</h3>
|
</h3>
|
||||||
<ds-space margin-bottom="large" />
|
<ds-space margin-bottom="large" />
|
||||||
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
||||||
@ -31,12 +21,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||||
import Comment from '~/components/Comment/Comment'
|
import Comment from '~/components/Comment/Comment'
|
||||||
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [scrollToAnchor],
|
mixins: [scrollToAnchor],
|
||||||
components: {
|
components: {
|
||||||
|
CounterIcon,
|
||||||
Comment,
|
Comment,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -58,9 +50,3 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.list-title {
|
|
||||||
margin-left: $space-x-small;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -85,7 +85,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'post.menu.delete')
|
.filter(item => item.text() === 'post.menu.delete')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('delete')
|
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'comment.menu.delete')
|
.filter(item => item.text() === 'comment.menu.delete')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('delete')
|
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'release.contribution.title')
|
.filter(item => item.text() === 'release.contribution.title')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
|
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can release comments', () => {
|
it('can release comments', () => {
|
||||||
@ -350,7 +350,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'release.comment.title')
|
.filter(item => item.text() === 'release.comment.title')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
|
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can release users', () => {
|
it('can release users', () => {
|
||||||
@ -368,7 +368,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'release.user.title')
|
.filter(item => item.text() === 'release.user.title')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
|
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can release organizations', () => {
|
it('can release organizations', () => {
|
||||||
@ -386,7 +386,7 @@ describe('ContentMenu.vue', () => {
|
|||||||
.filter(item => item.text() === 'release.organization.title')
|
.filter(item => item.text() === 'release.organization.title')
|
||||||
.at(0)
|
.at(0)
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
|
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export default {
|
|||||||
routes.push({
|
routes.push({
|
||||||
name: this.$t(`post.menu.delete`),
|
name: this.$t(`post.menu.delete`),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.openModal('delete')
|
this.openModal('confirm', 'delete')
|
||||||
},
|
},
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
})
|
})
|
||||||
@ -108,7 +108,7 @@ export default {
|
|||||||
routes.push({
|
routes.push({
|
||||||
name: this.$t(`comment.menu.delete`),
|
name: this.$t(`comment.menu.delete`),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.openModal('delete')
|
this.openModal('confirm', 'delete')
|
||||||
},
|
},
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
})
|
})
|
||||||
@ -137,7 +137,7 @@ export default {
|
|||||||
routes.push({
|
routes.push({
|
||||||
name: this.$t(`release.${this.resourceType}.title`),
|
name: this.$t(`release.${this.resourceType}.title`),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.openModal('release', this.resource.id)
|
this.openModal('release')
|
||||||
},
|
},
|
||||||
icon: 'eye',
|
icon: 'eye',
|
||||||
})
|
})
|
||||||
@ -190,13 +190,13 @@ export default {
|
|||||||
}
|
}
|
||||||
toggleMenu()
|
toggleMenu()
|
||||||
},
|
},
|
||||||
openModal(dialog) {
|
openModal(dialog, modalDataName = null) {
|
||||||
this.$store.commit('modal/SET_OPEN', {
|
this.$store.commit('modal/SET_OPEN', {
|
||||||
name: dialog,
|
name: dialog,
|
||||||
data: {
|
data: {
|
||||||
type: this.resourceType,
|
type: this.resourceType,
|
||||||
resource: this.resource,
|
resource: this.resource,
|
||||||
modalsData: this.modalsData,
|
modalData: modalDataName ? this.modalsData[modalDataName] : {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,9 +64,9 @@ describe('DropdownFilter.vue', () => {
|
|||||||
expect(unreadLink.text()).toEqual('Unread')
|
expect(unreadLink.text()).toEqual('Unread')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clicking on menu item emits filterNotifications', () => {
|
it('clicking on menu item emits filter', () => {
|
||||||
allLink.trigger('click')
|
allLink.trigger('click')
|
||||||
expect(wrapper.emitted().filterNotifications[0]).toEqual(
|
expect(wrapper.emitted().filter[0]).toEqual(
|
||||||
propsData.filterOptions.filter(option => option.label === 'All'),
|
propsData.filterOptions.filter(option => option.label === 'All'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -20,10 +20,10 @@ storiesOf('DropdownFilter', module)
|
|||||||
selected: filterOptions[0].label,
|
selected: filterOptions[0].label,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
filterNotifications: action('filterNotifications'),
|
filter: action('filter'),
|
||||||
},
|
},
|
||||||
template: `<dropdown-filter
|
template: `<dropdown-filter
|
||||||
@filterNotifications="filterNotifications"
|
@filter="filter"
|
||||||
:filterOptions="filterOptions"
|
:filterOptions="filterOptions"
|
||||||
:selected="selected"
|
:selected="selected"
|
||||||
/>`,
|
/>`,
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
class="dropdown-menu-item"
|
class="dropdown-menu-item"
|
||||||
:route="item.route"
|
:route="item.route"
|
||||||
:parents="item.parents"
|
:parents="item.parents"
|
||||||
@click.stop.prevent="filterNotifications(item.route, toggleMenu)"
|
@click.stop.prevent="filter(item.route, toggleMenu)"
|
||||||
>
|
>
|
||||||
{{ item.route.label }}
|
{{ item.route.label }}
|
||||||
</ds-menu-item>
|
</ds-menu-item>
|
||||||
@ -44,8 +44,8 @@ export default {
|
|||||||
filterOptions: { type: Array, default: () => [] },
|
filterOptions: { type: Array, default: () => [] },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterNotifications(option, toggleMenu) {
|
filter(option, toggleMenu) {
|
||||||
this.$emit('filterNotifications', option)
|
this.$emit('filter', option)
|
||||||
toggleMenu()
|
toggleMenu()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,11 +23,11 @@
|
|||||||
@close="close"
|
@close="close"
|
||||||
/>
|
/>
|
||||||
<confirm-modal
|
<confirm-modal
|
||||||
v-if="open === 'delete'"
|
v-if="open === 'confirm'"
|
||||||
:id="data.resource.id"
|
:id="data.resource.id"
|
||||||
:type="data.type"
|
:type="data.type"
|
||||||
:name="name"
|
:name="name"
|
||||||
:modalData="data.modalsData.delete"
|
:modalData="data.modalData"
|
||||||
@close="close"
|
@close="close"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export default {
|
|||||||
}, 500)
|
}, 500)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.success = false
|
this.isOpen = false
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,7 @@ describe('DisableModal.vue', () => {
|
|||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest
|
mutate: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce()
|
||||||
enable: 'u4711',
|
|
||||||
})
|
|
||||||
.mockRejectedValue({
|
.mockRejectedValue({
|
||||||
message: 'Not Authorised!',
|
message: 'Not Authorised!',
|
||||||
}),
|
}),
|
||||||
@ -159,11 +157,13 @@ describe('DisableModal.vue', () => {
|
|||||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes id to mutation', () => {
|
it('passes parameters to mutation', () => {
|
||||||
const calls = mocks.$apollo.mutate.mock.calls
|
const calls = mocks.$apollo.mutate.mock.calls
|
||||||
const [[{ variables }]] = calls
|
const [[{ variables }]] = calls
|
||||||
expect(variables).toEqual({
|
expect(variables).toMatchObject({
|
||||||
id: 'u4711',
|
resourceId: 'u4711',
|
||||||
|
disable: true,
|
||||||
|
closed: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -54,11 +54,13 @@ export default {
|
|||||||
// await this.modalData.buttons.confirm.callback()
|
// await this.modalData.buttons.confirm.callback()
|
||||||
await this.$apollo.mutate({
|
await this.$apollo.mutate({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation($id: ID!) {
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
disable(id: $id)
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
|
disable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { id: this.id },
|
variables: { resourceId: this.id, disable: true, closed: false },
|
||||||
})
|
})
|
||||||
this.$toast.success(this.$t('disable.success'))
|
this.$toast.success(this.$t('disable.success'))
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
@ -67,6 +69,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
|
this.isOpen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -149,6 +149,7 @@ export default {
|
|||||||
default:
|
default:
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
}
|
}
|
||||||
|
this.isOpen = false
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -27,7 +27,7 @@ describe('ReleaseModal.vue', () => {
|
|||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest
|
mutate: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce({ enable: 'u4711' })
|
.mockResolvedValueOnce()
|
||||||
.mockRejectedValue({ message: 'Not Authorised!' }),
|
.mockRejectedValue({ message: 'Not Authorised!' }),
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
@ -154,11 +154,13 @@ describe('ReleaseModal.vue', () => {
|
|||||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes id to mutation', () => {
|
it('passes parameters to mutation', () => {
|
||||||
const calls = mocks.$apollo.mutate.mock.calls
|
const calls = mocks.$apollo.mutate.mock.calls
|
||||||
const [[{ variables }]] = calls
|
const [[{ variables }]] = calls
|
||||||
expect(variables).toEqual({
|
expect(variables).toMatchObject({
|
||||||
id: 'u4711',
|
resourceId: 'u4711',
|
||||||
|
disable: false,
|
||||||
|
closed: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -53,11 +53,13 @@ export default {
|
|||||||
// await this.modalData.buttons.confirm.callback()
|
// await this.modalData.buttons.confirm.callback()
|
||||||
await this.$apollo.mutate({
|
await this.$apollo.mutate({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation($id: ID!) {
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
enable(id: $id)
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
|
disable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { id: this.id },
|
variables: { resourceId: this.id, disable: false, closed: false },
|
||||||
})
|
})
|
||||||
this.$toast.success(this.$t('release.success'))
|
this.$toast.success(this.$t('release.success'))
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
@ -66,6 +68,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
|
this.isOpen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user" v-if="displayAnonymous">
|
<div class="user" v-if="displayAnonymous">
|
||||||
<hc-avatar class="avatar" />
|
<hc-avatar v-if="showAvatar" class="avatar" />
|
||||||
<div>
|
<div>
|
||||||
<b class="username">{{ $t('profile.userAnonym') }}</b>
|
<b class="username">{{ $t('profile.userAnonym') }}</b>
|
||||||
</div>
|
</div>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
|
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
|
||||||
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
|
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
|
||||||
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
|
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
|
||||||
<hc-avatar class="avatar" :user="user" />
|
<hc-avatar v-if="showAvatar" class="avatar" :user="user" />
|
||||||
<div>
|
<div>
|
||||||
<ds-text class="userinfo">
|
<ds-text class="userinfo">
|
||||||
<b>{{ userSlug }}</b>
|
<b>{{ userSlug }}</b>
|
||||||
@ -17,8 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<ds-text class="username" align="left" size="small" color="soft">
|
<ds-text class="username" align="left" size="small" color="soft">
|
||||||
{{ userName | truncate(18) }}
|
{{ userName | truncate(18) }}
|
||||||
<base-icon name="clock" />
|
|
||||||
<template v-if="dateTime">
|
<template v-if="dateTime">
|
||||||
|
<base-icon name="clock" />
|
||||||
<hc-relative-date-time :date-time="dateTime" />
|
<hc-relative-date-time :date-time="dateTime" />
|
||||||
<slot name="dateTime"></slot>
|
<slot name="dateTime"></slot>
|
||||||
</template>
|
</template>
|
||||||
@ -103,7 +103,8 @@ export default {
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, default: null },
|
user: { type: Object, default: null },
|
||||||
trunc: { type: Number, default: null },
|
showAvatar: { type: Boolean, default: true },
|
||||||
|
trunc: { type: Number, default: 18 }, // "-1" is no trunc
|
||||||
dateTime: { type: [Date, String], default: null },
|
dateTime: { type: [Date, String], default: null },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -176,4 +177,8 @@ export default {
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-slug {
|
||||||
|
margin-bottom: $space-xx-small;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import CounterIcon from './CounterIcon'
|
||||||
|
import BaseIcon from '../BaseIcon/BaseIcon'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('CounterIcon.vue', () => {
|
||||||
|
let propsData, wrapper, tag
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(CounterIcon, { propsData, localVue })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given a valid icon name and count', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { icon: 'comments', count: 1 }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
tag = wrapper.find('.ds-tag')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders BaseIcon', () => {
|
||||||
|
expect(wrapper.find(BaseIcon).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the count', () => {
|
||||||
|
expect(tag.text()).toEqual('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a round tag', () => {
|
||||||
|
expect(tag.classes()).toContain('ds-tag-round')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a primary button', () => {
|
||||||
|
expect(tag.classes()).toContain('ds-tag-primary')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { storiesOf } from '@storybook/vue'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
import CounterIcon from './CounterIcon.vue'
|
||||||
|
|
||||||
|
storiesOf('CounterIcon', module)
|
||||||
|
.addDecorator(helpers.layout)
|
||||||
|
.add('flag icon with button in slot position', () => ({
|
||||||
|
components: { CounterIcon },
|
||||||
|
data() {
|
||||||
|
return { icon: 'flag', count: 3 }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<counter-icon icon="pizza" :count="count">
|
||||||
|
<ds-button ghost primary>
|
||||||
|
Report Details
|
||||||
|
</ds-button>
|
||||||
|
</counter-icon>
|
||||||
|
`,
|
||||||
|
}))
|
||||||
29
webapp/components/_new/generic/CounterIcon/CounterIcon.vue
Normal file
29
webapp/components/_new/generic/CounterIcon/CounterIcon.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<base-icon :name="icon" />
|
||||||
|
<ds-tag
|
||||||
|
style="margin-top: -4px; margin-left: -12px; position: absolute;"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ count }}
|
||||||
|
</ds-tag>
|
||||||
|
<span class="counter-icon-text">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
icon: { type: String, required: true },
|
||||||
|
count: { type: Number, required: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.counter-icon-text {
|
||||||
|
margin-left: $space-xx-small;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import { config, mount, RouterLinkStub } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import FiledReportsTable from './FiledReportsTable'
|
||||||
|
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
localVue.filter('truncate', string => string)
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('FiledReportsTable.vue', () => {
|
||||||
|
let wrapper, mocks, propsData, stubs, filed
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(string => string),
|
||||||
|
}
|
||||||
|
stubs = {
|
||||||
|
NuxtLink: RouterLinkStub,
|
||||||
|
}
|
||||||
|
propsData = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters: {
|
||||||
|
'auth/isModerator': () => true,
|
||||||
|
'auth/user': () => {
|
||||||
|
return { id: 'moderator' }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return mount(FiledReportsTable, {
|
||||||
|
propsData,
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
store,
|
||||||
|
stubs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given reports', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
filed = reports.map(report => report.filed)
|
||||||
|
propsData.filed = filed[0]
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a table', () => {
|
||||||
|
expect(wrapper.find('.ds-table').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has 4 columns', () => {
|
||||||
|
expect(wrapper.findAll('.ds-table-col')).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('FiledReport', () => {
|
||||||
|
it('renders the reporting user', () => {
|
||||||
|
const userSlug = wrapper.find('[data-test="filing-user"]')
|
||||||
|
expect(userSlug.text()).toContain('@community-moderator')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the reported date', () => {
|
||||||
|
const date = wrapper.find('[data-test="filed-date"]')
|
||||||
|
expect(date.text()).toEqual('10/02/2019')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the category text', () => {
|
||||||
|
const columns = wrapper.findAll('.ds-table-col')
|
||||||
|
const reasonCategory = columns.filter(
|
||||||
|
category =>
|
||||||
|
category.text() === 'report.reason.category.options.pornographic_content_links',
|
||||||
|
)
|
||||||
|
expect(reasonCategory.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders the Post's content", () => {
|
||||||
|
const columns = wrapper.findAll('.ds-table-col')
|
||||||
|
const reasonDescription = columns.filter(
|
||||||
|
column => column.text() === 'This comment is porno!!!',
|
||||||
|
)
|
||||||
|
expect(reasonDescription.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { storiesOf } from '@storybook/vue'
|
||||||
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
|
import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||||
|
|
||||||
|
storiesOf('FiledReportsTable', module)
|
||||||
|
.addDecorator(withA11y)
|
||||||
|
.addDecorator(helpers.layout)
|
||||||
|
.add('with filed reports', () => ({
|
||||||
|
components: { FiledReportsTable },
|
||||||
|
store: helpers.store,
|
||||||
|
data: () => ({
|
||||||
|
filed: reports[0].filed,
|
||||||
|
}),
|
||||||
|
template: `<table>
|
||||||
|
<tbody class="report-row">
|
||||||
|
<tr class="row">
|
||||||
|
<td colspan="100%">
|
||||||
|
<filed-reports-table :filed="filed" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>`,
|
||||||
|
}))
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<ds-table
|
||||||
|
class="nested-table"
|
||||||
|
v-if="filed && filed.length"
|
||||||
|
:data="filed"
|
||||||
|
:fields="fields"
|
||||||
|
condensed
|
||||||
|
>
|
||||||
|
<template #submitter="scope">
|
||||||
|
<hc-user
|
||||||
|
:user="scope.row.submitter"
|
||||||
|
:showAvatar="false"
|
||||||
|
:trunc="30"
|
||||||
|
data-test="filing-user"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #reportedOn="scope">
|
||||||
|
<ds-text size="small">
|
||||||
|
<hc-relative-date-time :date-time="scope.row.createdAt" data-test="filed-date" />
|
||||||
|
</ds-text>
|
||||||
|
</template>
|
||||||
|
<template #reasonCategory="scope">
|
||||||
|
{{ $t('report.reason.category.options.' + scope.row.reasonCategory) }}
|
||||||
|
</template>
|
||||||
|
<template #reasonDescription="scope">
|
||||||
|
{{ scope.row.reasonDescription.length ? scope.row.reasonDescription : '—' }}
|
||||||
|
</template>
|
||||||
|
</ds-table>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import HcUser from '~/components/User/User'
|
||||||
|
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcUser,
|
||||||
|
HcRelativeDateTime,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
filed: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fields() {
|
||||||
|
return {
|
||||||
|
submitter: {
|
||||||
|
label: this.$t('moderation.reports.submitter'),
|
||||||
|
width: '15%',
|
||||||
|
},
|
||||||
|
reportedOn: {
|
||||||
|
label: this.$t('moderation.reports.reportedOn'),
|
||||||
|
width: '20%',
|
||||||
|
},
|
||||||
|
reasonCategory: {
|
||||||
|
label: this.$t('moderation.reports.reasonCategory'),
|
||||||
|
width: '30%',
|
||||||
|
},
|
||||||
|
reasonDescription: {
|
||||||
|
label: this.$t('moderation.reports.reasonDescription'),
|
||||||
|
width: '35%',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.nested-table {
|
||||||
|
padding: $space-small;
|
||||||
|
border-top: $border-size-base solid $color-neutral-60;
|
||||||
|
border-bottom: $border-size-base solid $color-neutral-60;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
77
webapp/components/features/ReportList/ReportList.spec.js
Normal file
77
webapp/components/features/ReportList/ReportList.spec.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { config, mount } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import ReportList from './ReportList'
|
||||||
|
import { reports } from './ReportList.story'
|
||||||
|
import ReportsTable from '~/components/features/ReportsTable/ReportsTable'
|
||||||
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('ReportList', () => {
|
||||||
|
let mocks, mutations, getters, wrapper
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: { review: { disable: true, resourceId: 'some-resource', closed: true } },
|
||||||
|
})
|
||||||
|
.mockRejectedValue({ message: 'Unable to review' }),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$toast: {
|
||||||
|
error: jest.fn(message => message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mutations = {
|
||||||
|
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { slug: 'awesome-user' }
|
||||||
|
},
|
||||||
|
'auth/isModerator': () => true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
mutations,
|
||||||
|
getters,
|
||||||
|
})
|
||||||
|
return mount(ReportList, { mocks, localVue, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('renders children components', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders DropdownFilter', () => {
|
||||||
|
expect(wrapper.find(DropdownFilter).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders ReportsTable', () => {
|
||||||
|
expect(wrapper.find(ReportsTable).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('confirm is emitted by reports table', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.setData({ reports })
|
||||||
|
wrapper.find(ReportsTable).vm.$emit('confirm', reports[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls modal/SET_OPEN', () => {
|
||||||
|
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
193
webapp/components/features/ReportList/ReportList.story.js
Normal file
193
webapp/components/features/ReportList/ReportList.story.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { storiesOf } from '@storybook/vue'
|
||||||
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
|
import { action } from '@storybook/addon-actions'
|
||||||
|
import { post } from '~/components/PostCard/PostCard.story.js'
|
||||||
|
import { user } from '~/components/User/User.story.js'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
import ReportList from './ReportList'
|
||||||
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
|
import ReportsTable from '~/components/features/ReportsTable/ReportsTable'
|
||||||
|
helpers.init()
|
||||||
|
|
||||||
|
export const reports = [
|
||||||
|
{
|
||||||
|
__typename: 'Report',
|
||||||
|
closed: false,
|
||||||
|
createdAt: '2019-10-29T15:36:02.106Z',
|
||||||
|
updatedAt: '2019-12-02T15:56:35.651Z',
|
||||||
|
disable: false,
|
||||||
|
filed: [
|
||||||
|
{
|
||||||
|
__typename: 'FILED',
|
||||||
|
createdAt: '2019-10-02T15:56:35.676Z',
|
||||||
|
reasonCategory: 'pornographic_content_links',
|
||||||
|
reasonDescription: 'This comment is porno!!!',
|
||||||
|
submitter: {
|
||||||
|
...user,
|
||||||
|
name: 'Community moderator',
|
||||||
|
id: 'community-moderator',
|
||||||
|
slug: 'community-moderator',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resource: {
|
||||||
|
__typename: 'Comment',
|
||||||
|
id: 'b6b38937-3efc-4d5e-b12c-549e4d6551a5',
|
||||||
|
createdAt: '2019-10-29T15:38:25.184Z',
|
||||||
|
updatedAt: '2019-10-30T15:38:25.184Z',
|
||||||
|
disabled: false,
|
||||||
|
deleted: false,
|
||||||
|
content:
|
||||||
|
'<p><a class="mention" href="/profile/u1" data-mention-id="u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra magna ac placerat. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nibh praesent tristique magna sit amet purus gravida quis blandit. Magna eget est lorem ipsum dolor. In fermentum posuere urna nec. Eleifend donec pretium vulputate sapien nec sagittis aliquam. Augue interdum velit euismod in pellentesque. Id diam maecenas ultricies mi eget mauris pharetra. Donec pretium vulputate sapien nec. Dolor morbi non arcu risus quis varius quam quisque. Blandit turpis cursus in hac habitasse. Est ultricies integer quis auctor elit sed vulputate mi sit. Nunc consequat interdum varius sit amet mattis vulputate enim. Semper feugiat nibh sed pulvinar. Eget felis eget nunc lobortis mattis aliquam. Ultrices vitae auctor eu augue. Tellus molestie nunc non blandit massa enim nec dui. Pharetra massa massa ultricies mi quis hendrerit dolor. Nisl suscipit adipiscing bibendum est ultricies integer.</p>',
|
||||||
|
contentExcerpt:
|
||||||
|
'<p><a href="/profile/u1" target="_blank">@peter-lustig</a> </p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis egestas pretium aenean pharetra …</p>',
|
||||||
|
post,
|
||||||
|
author: user,
|
||||||
|
},
|
||||||
|
reviewed: [
|
||||||
|
{
|
||||||
|
updatedAt: '2019-10-30T15:38:25.184Z',
|
||||||
|
moderator: {
|
||||||
|
__typename: 'User',
|
||||||
|
...user,
|
||||||
|
name: 'Moderator',
|
||||||
|
id: 'moderator',
|
||||||
|
slug: 'moderator',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
updatedAt: '2019-10-29T15:38:25.184Z',
|
||||||
|
moderator: {
|
||||||
|
__typename: 'User',
|
||||||
|
...user,
|
||||||
|
name: 'Peter Lustig',
|
||||||
|
id: 'u3',
|
||||||
|
slug: 'peter-lustig',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Report',
|
||||||
|
closed: false,
|
||||||
|
createdAt: '2019-10-31T15:36:02.106Z',
|
||||||
|
updatedAt: '2019-12-03T15:56:35.651Z',
|
||||||
|
disable: true,
|
||||||
|
filed: [
|
||||||
|
{
|
||||||
|
__typename: 'FILED',
|
||||||
|
createdAt: '2019-10-31T15:36:02.106Z',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'This post is bigoted',
|
||||||
|
submitter: {
|
||||||
|
...user,
|
||||||
|
name: 'Modertation team',
|
||||||
|
id: 'moderation-team',
|
||||||
|
slug: 'moderation-team',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resource: {
|
||||||
|
__typename: 'Post',
|
||||||
|
author: {
|
||||||
|
...user,
|
||||||
|
id: 'u7',
|
||||||
|
name: 'Dagobert',
|
||||||
|
slug: 'dagobert',
|
||||||
|
},
|
||||||
|
deleted: false,
|
||||||
|
disabled: false,
|
||||||
|
id: 'p2',
|
||||||
|
slug: 'bigoted-post',
|
||||||
|
title: "I'm a bigoted post!",
|
||||||
|
},
|
||||||
|
reviewed: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Report',
|
||||||
|
closed: true,
|
||||||
|
createdAt: '2019-10-30T15:36:02.106Z',
|
||||||
|
updatedAt: '2019-12-01T15:56:35.651Z',
|
||||||
|
disable: true,
|
||||||
|
filed: [
|
||||||
|
{
|
||||||
|
__typename: 'FILED',
|
||||||
|
createdAt: '2019-10-30T15:36:02.106Z',
|
||||||
|
reasonCategory: 'discrimination_etc',
|
||||||
|
reasonDescription: 'this user is attacking me for who I am!',
|
||||||
|
submitter: {
|
||||||
|
...user,
|
||||||
|
name: 'Helpful user',
|
||||||
|
id: 'helpful-user',
|
||||||
|
slug: 'helpful-user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resource: {
|
||||||
|
__typename: 'User',
|
||||||
|
commentedCount: 0,
|
||||||
|
contributionsCount: 0,
|
||||||
|
deleted: false,
|
||||||
|
disabled: true,
|
||||||
|
followedByCount: 0,
|
||||||
|
id: 'u5',
|
||||||
|
name: 'Abusive user',
|
||||||
|
slug: 'abusive-user',
|
||||||
|
},
|
||||||
|
reviewed: [
|
||||||
|
{
|
||||||
|
updatedAt: '2019-12-01T15:56:35.651Z',
|
||||||
|
moderator: {
|
||||||
|
__typename: 'User',
|
||||||
|
...user,
|
||||||
|
name: 'Peter Lustig',
|
||||||
|
id: 'u3',
|
||||||
|
slug: 'peter-lustig',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
updatedAt: '2019-11-30T15:56:35.651Z',
|
||||||
|
moderator: {
|
||||||
|
__typename: 'User',
|
||||||
|
...user,
|
||||||
|
name: 'Moderator',
|
||||||
|
id: 'moderator',
|
||||||
|
slug: 'moderator',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const unreviewedReports = reports.filter(report => !report.reviewed)
|
||||||
|
const reviewedReports = reports.filter(report => report.reviewed)
|
||||||
|
const closedReports = reports.filter(report => report.closed)
|
||||||
|
const filterOptions = [
|
||||||
|
{ label: 'All', value: reports },
|
||||||
|
{ label: 'Unreviewed', value: unreviewedReports },
|
||||||
|
{ label: 'Reviewed', value: reviewedReports },
|
||||||
|
{ label: 'Closed', value: closedReports },
|
||||||
|
]
|
||||||
|
|
||||||
|
storiesOf('ReportList', module)
|
||||||
|
.addDecorator(withA11y)
|
||||||
|
.addDecorator(helpers.layout)
|
||||||
|
.add('with reports', () => ({
|
||||||
|
components: { ReportList, DropdownFilter, ReportsTable },
|
||||||
|
store: helpers.store,
|
||||||
|
data: () => ({
|
||||||
|
filterOptions,
|
||||||
|
selected: filterOptions[0].label,
|
||||||
|
reports,
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
openModal: action('openModal'),
|
||||||
|
filter: action('filter'),
|
||||||
|
},
|
||||||
|
template: `<ds-card>
|
||||||
|
<div class="reports-header">
|
||||||
|
<h3 class="title">Reports</h3>
|
||||||
|
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||||
|
</div>
|
||||||
|
<reports-table :reports="reports" @confirm="openModal" />
|
||||||
|
</ds-card>`,
|
||||||
|
}))
|
||||||
142
webapp/components/features/ReportList/ReportList.vue
Normal file
142
webapp/components/features/ReportList/ReportList.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card>
|
||||||
|
<div class="reports-header">
|
||||||
|
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
|
||||||
|
<client-only>
|
||||||
|
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||||
|
</client-only>
|
||||||
|
</div>
|
||||||
|
<reports-table :reports="reports" @confirm="openModal" />
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapMutations } from 'vuex'
|
||||||
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
|
import ReportsTable from '~/components/features/ReportsTable/ReportsTable'
|
||||||
|
import { reportsListQuery, reviewMutation } from '~/graphql/Moderation.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DropdownFilter,
|
||||||
|
ReportsTable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
reports: [],
|
||||||
|
allReports: [],
|
||||||
|
unreviewedReports: [],
|
||||||
|
reviewedReports: [],
|
||||||
|
closedReports: [],
|
||||||
|
selected: this.$t('moderation.reports.filterLabel.all'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filterOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.$t('moderation.reports.filterLabel.all'), value: this.allReports },
|
||||||
|
{
|
||||||
|
label: this.$t('moderation.reports.filterLabel.unreviewed'),
|
||||||
|
value: this.unreviewedReports,
|
||||||
|
},
|
||||||
|
{ label: this.$t('moderation.reports.filterLabel.reviewed'), value: this.reviewedReports },
|
||||||
|
{ label: this.$t('moderation.reports.filterLabel.closed'), value: this.closedReports },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
modalData() {
|
||||||
|
return function(report) {
|
||||||
|
const identStart =
|
||||||
|
'moderation.reports.decideModal.' +
|
||||||
|
report.resource.__typename +
|
||||||
|
'.' +
|
||||||
|
(report.resource.disabled ? 'disable' : 'enable')
|
||||||
|
return {
|
||||||
|
name: 'confirm',
|
||||||
|
data: {
|
||||||
|
type: report.resource.__typename,
|
||||||
|
resource: report.resource,
|
||||||
|
modalData: {
|
||||||
|
titleIdent: identStart + '.title',
|
||||||
|
messageIdent: identStart + '.message',
|
||||||
|
messageParams: {
|
||||||
|
name:
|
||||||
|
report.resource.name ||
|
||||||
|
this.$filters.truncate(report.resource.title, 30) ||
|
||||||
|
this.$filters.truncate(
|
||||||
|
this.$filters.removeHtml(report.resource.contentExcerpt),
|
||||||
|
30,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
danger: true,
|
||||||
|
icon: report.resource.disabled ? 'eye-slash' : 'eye',
|
||||||
|
textIdent: 'moderation.reports.decideModal.submit',
|
||||||
|
callback: () => {
|
||||||
|
this.confirmCallback(report.resource)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
icon: 'close',
|
||||||
|
textIdent: 'moderation.reports.decideModal.cancel',
|
||||||
|
callback: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
commitModalData: 'modal/SET_OPEN',
|
||||||
|
}),
|
||||||
|
filter(option) {
|
||||||
|
this.reports = option.value
|
||||||
|
this.selected = option.label
|
||||||
|
},
|
||||||
|
async confirmCallback(resource) {
|
||||||
|
const { disabled: disable, id: resourceId } = resource
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: reviewMutation(),
|
||||||
|
variables: { disable, resourceId, closed: true },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$t('moderation.reports.DecisionSuccess'))
|
||||||
|
this.$apollo.queries.reportsList.refetch()
|
||||||
|
})
|
||||||
|
.catch(error => this.$toast.error(error.message))
|
||||||
|
},
|
||||||
|
openModal(report) {
|
||||||
|
this.commitModalData(this.modalData(report))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
reportsList: {
|
||||||
|
query: reportsListQuery(),
|
||||||
|
update({ reports }) {
|
||||||
|
this.reports = reports
|
||||||
|
this.allReports = reports
|
||||||
|
this.unreviewedReports = reports.filter(report => !report.reviewed)
|
||||||
|
this.reviewedReports = reports.filter(report => report.reviewed)
|
||||||
|
this.closedReports = reports.filter(report => report.closed)
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.reports-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: $space-small 0;
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-size-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
187
webapp/components/features/ReportRow/ReportRow.spec.js
Normal file
187
webapp/components/features/ReportRow/ReportRow.spec.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { config, mount, RouterLinkStub } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import ReportRow from './ReportRow.vue'
|
||||||
|
import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon'
|
||||||
|
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('ReportRow', () => {
|
||||||
|
let propsData, mocks, stubs, getters, wrapper
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {}
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(string => string),
|
||||||
|
}
|
||||||
|
stubs = {
|
||||||
|
NuxtLink: RouterLinkStub,
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { slug: 'awesome-user' }
|
||||||
|
},
|
||||||
|
'auth/isModerator': () => true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a report ', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters,
|
||||||
|
})
|
||||||
|
return mount(ReportRow, { propsData, mocks, stubs, localVue, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('has not been closed', () => {
|
||||||
|
let confirmButton
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[1] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
confirmButton = wrapper.find('.ds-button-danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a confirm button', () => {
|
||||||
|
expect(confirmButton.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits confirm event', () => {
|
||||||
|
confirmButton.trigger('click')
|
||||||
|
expect(wrapper.emitted('confirm-report')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has been closed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[2] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a decided text', () => {
|
||||||
|
const decidedTitle = wrapper
|
||||||
|
.findAll('.title')
|
||||||
|
.filter(title => title.text() === 'moderation.reports.decided')
|
||||||
|
expect(decidedTitle.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has not been reviewed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[1] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders its current status', () => {
|
||||||
|
expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.enabled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has been reviewed', () => {
|
||||||
|
describe('and disabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[2] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
it('renders the disabled icon', () => {
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('.status-line')
|
||||||
|
.find(BaseIcon)
|
||||||
|
.props().name,
|
||||||
|
).toEqual('eye-slash')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders its current status', () => {
|
||||||
|
expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.disabledBy')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and enabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[0] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
it('renders the enabled icon', () => {
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('.status-line')
|
||||||
|
.find(BaseIcon)
|
||||||
|
.props().name,
|
||||||
|
).toEqual('eye')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders its current status', () => {
|
||||||
|
expect(wrapper.find('.status-line').text()).toEqual('moderation.reports.enabledBy')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the moderator who reviewed the resource', () => {
|
||||||
|
const username = wrapper.find('[data-test="report-reviewer"]')
|
||||||
|
expect(username.text()).toContain('@moderator')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('concerns a Comment', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[0] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a comments icon', () => {
|
||||||
|
const commentsIcon = wrapper.find(BaseIcon).props().name
|
||||||
|
expect(commentsIcon).toEqual('comments')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a link to the post, with the comment contentExcerpt', () => {
|
||||||
|
const postLink = wrapper.find('.title')
|
||||||
|
expect(postLink.text()).toEqual('@peter-lustig Lorem ipsum dolor sit amet, …')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the author', () => {
|
||||||
|
const userSlug = wrapper.find('[data-test="report-author"]')
|
||||||
|
expect(userSlug.text()).toContain('@louie')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('concerns a Post', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[1] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a bookmark icon', () => {
|
||||||
|
const postIcon = wrapper.find(BaseIcon).props().name
|
||||||
|
expect(postIcon).toEqual('bookmark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a link to the post', () => {
|
||||||
|
const postLink = wrapper.find('.title')
|
||||||
|
expect(postLink.text()).toEqual("I'm a bigoted post!")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the author', () => {
|
||||||
|
const username = wrapper.find('[data-test="report-author"]')
|
||||||
|
expect(username.text()).toContain('@dagobert')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('concerns a User', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, report: reports[2] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a user icon', () => {
|
||||||
|
const userIcon = wrapper.find(BaseIcon).props().name
|
||||||
|
expect(userIcon).toEqual('user')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a link to the user profile', () => {
|
||||||
|
const userLink = wrapper.find('[data-test="report-content"]')
|
||||||
|
expect(userLink.text()).toContain('@abusive-user')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
183
webapp/components/features/ReportRow/ReportRow.vue
Normal file
183
webapp/components/features/ReportRow/ReportRow.vue
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<tbody class="report-row">
|
||||||
|
<tr>
|
||||||
|
<!-- Icon Column -->
|
||||||
|
<td class="ds-table-col">
|
||||||
|
<base-icon :name="iconName" :title="iconLabel" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Number of Filed Reports Column -->
|
||||||
|
<td class="ds-table-col">
|
||||||
|
<span class="user-count">
|
||||||
|
{{ $t('moderation.reports.numberOfUsers', { count: report.filed.length }) }}
|
||||||
|
</span>
|
||||||
|
<ds-button size="small" @click="showFiledReports = !showFiledReports">
|
||||||
|
{{ $t('moderation.reports.moreDetails') }}
|
||||||
|
</ds-button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Content Column -->
|
||||||
|
<td class="ds-table-col" data-test="report-content">
|
||||||
|
<client-only v-if="isUser">
|
||||||
|
<hc-user :user="report.resource" :showAvatar="false" :trunc="30" />
|
||||||
|
</client-only>
|
||||||
|
<nuxt-link v-else class="title" :to="linkTarget">
|
||||||
|
{{ linkText | truncate(50) }}
|
||||||
|
</nuxt-link>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Author Column -->
|
||||||
|
<td class="ds-table-col" data-test="report-author">
|
||||||
|
<client-only v-if="!isUser">
|
||||||
|
<hc-user :user="report.resource.author" :showAvatar="false" :trunc="30" />
|
||||||
|
</client-only>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<td class="ds-table-col" data-test="report-reviewer">
|
||||||
|
<span class="status-line">
|
||||||
|
<base-icon :name="statusIconName" :class="isDisabled ? '--disabled' : '--enabled'" />
|
||||||
|
{{ statusText }}
|
||||||
|
</span>
|
||||||
|
<client-only v-if="report.reviewed">
|
||||||
|
<hc-user
|
||||||
|
:user="moderatorOfLatestReview"
|
||||||
|
:showAvatar="false"
|
||||||
|
:trunc="30"
|
||||||
|
:date-time="report.updatedAt"
|
||||||
|
/>
|
||||||
|
</client-only>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Decision Column -->
|
||||||
|
<td class="ds-table-col">
|
||||||
|
<span v-if="report.closed" class="title">
|
||||||
|
{{ $t('moderation.reports.decided') }}
|
||||||
|
</span>
|
||||||
|
<ds-button
|
||||||
|
v-else
|
||||||
|
danger
|
||||||
|
data-test="confirm"
|
||||||
|
size="small"
|
||||||
|
:icon="statusIconName"
|
||||||
|
@click="$emit('confirm-report')"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.decideButton') }}
|
||||||
|
</ds-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="row">
|
||||||
|
<td colspan="100%">
|
||||||
|
<filed-reports-table :filed="report.filed" v-if="showFiledReports" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable'
|
||||||
|
import HcUser from '~/components/User/User'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FiledReportsTable,
|
||||||
|
HcUser,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
report: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showFiledReports: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isPost() {
|
||||||
|
return this.report.resource.__typename === 'Post'
|
||||||
|
},
|
||||||
|
isComment() {
|
||||||
|
return this.report.resource.__typename === 'Comment'
|
||||||
|
},
|
||||||
|
isUser() {
|
||||||
|
return this.report.resource.__typename === 'User'
|
||||||
|
},
|
||||||
|
isDisabled() {
|
||||||
|
return this.report.resource.disabled
|
||||||
|
},
|
||||||
|
iconName() {
|
||||||
|
if (this.isPost) return 'bookmark'
|
||||||
|
else if (this.isComment) return 'comments'
|
||||||
|
else if (this.isUser) return 'user'
|
||||||
|
else return null
|
||||||
|
},
|
||||||
|
iconLabel() {
|
||||||
|
if (this.isPost) return this.$t('report.contribution.type')
|
||||||
|
else if (this.isComment) return this.$t('report.comment.type')
|
||||||
|
else if (this.isUser) return this.$t('report.user.type')
|
||||||
|
else return null
|
||||||
|
},
|
||||||
|
linkTarget() {
|
||||||
|
const { id, slug } = this.isComment ? this.report.resource.post : this.report.resource
|
||||||
|
return {
|
||||||
|
name: 'post-id-slug',
|
||||||
|
params: { id, slug },
|
||||||
|
hash: this.isComment ? `#commentId-${this.report.resource.id}` : '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
linkText() {
|
||||||
|
return (
|
||||||
|
this.report.resource.title || this.$filters.removeHtml(this.report.resource.contentExcerpt)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
statusIconName() {
|
||||||
|
return this.isDisabled ? 'eye-slash' : 'eye'
|
||||||
|
},
|
||||||
|
statusText() {
|
||||||
|
if (!this.report.reviewed) return this.$t('moderation.reports.enabled')
|
||||||
|
else if (this.isDisabled) return this.$t('moderation.reports.disabledBy')
|
||||||
|
else return this.$t('moderation.reports.enabledBy')
|
||||||
|
},
|
||||||
|
moderatorOfLatestReview() {
|
||||||
|
return this.report.reviewed[0].moderator
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.report-row {
|
||||||
|
&:nth-child(2n + 1) {
|
||||||
|
background-color: $color-neutral-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-line {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
> .base-icon {
|
||||||
|
margin-right: $space-xx-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-count {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: $space-xx-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.--disabled {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.--enabled {
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
webapp/components/features/ReportsTable/ReportsTable.spec.js
Normal file
62
webapp/components/features/ReportsTable/ReportsTable.spec.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { config, mount } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import ReportsTable from './ReportsTable.vue'
|
||||||
|
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('ReportsTable', () => {
|
||||||
|
let propsData, mocks, getters, wrapper, reportsTable
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {}
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(string => string),
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { slug: 'awesome-user' }
|
||||||
|
},
|
||||||
|
'auth/isModerator': () => true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters,
|
||||||
|
})
|
||||||
|
return mount(ReportsTable, { propsData, mocks, localVue, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given no reports', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, reports: [] }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a placeholder', () => {
|
||||||
|
expect(wrapper.text()).toContain('moderation.reports.empty')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given reports', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = { ...propsData, reports }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
reportsTable = wrapper.find('.ds-table')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a table', () => {
|
||||||
|
expect(reportsTable.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders at least one ReportRow component', () => {
|
||||||
|
expect(wrapper.find('.report-row').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { storiesOf } from '@storybook/vue'
|
||||||
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
|
import { action } from '@storybook/addon-actions'
|
||||||
|
import ReportsTable from '~/components/features/ReportsTable/ReportsTable'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||||
|
|
||||||
|
helpers.init()
|
||||||
|
|
||||||
|
storiesOf('ReportsTable', module)
|
||||||
|
.addDecorator(withA11y)
|
||||||
|
.addDecorator(helpers.layout)
|
||||||
|
.add('with reports', () => ({
|
||||||
|
components: { ReportsTable },
|
||||||
|
store: helpers.store,
|
||||||
|
data: () => ({
|
||||||
|
reports,
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
confirm: action('confirm'),
|
||||||
|
},
|
||||||
|
template: `<reports-table :reports="reports" @confirm="confirm" />`,
|
||||||
|
}))
|
||||||
|
.add('without reports', () => ({
|
||||||
|
components: { ReportsTable },
|
||||||
|
store: helpers.store,
|
||||||
|
template: `<reports-table />`,
|
||||||
|
}))
|
||||||
50
webapp/components/features/ReportsTable/ReportsTable.vue
Normal file
50
webapp/components/features/ReportsTable/ReportsTable.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<table
|
||||||
|
v-if="reports && reports.length"
|
||||||
|
class="ds-table ds-table-condensed ds-table-bordered reports-table"
|
||||||
|
cellspacing="0"
|
||||||
|
cellpadding="0"
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col width="4%" />
|
||||||
|
<col width="14%" />
|
||||||
|
<col width="36%" />
|
||||||
|
<col width="14%" />
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="12%" />
|
||||||
|
</colgroup>
|
||||||
|
<thead class="ds-table-col ds-table-head-col">
|
||||||
|
<tr valign="top">
|
||||||
|
<th class="ds-table-head-col"></th>
|
||||||
|
<th class="ds-table-head-col">{{ $t('moderation.reports.submitter') }}</th>
|
||||||
|
<th class="ds-table-head-col">{{ $t('moderation.reports.content') }}</th>
|
||||||
|
<th class="ds-table-head-col">{{ $t('moderation.reports.author') }}</th>
|
||||||
|
<th class="ds-table-head-col">{{ $t('moderation.reports.status') }}</th>
|
||||||
|
<th class="ds-table-head-col">{{ $t('moderation.reports.decision') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<template v-for="report in reports">
|
||||||
|
<report-row
|
||||||
|
:key="report.resource.id"
|
||||||
|
:report="report"
|
||||||
|
@confirm-report="$emit('confirm', report)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HcEmpty from '~/components/Empty/Empty'
|
||||||
|
import ReportRow from '~/components/features/ReportRow/ReportRow'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcEmpty,
|
||||||
|
ReportRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
reports: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/REPORTED.gql"
|
// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/FILED.gql"
|
||||||
export const valuesReasonCategoryOptions = [
|
export const valuesReasonCategoryOptions = [
|
||||||
'discrimination_etc',
|
'discrimination_etc',
|
||||||
'pornographic_content_links',
|
'pornographic_content_links',
|
||||||
|
|||||||
@ -1,80 +1,95 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const reportListQuery = () => {
|
export const reportsListQuery = () => {
|
||||||
// no limit vor the moment like before: "reports(first: 20, orderBy: createdAt_desc)"
|
// no limit for the moment like before: "reports(first: 20, orderBy: createdAt_desc)"
|
||||||
return gql`
|
return gql`
|
||||||
query {
|
query {
|
||||||
reports(orderBy: createdAt_desc) {
|
reports(orderBy: createdAt_desc) {
|
||||||
|
id
|
||||||
createdAt
|
createdAt
|
||||||
reasonCategory
|
updatedAt
|
||||||
reasonDescription
|
disable
|
||||||
type
|
closed
|
||||||
submitter {
|
reviewed {
|
||||||
id
|
createdAt
|
||||||
slug
|
updatedAt
|
||||||
name
|
disable
|
||||||
disabled
|
moderator {
|
||||||
deleted
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
followedByCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user {
|
resource {
|
||||||
id
|
__typename
|
||||||
slug
|
... on User {
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
disabledBy {
|
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
|
followedByCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
}
|
}
|
||||||
}
|
... on Comment {
|
||||||
comment {
|
|
||||||
id
|
|
||||||
contentExcerpt
|
|
||||||
author {
|
|
||||||
id
|
id
|
||||||
slug
|
contentExcerpt
|
||||||
name
|
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
followedByCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
post {
|
... on Post {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
title
|
title
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
}
|
author {
|
||||||
disabledBy {
|
id
|
||||||
id
|
slug
|
||||||
slug
|
name
|
||||||
name
|
disabled
|
||||||
disabled
|
deleted
|
||||||
deleted
|
followedByCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post {
|
filed {
|
||||||
id
|
submitter {
|
||||||
slug
|
|
||||||
title
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
disabledBy {
|
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
|
followedByCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
}
|
}
|
||||||
|
createdAt
|
||||||
|
reasonCategory
|
||||||
|
reasonDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,12 +99,23 @@ export const reportListQuery = () => {
|
|||||||
export const reportMutation = () => {
|
export const reportMutation = () => {
|
||||||
return gql`
|
return gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
report(
|
fileReport(
|
||||||
resourceId: $resourceId
|
resourceId: $resourceId
|
||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
type
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reviewMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
|
disable
|
||||||
|
closed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -316,12 +316,67 @@
|
|||||||
"reports": {
|
"reports": {
|
||||||
"empty": "Glückwunsch, es gibt nichts zu moderieren.",
|
"empty": "Glückwunsch, es gibt nichts zu moderieren.",
|
||||||
"name": "Meldungen",
|
"name": "Meldungen",
|
||||||
"reporter": "gemeldet von",
|
"status": "Aktueller Status",
|
||||||
"submitter": "gemeldet von",
|
"content": "Inhalt",
|
||||||
"disabledBy": "deaktiviert von",
|
"author": "Autor",
|
||||||
|
"decision": "Entscheidung",
|
||||||
|
"enabled": "Entsperrt",
|
||||||
|
"disabled": "Gesperrt",
|
||||||
|
"decided": "Entschieden",
|
||||||
|
"noDecision": "Keine Entscheidung!",
|
||||||
|
"decideButton": "Bestätige",
|
||||||
|
"DecisionSuccess": "Erfolgreich entschieden!",
|
||||||
|
"enabledBy": "Entsperrt von",
|
||||||
|
"disabledBy": "Gesperrt von",
|
||||||
|
"previousDecision": "Vorherige Entscheidung:",
|
||||||
|
"enabledAt": "Entsperrt am",
|
||||||
|
"disabledAt": "Gesperrt am",
|
||||||
"reasonCategory": "Kategorie",
|
"reasonCategory": "Kategorie",
|
||||||
"reasonDescription": "Beschreibung",
|
"reasonDescription": "Beschreibung",
|
||||||
"createdAt": "Datum"
|
"submitter": "Gemeldet von",
|
||||||
|
"numberOfUsers": "{count} Nutzern",
|
||||||
|
"filterLabel": {
|
||||||
|
"all": "Alle",
|
||||||
|
"unreviewed": "Nicht bearbeitet",
|
||||||
|
"reviewed": "Bearbeitet",
|
||||||
|
"closed": "Abgeschlossen"
|
||||||
|
},
|
||||||
|
"reportedOn": "Datum",
|
||||||
|
"moreDetails": "Details öffnen",
|
||||||
|
"decideModal": {
|
||||||
|
"submit": "Bestätige Entscheidung",
|
||||||
|
"cancel": "Abbruch",
|
||||||
|
"User": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Sperre den Benutzer abschließend",
|
||||||
|
"message": "Möchtest du den Benutzer \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Entsperre den Benutzer abschließend",
|
||||||
|
"message": "Möchtest du den Benutzer \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Post": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Sperre den Beitrag abschließend",
|
||||||
|
"message": "Möchtest du den Beitrag \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Entsperre den Beitrag abschließend",
|
||||||
|
"message": "Möchtest du den Beitrag \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comment": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Sperre den Kommentar abschließend",
|
||||||
|
"message": "Möchtest du den Kommentar \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Entsperre den Kommentar abschließend",
|
||||||
|
"message": "Möchtest du den Kommentar \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable": {
|
"disable": {
|
||||||
@ -347,6 +402,7 @@
|
|||||||
"report": {
|
"report": {
|
||||||
"submit": "Meldung senden",
|
"submit": "Meldung senden",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"success": "Vielen Dank für diese Meldung!",
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Nutzer melden",
|
"title": "Nutzer melden",
|
||||||
"type": "Nutzer",
|
"type": "Nutzer",
|
||||||
@ -365,7 +421,6 @@
|
|||||||
"message": "Bist Du sicher, dass Du den Kommentar von „<b>{name}<\/b>“ melden möchtest?",
|
"message": "Bist Du sicher, dass Du den Kommentar von „<b>{name}<\/b>“ melden möchtest?",
|
||||||
"error": "Du hast den Kommentar bereits gemeldet!"
|
"error": "Du hast den Kommentar bereits gemeldet!"
|
||||||
},
|
},
|
||||||
"success": "Vielen Dank für diese Meldung!",
|
|
||||||
"reason": {
|
"reason": {
|
||||||
"category": {
|
"category": {
|
||||||
"label": "Wähle eine Kategorie:",
|
"label": "Wähle eine Kategorie:",
|
||||||
@ -753,4 +808,4 @@
|
|||||||
"donate-now": "Jetzt spenden",
|
"donate-now": "Jetzt spenden",
|
||||||
"amount-of-total": "{amount} von {total} € erreicht"
|
"amount-of-total": "{amount} von {total} € erreicht"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -482,11 +482,67 @@
|
|||||||
"reports": {
|
"reports": {
|
||||||
"empty": "Congratulations, nothing to moderate.",
|
"empty": "Congratulations, nothing to moderate.",
|
||||||
"name": "Reports",
|
"name": "Reports",
|
||||||
|
"status": "Current status",
|
||||||
|
"content": "Content",
|
||||||
|
"author": "Author",
|
||||||
|
"decision": "Decision",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"decided": "Decided",
|
||||||
|
"noDecision": "No decision!",
|
||||||
|
"decideButton": "Confirm",
|
||||||
|
"DecisionSuccess": "Decided successfully!",
|
||||||
|
"enabledBy": "Enabled by",
|
||||||
|
"disabledBy": "Disabled by",
|
||||||
|
"previousDecision": "Previous decision:",
|
||||||
|
"enabledAt": "Enabled at",
|
||||||
|
"disabledAt": "Disabled at",
|
||||||
"reasonCategory": "Category",
|
"reasonCategory": "Category",
|
||||||
"reasonDescription": "Description",
|
"reasonDescription": "Description",
|
||||||
"createdAt": "Date",
|
|
||||||
"submitter": "Reported by",
|
"submitter": "Reported by",
|
||||||
"disabledBy": "Disabled by"
|
"numberOfUsers": "{count} users",
|
||||||
|
"filterLabel": {
|
||||||
|
"all": "All",
|
||||||
|
"unreviewed": "Unreviewed",
|
||||||
|
"reviewed": "Reviewed",
|
||||||
|
"closed": "Closed"
|
||||||
|
},
|
||||||
|
"reportedOn": "Date",
|
||||||
|
"moreDetails": "View Details",
|
||||||
|
"decideModal": {
|
||||||
|
"submit": "Confirm decision",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"User": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Finally Disable User",
|
||||||
|
"message": "Do you really want to let the user \"<b>{name}</b>\" stay <b>disabled</b>?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Finally Enable User",
|
||||||
|
"message": "Do you really want to let the user \"<b>{name}</b>\" stay <b>enabled</b>?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Post": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Finally Disable Post",
|
||||||
|
"message": "Do you really want to let the post \"<b>{name}</b>\" stay <b>disabled</b>?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Finally Enable Post",
|
||||||
|
"message": "Do you really want to let the post \"<b>{name}</b>\" stay <b>enabled</b>?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comment": {
|
||||||
|
"disable": {
|
||||||
|
"title": "Finally Disable Comment",
|
||||||
|
"message": "Do you really want to let the comment \"<b>{name}</b>\" stay <b>disabled</b>?"
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"title": "Finally Enable Comment",
|
||||||
|
"message": "Do you really want to let the comment \"<b>{name}</b>\" stay <b>enabled</b>?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable": {
|
"disable": {
|
||||||
|
|||||||
@ -1,168 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card space="small">
|
<report-list />
|
||||||
<ds-heading tag="h3">{{ $t('moderation.reports.name') }}</ds-heading>
|
|
||||||
<ds-table v-if="reports && reports.length" :data="reports" :fields="fields" condensed>
|
|
||||||
<!-- Icon -->
|
|
||||||
<template slot="type" slot-scope="scope">
|
|
||||||
<ds-text color="soft">
|
|
||||||
<base-icon
|
|
||||||
v-if="scope.row.type === 'Post'"
|
|
||||||
v-tooltip="{ content: $t('report.contribution.type'), placement: 'right' }"
|
|
||||||
name="bookmark"
|
|
||||||
/>
|
|
||||||
<base-icon
|
|
||||||
v-else-if="scope.row.type === 'Comment'"
|
|
||||||
v-tooltip="{ content: $t('report.comment.type'), placement: 'right' }"
|
|
||||||
name="comments"
|
|
||||||
/>
|
|
||||||
<base-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="{
|
|
||||||
name: 'post-id-slug',
|
|
||||||
params: { id: scope.row.post.id, slug: scope.row.post.slug },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<b>{{ scope.row.post.title | truncate(50) }}</b>
|
|
||||||
</nuxt-link>
|
|
||||||
<br />
|
|
||||||
<ds-text size="small" color="soft">{{ scope.row.post.author.name }}</ds-text>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="scope.row.type === 'Comment'">
|
|
||||||
<nuxt-link
|
|
||||||
:to="{
|
|
||||||
name: 'post-id-slug',
|
|
||||||
params: {
|
|
||||||
id: scope.row.comment.post.id,
|
|
||||||
slug: scope.row.comment.post.slug,
|
|
||||||
},
|
|
||||||
hash: `#commentId-${scope.row.comment.id}`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<b>{{ scope.row.comment.contentExcerpt | removeHtml | truncate(50) }}</b>
|
|
||||||
</nuxt-link>
|
|
||||||
<br />
|
|
||||||
<ds-text size="small" color="soft">{{ scope.row.comment.author.name }}</ds-text>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<nuxt-link
|
|
||||||
:to="{
|
|
||||||
name: 'profile-id-slug',
|
|
||||||
params: { id: scope.row.user.id, slug: scope.row.user.slug },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<b>{{ scope.row.user.name | truncate(50) }}</b>
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- 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="{
|
|
||||||
name: 'profile-id-slug',
|
|
||||||
params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ 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"
|
|
||||||
:to="{
|
|
||||||
name: 'profile-id-slug',
|
|
||||||
params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link
|
|
||||||
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
|
|
||||||
:to="{
|
|
||||||
name: 'profile-id-slug',
|
|
||||||
params: {
|
|
||||||
id: scope.row.comment.disabledBy.id,
|
|
||||||
slug: scope.row.comment.disabledBy.slug,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link
|
|
||||||
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
|
|
||||||
:to="{
|
|
||||||
name: 'profile-id-slug',
|
|
||||||
params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<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')" />
|
|
||||||
</ds-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HcEmpty from '~/components/Empty/Empty'
|
import ReportList from '~/components/features/ReportList/ReportList'
|
||||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
|
||||||
import { reportListQuery } from '~/graphql/Moderation.js'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HcEmpty,
|
ReportList,
|
||||||
HcRelativeDateTime,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
reports: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fields() {
|
|
||||||
return {
|
|
||||||
type: ' ',
|
|
||||||
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: {
|
|
||||||
reports: {
|
|
||||||
query: reportListQuery(),
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -71,7 +71,7 @@ describe('PostIndex', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filterNotifications', () => {
|
describe('filter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData.filterOptions = [
|
propsData.filterOptions = [
|
||||||
{ label: 'All', value: null },
|
{ label: 'All', value: null },
|
||||||
@ -79,7 +79,7 @@ describe('PostIndex', () => {
|
|||||||
{ label: 'Unread', value: false },
|
{ label: 'Unread', value: false },
|
||||||
]
|
]
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
wrapper.find(DropdownFilter).vm.$emit('filterNotifications', propsData.filterOptions[1])
|
wrapper.find(DropdownFilter).vm.$emit('filter', propsData.filterOptions[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets `notificationRead` to value of received option', () => {
|
it('sets `notificationRead` to value of received option', () => {
|
||||||
|
|||||||
@ -6,11 +6,7 @@
|
|||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item width="110px">
|
<ds-flex-item width="110px">
|
||||||
<client-only>
|
<client-only>
|
||||||
<dropdown-filter
|
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||||
@filterNotifications="filterNotifications"
|
|
||||||
:filterOptions="filterOptions"
|
|
||||||
:selected="selected"
|
|
||||||
/>
|
|
||||||
</client-only>
|
</client-only>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
@ -60,7 +56,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterNotifications(option) {
|
filter(option) {
|
||||||
this.notificationRead = option.value
|
this.notificationRead = option.value
|
||||||
this.selected = option.label
|
this.selected = option.label
|
||||||
this.$apollo.queries.notifications.refresh()
|
this.$apollo.queries.notifications.refresh()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user