Merge pull request #1288 from coralproject/reply-on-hidden-comment

Replies to hidden/deleted comments
This commit is contained in:
Kim Gardner
2018-01-18 11:18:53 -05:00
committed by GitHub
9 changed files with 128 additions and 72 deletions
+13 -3
View File
@@ -265,6 +265,15 @@ const ErrCannotIgnoreStaff = new APIError('Cannot ignore staff members.', {
status: 400,
});
// ErrParentDoesNotVisible is returned when the user tries to reply to a comment
// that isn't visible.
const ErrParentDoesNotVisible = new APIError(
'Cannot reply to a comment that is not visible',
{
translation_key: 'COMMENT_PARENT_NOT_VISIBLE',
}
);
module.exports = {
APIError,
ErrAlreadyExists,
@@ -276,24 +285,25 @@ module.exports = {
ErrContainsProfanity,
ErrEditWindowHasEnded,
ErrEmailTaken,
ErrEmailVerificationToken,
ErrInstallLock,
ErrInvalidAssetURL,
ErrLoginAttemptMaximumExceeded,
ErrMaxRateLimit,
ErrMissingEmail,
ErrMissingPassword,
ErrEmailVerificationToken,
ErrPasswordResetToken,
ErrMissingUsername,
ErrNotAuthorized,
ErrNotFound,
ErrNotVerified,
ErrParentDoesNotVisible,
ErrPasswordResetToken,
ErrPasswordTooShort,
ErrPermissionUpdateUsername,
ErrSameUsernameProvided,
ErrSettingsInit,
ErrSettingsNotInit,
ErrSpecialChars,
ErrUsernameTaken,
ErrSameUsernameProvided,
ExtendableError,
};
+1
View File
@@ -198,6 +198,7 @@ en:
embedlink:
copy: "Copy to Clipboard"
error:
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
COMMENT_TOO_SHORT: "Comments should be more than one character, please revise your comment and try again."
+1
View File
@@ -182,6 +182,7 @@ es:
embedlink:
copy: "Copiar al portapapeles"
error:
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
+20 -15
View File
@@ -7,24 +7,31 @@ const SettingsService = require('./settings');
const cloneDeep = require('lodash/cloneDeep');
const errors = require('../errors');
const events = require('./events');
const merge = require('lodash/merge');
const { COMMENTS_NEW, COMMENTS_EDIT } = require('./events/constants');
module.exports = class CommentsService {
/**
* Creates a new Comment that came from a public source.
* @param {Mixed} comment either a single comment or an array of comments.
* @param {Object} input either a single comment or an array of comments.
* @return {Promise}
*/
static async publicCreate(comment) {
// Check to see if this is an array of comments, if so map it out.
if (Array.isArray(comment)) {
return Promise.all(comment.map(CommentsService.publicCreate));
static async publicCreate(input) {
// Extract the parent_id from the comment, if there is one.
const { status = 'NONE', parent_id = null } = input;
// Check to see if we are replying to a comment, and if that comment is
// visible.
if (parent_id !== null) {
const parent = await CommentModel.findOne({ id: parent_id });
if (parent === null || !parent.visible) {
throw errors.ErrParentDoesNotVisible;
}
}
const { status = 'NONE' } = comment;
const commentModel = new CommentModel(
Object.assign(
// Create the comment in the database.
const comment = await CommentModel.create(
merge(
{
status_history: status
? [
@@ -36,21 +43,19 @@ module.exports = class CommentsService {
: [],
body_history: [
{
body: comment.body,
body: input.body,
created_at: new Date(),
},
],
},
comment
input
)
);
const savedCommentModel = await commentModel.save();
// Emit that the comment was created!
await events.emitAsync(COMMENTS_NEW, savedCommentModel);
await events.emitAsync(COMMENTS_NEW, comment);
return savedCommentModel;
return comment;
}
/**
+1 -1
View File
@@ -7,6 +7,6 @@
],
"extends": "../.eslintrc.json",
"rules": {
"mocha/no-exclusive-tests": "warn"
"mocha/no-exclusive-tests": "error"
}
}
@@ -116,6 +116,8 @@ describe('graph.mutations.createComment', () => {
}
expect(data.createComment).to.have.property('errors').null;
expect(data.createComment).to.have.property('comment').not.null;
expect(data.createComment.comment).to.have.property('id').not
.null;
}
}
);
+8 -6
View File
@@ -34,12 +34,14 @@ describe('graph.queries.asset', () => {
username: 'usernameC',
},
]);
comments = await CommentsService.publicCreate(
[0, 0, 1, 1].map(idx => ({
author_id: users[idx].id,
asset_id: assets[idx].id,
body: `hello there! ${String(Math.random()).slice(2)}`,
}))
comments = await Promise.all(
[0, 0, 1, 1].map(idx =>
CommentsService.publicCreate({
author_id: users[idx].id,
asset_id: assets[idx].id,
body: `hello there! ${String(Math.random()).slice(2)}`,
})
)
);
});
+24 -22
View File
@@ -176,28 +176,30 @@ describe('services.AssetsService', () => {
);
// Create some comments on both assets.
await CommentsService.publicCreate([
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
]);
await Promise.all(
[
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
].map(comment => CommentsService.publicCreate(comment))
);
// Merge all the comments from asset 1 into asset 2, followed by deleting
// asset 1.
+58 -25
View File
@@ -131,6 +131,64 @@ describe('services.CommentsService', () => {
});
describe('#publicCreate()', () => {
describe('does not allow replies to comments that are not visible', () => {
it('parent not found', async () => {
try {
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: 'does not exist',
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
it('parent REJECTED', async () => {
try {
const parent = await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'REJECTED',
});
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: parent.id,
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
it('parent SYSTEM_WITHHELD', async () => {
try {
const parent = await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'SYSTEM_WITHHELD',
});
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: parent.id,
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
});
it('creates a new comment', async () => {
const c = await CommentsService.publicCreate({
body: 'This is a comment!',
@@ -142,31 +200,6 @@ describe('services.CommentsService', () => {
expect(c.id).to.be.uuid;
expect(c.status).to.be.equal('ACCEPTED');
});
it('creates many new comments', async () => {
const [c1, c2, c3] = await CommentsService.publicCreate([
{
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
body: 'This is another comment!',
},
{
body: 'This is a rejected comment!',
status: 'REJECTED',
},
]);
expect(c1).to.not.be.null;
expect(c1.status).to.be.equal('ACCEPTED');
expect(c2).to.not.be.null;
expect(c2.status).to.be.equal('NONE');
expect(c3).to.not.be.null;
expect(c3.status).to.be.equal('REJECTED');
});
});
describe('#edit', () => {