added event for edit to manage reply count

This commit is contained in:
Wyatt Johnson
2017-08-19 10:49:50 -06:00
parent 77b7ddf337
commit b2e871f4a6
6 changed files with 218 additions and 119 deletions
+8
View File
@@ -70,6 +70,9 @@ const CommentSchema = new Schema({
// parent_id is the id of the parent comment (null if there is none).
parent_id: String,
// The number of replies to this comment directly.
reply_count: Number,
// Counts to store related to actions taken on the given comment.
action_counts: {
default: {},
@@ -123,6 +126,11 @@ CommentSchema.virtual('edited').get(function() {
return this.body_history.length > 1;
});
// Visable is true when the comment is visible to the public.
CommentSchema.virtual('visible').get(function() {
return ['ACCEPTED', 'NONE'].includes(this.status);
});
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
+2
View File
@@ -198,6 +198,8 @@
"mocha-junit-reporter": "^1.12.1",
"nodemon": "^1.11.0",
"pre-git": "^3.10.0",
"sinon": "^3.2.1",
"sinon-chai": "^2.13.0",
"supertest": "^2.0.1"
},
"engines": {
+109 -71
View File
@@ -10,6 +10,8 @@ const events = require('./events');
const {
ACTIONS_NEW,
ACTIONS_DELETE,
COMMENTS_NEW,
COMMENTS_EDIT,
} = require('./events/constants');
module.exports = class CommentsService {
@@ -19,7 +21,7 @@ module.exports = class CommentsService {
* @param {Mixed} comment either a single comment or an array of comments.
* @return {Promise}
*/
static publicCreate(comment) {
static async publicCreate(comment) {
// Check to see if this is an array of comments, if so map it out.
if (Array.isArray(comment)) {
@@ -41,7 +43,12 @@ module.exports = class CommentsService {
}]
}, comment));
return commentModel.save();
const savedCommentModel = await commentModel.save();
// Emit that the comment was created!
events.emitAsync(COMMENTS_NEW, savedCommentModel);
return savedCommentModel;
}
/**
@@ -54,7 +61,10 @@ module.exports = class CommentsService {
static async edit(id, author_id, {body, status, ignoreEditWindow = false}) {
const query = {
id,
author_id
author_id,
status: {
$in: ['NONE', 'PREMOD'],
},
};
// Establish the edit window (if it exists) and add the condition to the
@@ -68,9 +78,7 @@ module.exports = class CommentsService {
};
}
const {
value: comment
} = await CommentModel.findOneAndUpdate(query, {
const originalComment = await CommentModel.findOneAndUpdate(query, {
$set: {
body,
status,
@@ -85,12 +93,9 @@ module.exports = class CommentsService {
created_at: new Date(),
}
},
}, {
new: true,
rawResult: true
});
if (comment === null) {
if (originalComment === null) {
// Try to get the comment.
const comment = await CommentsService.findById(id);
@@ -103,6 +108,11 @@ module.exports = class CommentsService {
throw errors.ErrNotAuthorized;
}
// Check to see if the comment had a status that was editable.
if (!['NONE', 'PREMOD'].includes(comment.status)) {
throw errors.ErrNotAuthorized;
}
// Check to see if the edit window expired.
if (!ignoreEditWindow && comment.created_at <= lastEditableCommentCreatedAt) {
throw errors.ErrEditWindowHasEnded;
@@ -111,7 +121,22 @@ module.exports = class CommentsService {
throw new Error('comment edit failed for an unexpected reason');
}
return comment;
// Mutate the comment like Mongo would have.
const editedComment = originalComment;
editedComment.status = status;
editedComment.body = body;
editedComment.body_history.push({
body,
created_at: new Date(),
});
editedComment.status_history.push({
type: status,
created_at: new Date(),
});
events.emitAsync(COMMENTS_EDIT, originalComment, editedComment);
return editedComment;
}
/**
@@ -211,21 +236,36 @@ module.exports = class CommentsService {
* moderation action
* @return {Promise}
*/
static pushStatus(id, status, assigned_by = null) {
return CommentModel.findOneAndUpdate({id}, {
static async pushStatus(id, status, assigned_by = null) {
const created_at = new Date();
const originalComment = await CommentModel.findOneAndUpdate({id}, {
$push: {
status_history: {
type: status,
created_at: new Date(),
assigned_by
created_at,
assigned_by,
}
},
$set: {status}
}, {
// return modified comment.
new: true,
});
if (originalComment === null) {
throw errors.ErrNotFound;
}
const editedComment = new CommentModel(originalComment.toObject());
editedComment.status_history.push({
type: status,
created_at,
assigned_by,
});
editedComment.status = status;
// Emit that the comment was edited, and pass the original comment and the
// edited comment.
events.emitAsync(COMMENTS_EDIT, originalComment, editedComment);
return editedComment;
}
/**
@@ -244,57 +284,6 @@ module.exports = class CommentsService {
metadata
});
}
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* @return {Promise}
*/
static removeById(id) {
return CommentModel.remove({id});
}
/**
* Remove an action from the comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} action_type the type of the action to be removed
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
static removeAction(item_id, user_id, action_type) {
return ActionModel.remove({
action_type,
item_type: 'COMMENTS',
item_id,
user_id
});
}
/**
* Returns all the comments in the collection.
* @return {Promise}
*/
static all() {
return CommentModel.find({});
}
/**
* Returns all the comments by user
* probably to be paginated at some point in the future
* @return {Promise} array resolves to an array of comments by that user
*/
static findByUserId(author_id, admin = false) {
// do not return un-published comments for non-admins
let query = {author_id};
if (!admin) {
query.$nor = [{status: 'PREMOD'}, {status: 'REJECTED'}];
}
return CommentModel.find(query);
}
};
//==============================================================================
@@ -334,7 +323,7 @@ events.on(ACTIONS_NEW, async (action) => {
return incrActionCounts(action, 1);
});
// When an action is deleted, modify the comment.
// When an action is deleted, remove the action count on the comment.
events.on(ACTIONS_DELETE, async (action) => {
if (!action || action.item_type !== 'COMMENTS') {
return;
@@ -342,3 +331,52 @@ events.on(ACTIONS_DELETE, async (action) => {
return incrActionCounts(action, -1);
});
const incrReplyCount = async (comment, value) => {
try {
await CommentModel.update({
id: comment.parent_id,
}, {
$inc: {
reply_count: value,
},
});
} catch (err) {
console.error('Can\'t mutate the reply count:', err);
}
};
// When a comment is created, if it is a reply, increment the reply count on the
// parent's document.
events.on(COMMENTS_NEW, async (comment) => {
if (
!comment || // Check that the comment is defined.
(comment.parent_id === null || comment.parent_id.length === 0) || // Check that the comment has a parent (is a reply).
!(comment.status === 'NONE' || comment.status === 'APPROVED') // Check that the comment is visible.
) {
return;
}
return incrReplyCount(comment, 1);
});
// When a comment is edited, if the visability changed publicly, then modify the
// comment.
events.on(COMMENTS_EDIT, async (originalComment, editedComment) => {
if (
!editedComment || // Check that the comment is defined.
(editedComment.parent_id === null || editedComment.parent_id.length === 0) // Check that the comment has a parent (is a reply).
) {
return;
}
// If the comment was visible before, and now it isn't, decrement the count;
if (originalComment.visible && !editedComment.visible) {
return incrReplyCount(editedComment, -1);
}
// If the comment was not visible before, and now it is, increment the count.
if (!originalComment.visible && editedComment.visible) {
return incrReplyCount(editedComment, 1);
}
});
+2
View File
@@ -1,2 +1,4 @@
module.exports.ACTIONS_DELETE = 'actions.delete';
module.exports.ACTIONS_NEW = 'actions.new';
module.exports.COMMENTS_NEW = 'comments.new';
module.exports.COMMENTS_EDIT = 'comments.edit';
+26 -48
View File
@@ -1,7 +1,8 @@
const CommentModel = require('../../../models/comment');
const ActionModel = require('../../../models/action');
const ActionsService = require('../../../services/actions');
const events = require('../../../services/events');
const {COMMENTS_EDIT} = require('../../../services/events/constants');
const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
@@ -10,8 +11,11 @@ const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'],
const chai = require('chai');
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
const expect = chai.expect;
const sinon = require('sinon');
describe('services.CommentsService', () => {
const comments = [{
body: 'comment 10',
@@ -188,58 +192,32 @@ describe('services.CommentsService', () => {
});
describe('#removeAction', () => {
it('should remove an action', () => {
return CommentsService
.removeAction('3', '123', 'flag')
.then(() => {
return ActionsService.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
});
});
describe('#findByUserId', () => {
it('should return all comments if admin', () => {
return CommentsService
.findByUserId('456', true)
.then((comments) => {
expect(comments).to.have.length(4);
});
});
it('should not return premod and rejected comments if not admin', () => {
return CommentsService
.findByUserId('456')
.then((comments) => {
expect(comments).to.have.length(1);
});
});
});
describe('#changeStatus', () => {
it('should change the status of a comment from no status', () => {
it('should change the status of a comment from no status', async () => {
let comment_id = comments[0].id;
return CommentsService.findById(comment_id)
.then((c) => {
expect(c.status).to.be.equal('NONE');
let c = await CommentsService.findById(comment_id);
expect(c.status).to.be.equal('NONE');
return CommentsService.pushStatus(comment_id, 'REJECTED', '123');
})
.then(() => CommentsService.findById(comment_id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.equal('REJECTED');
expect(c.status_history).to.have.length(1);
expect(c.status_history[0]).to.have.property('type', 'REJECTED');
expect(c.status_history[0]).to.have.property('assigned_by', '123');
});
const spy = sinon.spy();
events.once(COMMENTS_EDIT, () => spy());
let c2 = await CommentsService.pushStatus(comment_id, 'REJECTED', '123');
expect(c2).to.have.property('status');
expect(c2.status).to.equal('REJECTED');
expect(c2.status_history).to.have.length(1);
expect(c2.status_history[0]).to.have.property('type', 'REJECTED');
expect(c2.status_history[0]).to.have.property('assigned_by', '123');
expect(spy).to.have.been.called;
let c3 = await CommentsService.findById(comment_id);
expect(c3).to.have.property('status');
expect(c3.status).to.equal('REJECTED');
expect(c3.status_history).to.have.length(1);
expect(c3.status_history[0]).to.have.property('type', 'REJECTED');
expect(c3.status_history[0]).to.have.property('assigned_by', '123');
});
it('should change the status of a comment from accepted', () => {
+71
View File
@@ -2143,6 +2143,10 @@ diff@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
diff@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9"
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -2918,6 +2922,12 @@ form-data@^2.1.2, form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
formatio@1.2.0, formatio@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
dependencies:
samsam "1.x"
formidable@^1.0.17:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
@@ -4260,6 +4270,10 @@ jsx-ast-utils@^1.3.4:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
just-extend@^1.1.22:
version "1.1.22"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff"
jwa@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
@@ -4591,6 +4605,14 @@ lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lo
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lolex@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
lolex@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.1.2.tgz#2694b953c9ea4d013e5b8bfba891c991025b2629"
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -4908,6 +4930,10 @@ nan@^2.0.0, nan@^2.3.0, nan@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8"
native-promise-only@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -4945,6 +4971,15 @@ nib@~1.1.2:
dependencies:
stylus "0.54.5"
nise@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nise/-/nise-1.0.1.tgz#0da92b10a854e97c0f496f6c2845a301280b3eef"
dependencies:
formatio "^1.2.0"
just-extend "^1.1.22"
lolex "^1.6.0"
path-to-regexp "^1.7.0"
no-case@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081"
@@ -5413,6 +5448,12 @@ path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
path-to-regexp@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
dependencies:
isarray "0.0.1"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -6751,6 +6792,10 @@ safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
samsam@1.x, samsam@^1.1.3:
version "1.2.1"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67"
sax@0.5.x:
version "0.5.8"
resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1"
@@ -6877,6 +6922,24 @@ simplemde@^1.11.2:
codemirror-spell-checker "*"
marked "*"
sinon-chai@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.13.0.tgz#b9a42e801c20234bfc2f43b29e6f4f61b60990c4"
sinon@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-3.2.1.tgz#d8adabd900730fd497788a027049c64b08be91c2"
dependencies:
diff "^3.1.0"
formatio "1.2.0"
lolex "^2.1.2"
native-promise-only "^0.8.1"
nise "^1.0.1"
path-to-regexp "^1.7.0"
samsam "^1.1.3"
text-encoding "0.6.4"
type-detect "^4.0.0"
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -7276,6 +7339,10 @@ tcomb@^2.5.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0"
text-encoding@0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -7433,6 +7500,10 @@ type-detect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
type-detect@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea"
type-is@~1.6.14:
version "1.6.15"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"