Revert "Revert "Status history""

This commit is contained in:
Wyatt Johnson
2016-12-08 16:12:44 -05:00
committed by GitHub
parent a81125ae09
commit aaebbabef9
23 changed files with 846 additions and 435 deletions
+4
View File
@@ -96,6 +96,10 @@ function startApp() {
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
+15
View File
@@ -76,6 +76,21 @@ cache.get = (key) => new Promise((resolve, reject) => {
});
});
/**
* This invalidates a cached entry in the cache.
* @param {Mixed} key Either an array of items composing a key or a string
* @return {Promise}
*/
cache.invalidate = (key) => new Promise((resolve, reject) => {
cache.client.del(keyfunc(key), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
/**
* This sets a value on the key with the expiry and then resolves once it is
* done.
+29
View File
@@ -1,5 +1,6 @@
const mongoose = require('../mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const Schema = mongoose.Schema;
const ActionSchema = new Schema({
@@ -66,6 +67,34 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
});
};
/**
* Fetches the action summaries for the given asset, and comments around the
* given user id.
* @param {[type]} asset_id [description]
* @param {[type]} comments [description]
* @param {String} [current_user_id=''] [description]
* @return {[type]} [description]
*/
ActionSchema.statics.getActionSummariesFromComments = (asset_id = '', comments, current_user_id = '') => {
// Get the user id's from the author id's as a unique array that gets
// sorted.
let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
// Fetch the actions for pretty much everything at this point.
return Action.getActionSummaries(_.uniq([
// Actions can be on assets...
asset_id,
// Comments...
...comments.map((comment) => comment.id),
// Or Authors...
...userIDs
].filter((e) => e)), current_user_id);
};
/**
* Returns summaries of actions for an array of ids
* @param {String} ids array of user identifiers (uuid)
+21 -5
View File
@@ -1,6 +1,8 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const Setting = require('./setting');
const uuid = require('uuid');
const AssetSchema = new Schema({
@@ -58,11 +60,6 @@ AssetSchema.index({
background: true
});
/**
* Search for assets. Currently only returns all.
*/
AssetSchema.statics.search = (query) => Asset.find(query);
/**
* Finds an asset by its id.
* @param {String} id identifier of the asset (uuid).
@@ -75,6 +72,25 @@ AssetSchema.statics.findById = (id) => Asset.findOne({id});
*/
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
/**
* Retrieves the settings given an asset query and rectifies it against the
* global settings.
* @param {Promise} assetQuery an asset query that returns a single asset.
* @return {Promise}
*/
AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
Setting.retrieve(),
assetQuery
]).then(([settings, asset]) => {
// If the asset exists and has settings then return the merged object.
if (asset && asset.settings) {
return Object.assign({}, settings, asset.settings);
}
return settings;
});
/**
* Finds a asset by its url.
*
+204 -92
View File
@@ -1,9 +1,37 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const Action = require('./action');
const Schema = mongoose.Schema;
/**
* The Mongo schema for a Comment Status.
* @type {Schema}
*/
const StatusSchema = new Schema({
type: {
type: String,
enum: [
'accepted',
'rejected',
'premod',
],
},
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null
},
created_at: Date
}, {
_id: false
});
/**
* The Mongo schema for a Comment.
* @type {Schema}
*/
const CommentSchema = new Schema({
id: {
type: String,
@@ -17,11 +45,7 @@ const CommentSchema = new Schema({
},
asset_id: String,
author_id: String,
status: {
type: String,
enum: ['accepted', 'rejected', ''],
default: ''
},
status: [StatusSchema],
parent_id: String
}, {
timestamps: {
@@ -30,90 +54,168 @@ const CommentSchema = new Schema({
}
});
//==============================================================================
// Find Statics
//==============================================================================
/**
* toJSON overrides to remove fields from the json
* output.
*/
CommentSchema.options.toJSON = {};
CommentSchema.options.toJSON.hide = '_id status';
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* toJSON overrides to remove fields from the json
* output.
*/
CommentSchema.options.toJSON = {};
CommentSchema.options.toJSON.hide = '_id';
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* Sets up a virtual getter function on a comment such that when you try and
* access the `comment.last_status` it returns the last status in the array
* of status's on the comment, or `null` if there was no status.
*/
CommentSchema.virtual('last_status').get(function() {
if (this.status && this.status.length > 0) {
return this.status[this.status.length - 1].type;
}
return null;
});
/**
* Creates a new Comment that came from a public source.
* @param {Mixed} comment either a single comment or an array of comments.
* @return {Promise}
*/
CommentSchema.statics.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(Comment.publicCreate));
}
const {
body,
asset_id,
parent_id,
status = false,
author_id
} = comment;
comment = new Comment({
body,
asset_id,
parent_id,
status: status ? [{
type: status,
created_at: new Date()
}] : [],
author_id
});
return comment.save();
};
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
* @return {Promise}
*/
CommentSchema.statics.findById = function(id) {
return Comment.findOne({'id': id});
};
CommentSchema.statics.findById = (id) => Comment.findOne({id});
/**
* Finds ALL the comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
* @return {Promise}
*/
CommentSchema.statics.findByAssetId = function(asset_id) {
return Comment.find({asset_id});
};
CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
asset_id
});
/**
* Finds the accepted comments by the asset_id.
* get the comments that are accepted.
* Finds the accepted comments by the asset_id get the comments that are
* accepted.
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
* @return {Promise}
*/
CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
return Comment.find({asset_id: asset_id, status:'accepted'});
};
CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
asset_id,
'status.type': 'accepted'
});
/**
* Finds the new and accepted comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
* @return {Promise}
*/
CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}});
};
CommentSchema.statics.findAcceptedAndNewByAssetId = (asset_id) => Comment.find({
asset_id,
$or: [
{
'status.type': 'accepted'
},
{
status: {
$size: 0
}
}
]
});
/**
* Find comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
* @return {Promise}
*/
CommentSchema.statics.findByActionType = function(action_type) {
return Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => {
return Comment.find({'id': {'$in': actions.map(function(a){
return a.item_id;})}
});
});
};
CommentSchema.statics.findByActionType = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => Comment.find({
id: {
$in: actions.map((a) => a.item_id)
}
}));
/**
* Find not moderated comments by an action that was performed on them.
* Find comment id's where the action type matches the argument.
* @param {String} action_type the type of action that was performed on the comment
* @param {String} status the status of the comment to search for
* @return {Promise}
*/
CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
return Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => {
return Comment.find({
status: status,
id: {
$in: actions.map(a => a.item_id)
}
});
});
};
CommentSchema.statics.findIdsByActionType = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => actions.map(a => a.item_id));
/**
* Find comments by their status.
* @param {String} status the status of the comment to search for
* @return {Promise}
*/
CommentSchema.statics.findByStatus = function(status) {
return Comment.find({
status: status === 'new' ? '' : status
});
CommentSchema.statics.findByStatus = (status = false) => {
let q = {};
if (status) {
q['status.type'] = status;
} else {
q.status = {$size: 0};
}
return Comment.find(q);
};
/**
@@ -121,39 +223,59 @@ CommentSchema.statics.findByStatus = function(status) {
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
* @return {Promise}
*/
CommentSchema.statics.moderationQueue = function(moderation) {
switch(moderation){
CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
// Pre-moderation: New comments are shown in the moderator queues immediately.
case 'pre':
return Comment.findByStatus('').then((comments) => {
return comments;
});
/**
* This adds the asset_id requirement to the query if the asset_id is defined.
*/
const assetIDWrap = (query) => {
if (asset_id) {
query = query.where('asset_id', asset_id);
}
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
case 'post':
return Comment.findByStatusByActionType('', 'flag').then((comments) => {
return comments;
});
return query;
};
default:
return Promise.reject(Error('Moderation setting not found.'));
// Decide on whether or not we need to load extended options for the
// moderation based on the moderation options.
let comments;
if (moderation === 'pre') {
// Pre-moderation: New comments are shown in the moderator queues immediately.
comments = assetIDWrap(CommentSchema.statics.findByStatus('premod'));
} else {
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
comments = CommentSchema.statics.findIdsByActionType('flag')
.then((ids) => assetIDWrap(Comment.find({
id: {
$in: ids
}
})));
}
};
//==============================================================================
// Update Statics
//==============================================================================
return comments;
};
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* Pushes a new status in for the user.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* @param {String} assigned_by the user id for the user who performed the
* moderation action
* @return {Promise}
*/
CommentSchema.statics.changeStatus = function(id, status) {
return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}});
};
CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
$push: {
status: {
type: status,
created_at: new Date(),
assigned_by
}
}
});
/**
* Add an action to the comment.
@@ -169,19 +291,13 @@ CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.inse
action_type
});
//==============================================================================
// Remove Statics
//==============================================================================
/**
* 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}
*/
CommentSchema.statics.removeById = function(id) {
return Comment.remove({'id': id});
};
CommentSchema.statics.removeById = (id) => Comment.remove({id});
/**
* Remove an action from the comment.
@@ -190,22 +306,18 @@ CommentSchema.statics.removeById = function(id) {
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
return Action.remove({
action_type,
item_type: 'comment',
item_id,
user_id
});
};
CommentSchema.statics.removeAction = (item_id, user_id, action_type) => Action.remove({
action_type,
item_type: 'comment',
item_id,
user_id
});
/**
* Returns all the comments in the collection.
* @return {Promise}
*/
CommentSchema.statics.all = () => {
return Comment.find();
};
CommentSchema.statics.all = () => Comment.find();
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
+77 -40
View File
@@ -1,18 +1,33 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const cache = require('../cache');
/**
* this Schema manages application settings that get used on front and backend
* NOTE: when you set a setting here, it will not automatically be exposed to
* the front end. You must add it to the whitelist in the settings route
* in /routes/api/settings/index.js
* SettingSchema manages application settings that get used on front and backend.
* @type {Schema}
*/
const SettingSchema = new Schema({
id: {type: String, default: '1'},
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
infoBoxEnable: {type: Boolean, default: false},
infoBoxContent: {type: String, default: ''},
id: {
type: String,
default: '1'
},
moderation: {
type: String,
enum: [
'pre',
'post'
],
default: 'pre'
},
infoBoxEnable: {
type: Boolean,
default: false
},
infoBoxContent: {
type: String,
default: ''
},
wordlist: [String]
}, {
timestamps: {
@@ -22,48 +37,70 @@ const SettingSchema = new Schema({
});
/**
* this is run once when the app starts to ensure settings are populated
* @return {Promise} null initialize the global settings object
* The Mongo Mongoose object.
*/
SettingSchema.statics.init = function (defaults) {
return this.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
};
const Setting = mongoose.model('Setting', SettingSchema);
/**
* The Setting Service object exposing the Setting model.
* @type {Object}
*/
const SettingService = module.exports = {};
/**
* The selector used to uniquely identify the settings document.
*/
const selector = {id: '1'};
/**
* Cache expiry time in seconds for when the cached entry of the settings object
* expires. 2 minutes.
*/
const EXPIRY_TIME = 60 * 2;
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
SettingSchema.statics.getSettings = function () {
return this.findOne({id: '1'});
};
/**
* Gets the settings visible to the public
* @return {Promise} moderation the settings for how to moderate comments
*/
SettingSchema.statics.getPublicSettings = function () {
return this.findOne({id: '1'}).select('moderation infoBoxEnable infoBoxContent');
};
/**
* Gets the info box settings and sends it back
* @return {Promise} content the content of the info Box
*/
SettingSchema.statics.getInfoBoxSetting = function () {
return this.findOne({id: '1'}).select('infoBoxEnable infoBoxContent');
};
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => Setting.findOne(selector));
/**
* This will update the settings object with whatever you pass in
* @param {object} setting a hash of whatever settings you want to update
* @return {Promise} settings Promise that resolves to the entire (updated) settings object.
*/
SettingSchema.statics.updateSettings = function (setting) {
// There should only ever be one record unless something has gone wrong.
// In the future we may have multiple records for custom settings for objects/users.
return this.findOneAndUpdate({id: '1'}, {$set: setting}, {new: true});
SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
$set: settings
}, {
upsert: true,
new: true,
setDefaultsOnInsert: true
}).then((settings) => {
// Invalidate the settings cache.
return cache
.set('settings', settings, EXPIRY_TIME)
.then(() => settings);
});
/**
* Filters the document to ensure that the resulting document is indeed ready
* for non authenticated users.
* @param {Object} settings the source settings object
* @return {Object} the filtered settings object
*/
SettingService.public = (settings) => _.pick(settings, ['moderation', 'infoBoxEnable', 'infoBoxContent']);
/**
* This is run once when the app starts to ensure settings are populated.
* @return {Promise} null initialize the global settings object
*/
SettingService.init = (defaults) => {
// Inject the defaults on top of the passed in defaults to ensure that the new
// settings conform to the required selector.
defaults = Object.assign({}, defaults, selector);
// Actually update the settings collection.
return SettingService.update(defaults);
};
const Setting = mongoose.model('Setting', SettingSchema);
module.exports = Setting;
+1 -2
View File
@@ -9,7 +9,6 @@ const SALT_ROUNDS = 10;
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = [
'',
'admin',
'moderator'
];
@@ -106,7 +105,7 @@ UserSchema.index({
* output.
*/
UserSchema.options.toJSON = {};
UserSchema.options.toJSON.hide = 'password profiles roles disabled';
UserSchema.options.toJSON.hide = '_id password profiles roles disabled';
UserSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
+1
View File
@@ -89,6 +89,7 @@
"eslint-config-standard": "^6.2.1",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-mocha": "^4.7.0",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^6.6.0",
"eslint-plugin-standard": "^2.0.1",
+63 -24
View File
@@ -1,5 +1,6 @@
const express = require('express');
const Comment = require('../../../models/comment');
const Asset = require('../../../models/asset');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const wordlist = require('../../../services/wordlist');
@@ -9,24 +10,45 @@ const _ = require('lodash');
const router = express.Router();
router.get('/', authorization.needed('admin'), (req, res, next) => {
const {
status = null,
action_type = null,
asset_id = null
} = req.query;
/**
* This adds the asset_id requirement to the query if the asset_id is defined.
*/
const assetIDWrap = (query) => {
if (asset_id) {
query = query.where('asset_id', asset_id);
}
return query;
};
let query;
if (req.query.status) {
query = Comment.findByStatus(req.query.status);
} else if (req.query.action_type) {
query = Comment.findByActionType(req.query.action_type);
if (status) {
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
} else if (action_type) {
query = Comment
.findIdsByActionType(action_type)
.then((ids) => assetIDWrap(Comment.find({
id: {
$in: ids
},
})));
} else {
query = Comment.all();
query = assetIDWrap(Comment.all());
}
query.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
]);
})
.then(([comments, users, actions])=>
@@ -48,21 +70,38 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
parent_id
} = req.body;
Comment
.create({
body,
asset_id,
parent_id,
status: req.wordlist.matched ? 'rejected' : '',
author_id: req.user.id
})
.then((comment) => {
// Decide the status based on whether or not the current asset/settings
// has pre-mod enabled or not. If the comment was rejected based on the
// wordlist, then reject it, otherwise if the moderation setting is
// premod, set it to `premod`.
let status;
res.status(201).send(comment);
})
.catch((err) => {
next(err);
});
if (req.wordlist.matched) {
status = Promise.resolve('rejected');
} else {
status = Asset
.rectifySettings(Asset.findById(asset_id))
// Return `premod` if pre-moderation is enabled and an empty "new" status
// in the event that it is not in pre-moderation mode.
.then(({moderation}) => moderation === 'pre' ? 'premod' : '');
}
status.then((status) => Comment.publicCreate({
body,
asset_id,
parent_id,
status,
author_id: req.user.id
}))
.then((comment) => {
// The comment was created! Send back the created comment.
res.status(201).send(comment);
})
.catch((err) => {
next(err);
});
});
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
@@ -99,7 +138,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
} = req.body;
Comment
.changeStatus(req.params.comment_id, status)
.pushStatus(req.params.comment_id, status, req.user.id)
.then(() => {
res.status(204).end();
})
+47 -21
View File
@@ -3,6 +3,7 @@ const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const Setting = require('../../../models/setting');
const Asset = require('../../../models/asset');
const _ = require('lodash');
const router = express.Router();
@@ -16,27 +17,52 @@ const router = express.Router();
// Pre-moderation: New comments are shown in the moderator queues immediately.
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
router.get('/comments/pending', (req, res, next) => {
Setting.getPublicSettings().then(({moderation}) =>
Comment.moderationQueue(moderation))
.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions])=>
res.status(200).json({
comments,
users,
actions
}))
.catch(error => {
next(error);
});
const {
asset_id
} = req.query;
let settings = Setting.retrieve();
if (asset_id) {
// In the event that we have an asset_id, we should fetch the asset settings
// in order to actually determine if there is additional comments to parse.
settings = Promise.all([
settings,
Asset.findById(asset_id).select('settings')
]).then(([{moderation}, asset]) => {
if (asset.settings && asset.settings.moderation) {
return {moderation: asset.settings.moderation};
}
return {moderation};
});
}
settings
.then(({moderation}) => {
return Comment.moderationQueue(moderation);
}).then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions]) => {
res.json({
comments,
users,
actions
});
})
.catch(error => {
next(error);
});
});
module.exports = router;
+12 -10
View File
@@ -4,19 +4,21 @@ const Setting = require('../../../models/setting');
const router = express.Router();
router.get('/', (req, res, next) => {
Setting
.getSettings()
.then(settings => {
res.json(settings);
})
.catch(next);
Setting.retrieve().then((settings) => {
res.json(settings);
})
.catch((err) => {
next(err);
});
});
router.put('/', (req, res, next) => {
Setting
.updateSettings(req.body)
.then(() => res.status(204).end())
.catch(next);
Setting.update(req.body).then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
+5 -15
View File
@@ -26,14 +26,14 @@ router.get('/', (req, res, next) => {
return asset;
}),
// Get the public settings.
Setting.getPublicSettings()
// Get the moderation setting from the settings.
Setting.retrieve()
])
.then(([asset, settings]) => {
// Merge the asset specific settings with the returned settings object in
// the event that the asset that was returned also had settings.
if (asset.settings) {
if (asset && asset.settings) {
settings = Object.assign({}, settings, asset.settings);
}
@@ -70,17 +70,7 @@ router.get('/', (req, res, next) => {
let users = userIDs.length > 0 ? User.findByIdArray(userIDs) : [];
// Fetch the actions for pretty much everything at this point.
let actions = Action.getActionSummaries(_.uniq([
// Actions can be on assets...
asset.id,
// Comments...
...comments.map((comment) => comment.id),
// Or Authors...
...userIDs
]), req.user ? req.user.id : false);
let actions = Action.getActionSummariesFromComments(asset.id, comments, req.user ? req.user.id : false);
return Promise.all([
@@ -108,7 +98,7 @@ router.get('/', (req, res, next) => {
comments,
users,
actions,
settings
settings: Setting.public(settings)
});
})
.catch(error => {
+1 -1
View File
@@ -20,7 +20,7 @@ const wordlist = {
*/
wordlist.init = () => {
return Setting
.getSettings()
.retrieve()
.then((settings) => {
// Insert the settings wordlist.
+5 -1
View File
@@ -4,8 +4,12 @@
"node": true,
"mocha": true
},
"plugins": [
"mocha"
],
"extends": "../.eslintrc.json",
"rules": {
"no-undef": [0]
"no-undef": [0],
"mocha/no-exclusive-tests": "warn"
}
}
+23 -24
View File
@@ -1,35 +1,34 @@
const Action = require('../../models/action');
const expect = require('chai').expect;
describe('Action: models', () => {
let mockActions;
describe('models.Action', () => {
let mockActions = [];
beforeEach(() => {
return Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
mockActions = actions;
});
});
beforeEach(() => Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
mockActions = actions;
}));
describe('#findById()', () => {
it('should find an action by id', () => {
return Action.findById(mockActions[0].id).then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('action_type', 'flag');
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ const expect = chai.expect;
// Use the chai should.
chai.should();
describe('Asset: model', () => {
describe('models.Asset', () => {
beforeEach(() => {
const defaults = {url:'http://test.com'};
+129 -23
View File
@@ -7,35 +7,57 @@ const settings = {id: '1', moderation: 'pre'};
const expect = require('chai').expect;
describe('Comment: models', () => {
describe('models.Comment', () => {
const comments = [{
body: 'comment 10',
asset_id: '123',
status: '',
status: [],
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
status: 'accepted',
status: [{
type: 'accepted'
}],
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
status: '',
status: [],
parent_id: '',
author_id: '456',
id: '3'
}, {
body: 'comment 40',
asset_id: '123',
status: 'rejected',
status: [{
type: 'rejected'
}],
parent_id: '',
author_id: '456',
id: '4'
}, {
body: 'comment 50',
asset_id: '1234',
status: [{
type: 'premod'
}],
parent_id: '',
author_id: '456',
id: '5'
}, {
body: 'comment 60',
asset_id: '1234',
status: [{
type: 'premod'
}],
parent_id: '',
author_id: '456',
id: '6'
}];
const users = [{
@@ -60,25 +82,69 @@ describe('Comment: models', () => {
user_id: '456'
}];
beforeEach(() => {
return Promise.all([
Setting.create(settings),
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
]);
beforeEach(() => Promise.all([
Setting.init(settings),
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
]));
describe('#publicCreate()', () => {
it('creates a new comment', () => {
return Comment.publicCreate({
body: 'This is a comment!',
status: 'accepted'
}).then((c) => {
expect(c).to.not.be.null;
expect(c.id).to.not.be.null;
expect(c.id).to.be.uuid;
expect(c.status).to.have.length(1);
expect(c.status[0]).to.have.property('type', 'accepted');
});
});
it('creates many new comments', () => {
return Comment.publicCreate([{
body: 'This is a comment!',
status: 'accepted'
}, {
body: 'This is another comment!'
}, {
body: 'This is a rejected comment!',
status: 'rejected'
}]).then(([c1, c2, c3]) => {
expect(c1).to.not.be.null;
expect(c1.id).to.be.uuid;
expect(c1.status).to.have.length(1);
expect(c1.status[0]).to.have.property('type', 'accepted');
expect(c2).to.not.be.null;
expect(c2.id).to.be.uuid;
expect(c2.status).to.have.length(0);
expect(c3).to.not.be.null;
expect(c3.id).to.be.uuid;
expect(c3.status).to.have.length(1);
expect(c3.status[0]).to.have.property('type', 'rejected');
});
});
});
describe('#findById()', () => {
it('should find a comment by id', () => {
return Comment.findById('1').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('body', 'comment 10');
});
});
});
describe('#findByAssetId()', () => {
it('should find an array of all comments by asset id', () => {
return Comment.findByAssetId('123').then((result) => {
expect(result).to.have.length(3);
@@ -91,6 +157,7 @@ describe('Comment: models', () => {
expect(result[2]).to.have.property('body', 'comment 40');
});
});
it('should find an array of accepted comments by asset id', () => {
return Comment.findAcceptedByAssetId('123').then((result) => {
expect(result).to.have.length(1);
@@ -101,6 +168,7 @@ describe('Comment: models', () => {
expect(result[0]).to.have.property('body', 'comment 20');
});
});
it('should find an array of new and accepted comments by asset id', () => {
return Comment.findAcceptedAndNewByAssetId('123').then((result) => {
expect(result).to.have.length(2);
@@ -112,13 +180,16 @@ describe('Comment: models', () => {
});
});
});
describe('#moderationQueue()', () => {
it('should find an array of new comments to moderate when pre-moderation', () => {
return Comment.moderationQueue('pre').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.lengthOf(2);
});
});
it('should find an array of new comments to moderate when post-moderation', () => {
return Comment.moderationQueue('post').then((result) => {
expect(result).to.not.be.null;
@@ -126,21 +197,56 @@ describe('Comment: models', () => {
expect(result[0]).to.have.property('body', 'comment 30');
});
});
// it('should fail when the moderation is not pre or post', () => {
// return Comment.moderationQueue('any').catch(function(error) {
// expect(error).to.not.be.null;
// });
// });
});
describe('#removeAction', () => {
it('should remove an action', () => {
return Comment.removeAction('3', '123', 'flag').then(() => {
return Action.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
return Comment.removeAction('3', '123', 'flag')
.then(() => {
return Action.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
});
});
describe('#changeStatus', () => {
it('should change the status of a comment from no status', () => {
let comment_id = comments[0].id;
return Comment.findById(comment_id)
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(0);
return Comment.pushStatus(comment_id, 'rejected', '123');
})
.then(() => Comment.findById(comment_id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(1);
expect(c.status[0]).to.have.property('type', 'rejected');
expect(c.status[0]).to.have.property('assigned_by', '123');
});
});
it('should change the status of a comment from accepted', () => {
return Comment.pushStatus(comments[1].id, 'rejected', '123')
.then(() => Comment.findById(comments[1].id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(2);
expect(c.status[0]).to.have.property('type', 'accepted');
expect(c.status[0]).to.have.property('assigned_by', null);
expect(c.status[1]).to.have.property('type', 'rejected');
expect(c.status[1]).to.have.property('assigned_by', '123');
});
});
});
});
+9 -17
View File
@@ -1,34 +1,29 @@
const Setting = require('../../models/setting');
const expect = require('chai').expect;
describe('Setting: model', () => {
describe('models.Setting', () => {
beforeEach(() => {
const defaults = {
id: 1
};
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
beforeEach(() => Setting.init({moderation: 'pre'}));
describe('#getSettings()', () => {
describe('#retrieve()', () => {
it('should have a moderation field defined', () => {
return Setting.getSettings().then(settings => {
return Setting.retrieve().then(settings => {
expect(settings).to.have.property('moderation').and.to.equal('pre');
});
});
it('should have two infoBox fields defined', () => {
return Setting.getSettings().then(settings => {
return Setting.retrieve().then(settings => {
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
});
});
});
describe('#updateSettings()', () => {
describe('#update()', () => {
it('should update the settings with a passed object', () => {
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
return Setting.updateSettings(mockSettings).then(updatedSettings => {
return Setting.update(mockSettings).then(updatedSettings => {
expect(updatedSettings).to.be.an('object');
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
expect(updatedSettings).to.have.property('infoBoxEnable', true);
@@ -37,13 +32,10 @@ describe('Setting: model', () => {
});
});
describe('#getPublicSettings', () => {
describe('#get', () => {
it('should return the moderation settings', () => {
return Setting.getPublicSettings().then(({moderation, infoBoxEnable, infoBoxContent, wordlist}) => {
return Setting.retrieve().then(({moderation}) => {
expect(moderation).not.to.be.null;
expect(infoBoxEnable).not.to.be.null;
expect(infoBoxContent).not.to.be.null;
expect(wordlist).to.be.undefined;
});
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
const User = require('../../models/user');
const expect = require('chai').expect;
describe('User: models', () => {
describe('models.User', () => {
let mockUsers;
beforeEach(() => {
return User.createLocalUsers([{
+101 -65
View File
@@ -10,6 +10,7 @@ chai.use(require('chai-http'));
const wordlist = require('../../../../services/wordlist');
const Comment = require('../../../../models/comment');
const Asset = require('../../../../models/asset');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
@@ -17,63 +18,71 @@ const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
describe('/api/v1/comments', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
id: 'def-rejected',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
author_id: '456',
status: 'accepted'
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions),
wordlist.insert([
'bad words'
]),
Setting.create(settings)
]);
});
describe('#get', () => {
const comments = [{
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: [{
type: 'rejected'
}]
}, {
body: 'comment 30',
asset_id: '456',
status: [{
type: 'accepted'
}]
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments).then((newComments) => {
newComments.forEach((comment, i) => {
comments[i].id = comment.id;
});
actions[0].item_id = comments[0].id;
actions[1].item_id = comments[1].id;
return Action.create(actions);
}),
User.createLocalUsers(users),
wordlist.insert([
'bad words'
]),
Setting.init(settings)
]);
});
it('should return all the comments', () => {
return chai.request(app)
.get('/api/v1/comments')
@@ -91,7 +100,9 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('id', 'def-rejected');
expect(res.body).to.have.property('comments');
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', comments[2].id);
});
});
@@ -102,7 +113,7 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', 'hij');
expect(res.body.comments[0]).to.have.property('id', comments[3].id);
});
});
@@ -124,8 +135,7 @@ describe('/api/v1/comments', () => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', 'abc');
expect(res.body.comments[0]).to.have.property('id', comments[0].id);
});
});
});
@@ -151,7 +161,31 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', 'rejected');
expect(res.body).to.have.property('status').and.to.have.length(1);
expect(res.body.status[0]).to.have.property('type', 'rejected');
});
});
it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {moderation: 'pre'})
.then(() => asset);
})
.then((asset) => {
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('asset_id');
expect(res.body).to.have.property('status').and.to.have.length(1);
expect(res.body.status[0]).to.have.property('type', 'premod');
});
});
});
@@ -267,18 +301,22 @@ describe('/api/v1/comments/:comment_id/actions', () => {
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: ''
status: []
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
status: [{
type: 'rejected'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
status: [{
type: 'accepted'
}]
}];
const users = [{
@@ -316,10 +354,8 @@ describe('/api/v1/comments/:comment_id/actions', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('item_type', 'comment');
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('item_id', 'abc');
expect(res.body).to.have.property('user_id', '456');
});
});
});
+62 -62
View File
@@ -15,56 +15,54 @@ const User = require('../../../../models/user');
const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
beforeEach(() => {
return Setting.create(settings);
});
describe('/api/v1/queue', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: [{
type: 'rejected'
}]
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: [{
type: 'premod'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: [{
type: 'accepted'
}]
}];
describe('Get moderation queues rejected, pending, flags', () => {
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
describe('/api/v1/queue', () => {
let comments;
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
const users = [{
id: '456',
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
id: '123',
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
let actions;
beforeEach(() => {
comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
status: 'rejected'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
}];
actions = [{
action_type: 'flag',
item_type: 'comment'
}, {
action_type: 'like',
item_type: 'comment'
}];
return User.createLocalUsers(users)
beforeEach(() => {
return User.createLocalUsers(users)
.then((u) => {
comments[0].author_id = u[0].id;
comments[1].author_id = u[1].id;
@@ -76,22 +74,24 @@ describe('Get moderation queues rejected, pending, flags', () => {
actions[0].item_id = c[0].id;
actions[1].item_id = c[1].id;
return Action.create(actions);
return Promise.all([
Action.create(actions),
Setting.init(settings)
]);
});
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
});
+2 -2
View File
@@ -12,7 +12,7 @@ const defaults = {id: '1', moderation: 'pre'};
describe('/api/v1/settings', () => {
beforeEach(() => Setting.create(defaults));
beforeEach(() => Setting.init(defaults));
describe('#get', () => {
@@ -40,7 +40,7 @@ describe('/api/v1/settings', () => {
.then((res) => {
expect(res).to.have.status(204);
return Setting.getSettings();
return Setting.retrieve();
})
.then((settings) => {
expect(settings).to.have.property('moderation', 'post');
+33 -29
View File
@@ -20,7 +20,37 @@ describe('/api/v1/stream', () => {
moderation: 'post'
};
let comments;
const comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: []
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: [{
type: 'rejected'
}]
}];
const users = [{
displayName: 'Ana',
@@ -41,33 +71,6 @@ describe('/api/v1/stream', () => {
}];
beforeEach(() => {
comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: 'accepted'
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: ''
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: 'accepted'
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: 'rejected'
}];
return Promise.all([
User.createLocalUsers(users),
Asset.findOrCreateByUrl('http://test.com'),
@@ -94,10 +97,11 @@ describe('/api/v1/stream', () => {
return Promise.all([
Comment.create(comments),
Action.create(actions),
Setting.init().then(() => Setting.updateSettings(settings))
Setting.init(settings)
]);
});
});
it('should return a stream with comments, users and actions for an existing asset', () => {
return chai.request(app)
.get('/api/v1/stream')