mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 10:54:11 +08:00
Merge branch 'master' into bug-comment-number
This commit is contained in:
@@ -17,30 +17,29 @@ To launch a Talk server of your own from your browser without any need to muck a
|
||||
|
||||
### Configuration
|
||||
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
The Talk application looks for the following configuration values either as environment variables:
|
||||
|
||||
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
|
||||
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies.
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_SMTP_EMAIL` (*required*) - the address to send emails from using the
|
||||
|
||||
- `TALK_FACEBOOK_APP_ID` (*required for login via fb*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required for login via fb*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
|
||||
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
|
||||
SMTP provider.
|
||||
- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required*) - SMTP port.
|
||||
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
|
||||
|
||||
|
||||
### Install from Source
|
||||
|
||||
If you want to run Talk in development mode from source (without docker) you can read the [INSTALL file](INSTALL.md).
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
|
||||
### License
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ import coralApi from '../../../coral-framework/helpers/response';
|
||||
import * as commentTypes from '../constants/comments';
|
||||
import * as actionTypes from '../constants/actions';
|
||||
|
||||
function addUsersCommentsActions (dispatch, {comments, users, actions}) {
|
||||
dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
|
||||
dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions});
|
||||
}
|
||||
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchModerationQueueComments = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return Promise.all([
|
||||
coralApi('/queue/comments/pending'),
|
||||
coralApi('/comments?status=rejected'),
|
||||
coralApi('/comments?action_type=flag')
|
||||
coralApi('/queue/comments/rejected'),
|
||||
coralApi('/queue/comments/flagged')
|
||||
])
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
|
||||
@@ -21,14 +28,38 @@ export const fetchModerationQueueComments = () => {
|
||||
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
|
||||
};
|
||||
})
|
||||
.then(({comments, users, actions}) => {
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
/* Post comments and users to redux store. Actions will be posted when they are needed. */
|
||||
dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
|
||||
dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions});
|
||||
export const fetchPendingQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
});
|
||||
return coralApi('/queue/comments/pending')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchRejectedQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/rejected')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchFlaggedQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/flagged')
|
||||
.then(results => {
|
||||
results.comments.forEach(comment => comment.flagged = true);
|
||||
return results;
|
||||
})
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
updateStatus,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
fetchModerationQueueComments
|
||||
fetchPendingQueue,
|
||||
fetchRejectedQueue,
|
||||
fetchFlaggedQueue,
|
||||
fetchModerationQueueComments,
|
||||
} from 'actions/comments';
|
||||
import {userStatusUpdate} from 'actions/users';
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
@@ -54,6 +57,16 @@ class ModerationContainer extends React.Component {
|
||||
|
||||
onTabClick(activeTab) {
|
||||
this.setState({activeTab});
|
||||
|
||||
if (activeTab === 'pending') {
|
||||
this.props.fetchPendingQueue();
|
||||
} else if (activeTab === 'rejected') {
|
||||
this.props.fetchRejectedQueue();
|
||||
} else if (activeTab === 'flagged') {
|
||||
this.props.fetchFlaggedQueue();
|
||||
} else {
|
||||
this.props.fetchModerationQueueComments();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
@@ -90,6 +103,9 @@ const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchSettings: () => dispatch(fetchSettings()),
|
||||
fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()),
|
||||
fetchPendingQueue: () => dispatch(fetchPendingQueue()),
|
||||
fetchRejectedQueue: () => dispatch(fetchRejectedQueue()),
|
||||
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
|
||||
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
banUser: (userId, commentId) => dispatch(userStatusUpdate('banned', userId, commentId)).then(() => {
|
||||
|
||||
@@ -22,49 +22,61 @@ export default ({onTabClick, ...props}) => (
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
<CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.banUser}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
{
|
||||
props.activeTab === 'pending'
|
||||
? <div>
|
||||
<CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading} />
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.banUser}
|
||||
user={props.comments.banUser} />
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
<CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.rejectedIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading}
|
||||
/>
|
||||
{
|
||||
props.activeTab === 'rejected'
|
||||
? <CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.rejectedIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading} />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
||||
<CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.flaggedIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
modActions={['reject', 'approve']}
|
||||
loading={props.comments.loading}/>
|
||||
</div>
|
||||
{
|
||||
props.activeTab === 'flagged'
|
||||
? <CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'flagged'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.flaggedIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
modActions={['reject', 'approve']}
|
||||
loading={props.comments.loading} />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"PASSWORD_REQUIRED": "Must input a password",
|
||||
"PASSWORD_LENGTH": "Password is too short",
|
||||
"EMAIL_IN_USE": "Email address already in use",
|
||||
"EMAIL_DISPLAY_NAME_IN_USE": "Email address or display name already in use",
|
||||
"DISPLAYNAME_IN_USE": "Display name already in use",
|
||||
"DISPLAY_NAME_REQUIRED": "Must input a display name",
|
||||
"NO_SPECIAL_CHARACTERS": "Display names can contain letters, numbers and _ only",
|
||||
"PROFANITY_ERROR": "Display names must not contain profanity. Please contact the administrator if you believe this to be in error."
|
||||
@@ -34,6 +36,8 @@
|
||||
"PASSWORD_REQUIRED": "Debe ingresar una contraseña",
|
||||
"PASSWORD_LENGTH": "La contraseña es muy corta",
|
||||
"EMAIL_IN_USE": "La dirección de correo electrónico se encuentra en uso",
|
||||
"EMAIL_DISPLAY_NAME_IN_USE": "Correo o Nombre en uso.",
|
||||
"DISPLAYNAME_IN_USE": "Nombre en uso.",
|
||||
"DISPLAY_NAME_REQUIRED": "Debe ingresar un nombre",
|
||||
"NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _",
|
||||
"PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al administrador si cree que esto es un error"
|
||||
|
||||
@@ -19,6 +19,7 @@ export default {
|
||||
alreadyHaveAnAccount: 'Already have an account?',
|
||||
recoverPassword: 'Recover password',
|
||||
emailInUse: 'Email address already in use',
|
||||
emailORusernameInUse: 'Email address or Username already in use',
|
||||
requiredField: 'This field is required',
|
||||
passwordsDontMatch: 'Passwords don\'t match.',
|
||||
specialCharacters: 'Display names can contain letters, numbers and _ only',
|
||||
@@ -45,6 +46,7 @@ export default {
|
||||
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
|
||||
recoverPassword: 'Recuperar contraseña',
|
||||
emailInUse: 'Este email se encuentra en uso',
|
||||
emailORusernameInUse: 'Este email ó nombre se encuentran en uso',
|
||||
requiredField: 'Este campo es requerido',
|
||||
passwordsDontMatch: 'Las contraseñas no coinciden',
|
||||
specialCharacters: 'Los nombres pueden contener letras, números y _',
|
||||
|
||||
@@ -54,6 +54,11 @@ const ErrEmailTaken = new APIError('Email address already in use', {
|
||||
status: 400
|
||||
});
|
||||
|
||||
const ErrDisplayTaken = new APIError('Display name already in use', {
|
||||
translation_key: 'DISPLAYNAME_IN_USE',
|
||||
status: 400
|
||||
});
|
||||
|
||||
const ErrSpecialChars = new APIError('No special characters are allowed in a display name', {
|
||||
translation_key: 'NO_SPECIAL_CHARACTERS',
|
||||
status: 400
|
||||
@@ -116,6 +121,7 @@ module.exports = {
|
||||
ErrSpecialChars,
|
||||
ErrMissingDisplay,
|
||||
ErrContainsProfanity,
|
||||
ErrDisplayTaken,
|
||||
ErrAssetCommentingClosed,
|
||||
ErrNotFound,
|
||||
ErrInvalidAssetURL,
|
||||
|
||||
+36
-90
@@ -47,6 +47,7 @@ const CommentSchema = new Schema({
|
||||
asset_id: String,
|
||||
author_id: String,
|
||||
status_history: [StatusSchema],
|
||||
status: {type: String, default: null},
|
||||
parent_id: String
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -84,24 +85,6 @@ CommentSchema.method('filterForUser', function(user = false) {
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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_history.
|
||||
*/
|
||||
CommentSchema.virtual('status').get(function() {
|
||||
|
||||
// Here we are taking advantage of the fact that when documents are inserted
|
||||
// for the new status on a comment that they are always appended to the end
|
||||
// of the list in the order that they are inserted, hence, the last status
|
||||
// is always the most recent.
|
||||
if (this.status_history && this.status_history.length > 0) {
|
||||
return this.status_history[this.status_history.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.
|
||||
@@ -118,7 +101,7 @@ CommentSchema.statics.publicCreate = (comment) => {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status = false,
|
||||
status = null,
|
||||
author_id
|
||||
} = comment;
|
||||
|
||||
@@ -130,6 +113,7 @@ CommentSchema.statics.publicCreate = (comment) => {
|
||||
type: status,
|
||||
created_at: new Date()
|
||||
}] : [],
|
||||
status,
|
||||
author_id
|
||||
});
|
||||
|
||||
@@ -153,33 +137,18 @@ CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
|
||||
});
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* findByAssetIdWithStatuses finds all the comments where the asset id matches
|
||||
* what's provided and the status is one of the ones listed in the statuses
|
||||
* array.
|
||||
* @param {String} asset_id the asset id to search by
|
||||
* @param {Array} [statuses=[]] the array of statuses to search by
|
||||
* @return {Promise} resolves to an array of comments
|
||||
*/
|
||||
CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
|
||||
CommentSchema.statics.findByAssetIdWithStatuses = (asset_id, statuses = []) => Comment.find({
|
||||
asset_id,
|
||||
'status_history.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 = (asset_id) => Comment.find({
|
||||
asset_id,
|
||||
$or: [
|
||||
{
|
||||
'status_history.type': 'accepted'
|
||||
},
|
||||
{
|
||||
status_history: {
|
||||
$size: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
status: {
|
||||
$in: statuses
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -205,58 +174,26 @@ CommentSchema.statics.findIdsByActionType = (action_type) => Action
|
||||
.then((actions) => actions.map(a => a.item_id));
|
||||
|
||||
/**
|
||||
* Find comments by their status_history.
|
||||
* @param {String} status the status of the comment to search for
|
||||
* @return {Promise}
|
||||
* Find comments by current status
|
||||
* @param {String} status status of the comment to search for
|
||||
* @return {Promise} resovles to comment array
|
||||
*/
|
||||
CommentSchema.statics.findByStatus = (status = false) => {
|
||||
let q = {};
|
||||
|
||||
if (status) {
|
||||
q['status_history.type'] = status;
|
||||
} else {
|
||||
q.status_history = {$size: 0};
|
||||
}
|
||||
|
||||
return Comment.find(q);
|
||||
CommentSchema.statics.findByStatus = (status = null) => {
|
||||
return Comment.find({status});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find comments that need to be moderated (aka moderation queue).
|
||||
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
|
||||
* @param {String} asset_id
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
|
||||
CommentSchema.statics.moderationQueue = (status, asset_id = null) => {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
// Fetch the comments with statuses.
|
||||
let comments = Comment.findByStatus(status);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
})));
|
||||
if (asset_id) {
|
||||
comments = comments.where('asset_id', asset_id);
|
||||
}
|
||||
|
||||
return comments;
|
||||
@@ -277,7 +214,8 @@ CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.u
|
||||
created_at: new Date(),
|
||||
assigned_by
|
||||
}
|
||||
}
|
||||
},
|
||||
$set: {status}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -328,8 +266,16 @@ CommentSchema.statics.all = () => Comment.find();
|
||||
* probably to be paginated at some point in the future
|
||||
* @return {Promise} array resolves to an array of comments by that user
|
||||
*/
|
||||
CommentSchema.statics.findByUserId = function (author_id) {
|
||||
return Comment.find({author_id});
|
||||
CommentSchema.statics.findByUserId = function (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 Comment.find(query);
|
||||
};
|
||||
|
||||
// Comment model.
|
||||
|
||||
+8
-1
@@ -81,7 +81,11 @@ const UserSchema = new mongoose.Schema({
|
||||
|
||||
// This is sourced from the social provider or set manually during user setup
|
||||
// and simply provides a name to display for the given user.
|
||||
displayName: String,
|
||||
displayName: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true
|
||||
},
|
||||
|
||||
// This is true when the user account is disabled, no action should be
|
||||
// acknowledged when they are disabled. Logins are also prevented.
|
||||
@@ -379,6 +383,9 @@ UserService.createLocalUser = (email, password, displayName) => {
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
if (err.message.match('displayName')) {
|
||||
return reject(errors.ErrDisplayTaken);
|
||||
}
|
||||
return reject(errors.ErrEmailTaken);
|
||||
}
|
||||
return reject(err);
|
||||
|
||||
@@ -48,7 +48,7 @@ router.get('/', (req, res, next) => {
|
||||
// otherwise this will be a vulnerability if you pass user_id and something else,
|
||||
// the app will return admin-level data without the proper checks
|
||||
if (user_id) {
|
||||
query = Comment.findByUserId(user_id);
|
||||
query = Comment.findByUserId(user_id, authorization.has(req.user, 'admin'));
|
||||
} else if (status) {
|
||||
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
|
||||
} else if (action_type) {
|
||||
@@ -57,7 +57,7 @@ router.get('/', (req, res, next) => {
|
||||
.then((ids) => assetIDWrap(Comment.find({
|
||||
id: {
|
||||
$in: ids
|
||||
},
|
||||
}
|
||||
})));
|
||||
} else {
|
||||
query = assetIDWrap(Comment.all());
|
||||
@@ -124,7 +124,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : '';
|
||||
return moderation === 'pre' ? 'premod' : null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+54
-42
@@ -2,12 +2,22 @@ const express = require('express');
|
||||
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 authorization = require('../../../middleware/authorization');
|
||||
const _ = require('lodash');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function gatherActionsAndUsers (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)
|
||||
]))
|
||||
]);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Get Routes
|
||||
//==============================================================================
|
||||
@@ -16,49 +26,51 @@ const router = express.Router();
|
||||
// depending on the settings. The :moderation overwrites this settings.
|
||||
// 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) => {
|
||||
router.get('/comments/pending', authorization.needed('admin'), (req, res, next) => {
|
||||
|
||||
const {
|
||||
asset_id
|
||||
} = req.query;
|
||||
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)
|
||||
]))
|
||||
]);
|
||||
})
|
||||
Comment.moderationQueue('premod', asset_id)
|
||||
.then(gatherActionsAndUsers)
|
||||
.then(([comments, users, actions]) => {
|
||||
res.json({
|
||||
comments,
|
||||
users,
|
||||
actions
|
||||
});
|
||||
res.json({comments, users, actions});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/comments/rejected', authorization.needed('admin'), (req, res, next) => {
|
||||
const {asset_id} = req.query;
|
||||
|
||||
Comment.moderationQueue('rejected', asset_id)
|
||||
.then(gatherActionsAndUsers)
|
||||
.then(([comments, users, actions]) => {
|
||||
res.json({comments, users, actions});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/comments/flagged', authorization.needed('admin'), (req, res, next) => {
|
||||
const {asset_id} = req.query;
|
||||
|
||||
const assetIDWrap = (query) => {
|
||||
if (asset_id) {
|
||||
query = query.where('asset_id', asset_id);
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
Comment.findIdsByActionType('flag')
|
||||
.then(ids => assetIDWrap(Comment.find({
|
||||
id: {$in: ids}
|
||||
})))
|
||||
.then(gatherActionsAndUsers)
|
||||
.then(([comments, users, actions]) => {
|
||||
res.json({comments, users, actions});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
|
||||
@@ -48,20 +48,11 @@ router.get('/', (req, res, next) => {
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
// Fetch the appropriate comments stream.
|
||||
let comments;
|
||||
|
||||
if (settings.moderation === 'pre') {
|
||||
comments = Comment.findAcceptedByAssetId(asset.id);
|
||||
} else {
|
||||
comments = Comment.findAcceptedAndNewByAssetId(asset.id);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
|
||||
// This is the promised component... Fetch the comments based on the
|
||||
// moderation settings.
|
||||
comments,
|
||||
Comment.findByAssetIdWithStatuses(asset.id, [null, 'accepted']),
|
||||
|
||||
// Send back the reference to the asset.
|
||||
asset,
|
||||
|
||||
+25
-31
@@ -21,6 +21,7 @@ describe('models.Comment', () => {
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}],
|
||||
status: 'accepted',
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '2'
|
||||
@@ -37,6 +38,7 @@ describe('models.Comment', () => {
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}],
|
||||
status: 'rejected',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '4'
|
||||
@@ -46,6 +48,7 @@ describe('models.Comment', () => {
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
status: 'premod',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '5'
|
||||
@@ -55,6 +58,7 @@ describe('models.Comment', () => {
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
status: 'premod',
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '6'
|
||||
@@ -157,47 +161,17 @@ describe('models.Comment', () => {
|
||||
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);
|
||||
result.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
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);
|
||||
result.sort((a, b) => {
|
||||
if (a.body < b.body) {return -1;}
|
||||
else {return 1;}
|
||||
});
|
||||
expect(result[0]).to.have.property('body', 'comment 10');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#moderationQueue()', () => {
|
||||
|
||||
it('should find an array of new comments to moderate when pre-moderation', () => {
|
||||
return Comment.moderationQueue('pre').then((result) => {
|
||||
return Comment.moderationQueue('premod').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;
|
||||
expect(result).to.have.lengthOf(1);
|
||||
expect(result[0]).to.have.property('body', 'comment 30');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#removeAction', () => {
|
||||
@@ -213,6 +187,23 @@ describe('models.Comment', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findByUserId', () => {
|
||||
it('should return all comments if admin', () => {
|
||||
return Comment.findByUserId('456', true)
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return premod and rejected comments if not admin', () => {
|
||||
return Comment.findByUserId('456')
|
||||
.then(comments => {
|
||||
expect(comments).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#changeStatus', () => {
|
||||
|
||||
it('should change the status of a comment from no status', () => {
|
||||
@@ -227,6 +218,7 @@ describe('models.Comment', () => {
|
||||
.then(() => Comment.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');
|
||||
@@ -238,6 +230,8 @@ describe('models.Comment', () => {
|
||||
.then(() => Comment.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.equal('rejected');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
@@ -85,6 +85,22 @@ describe('models.User', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('#createLocalUser', () => {
|
||||
it('should not create a user with duplicate display name', () => {
|
||||
return User.createLocalUsers([{
|
||||
email: 'otrostampi@gmail.com',
|
||||
displayName: 'Stampi',
|
||||
password: '1Coralito!'
|
||||
}])
|
||||
.then((user) => {
|
||||
expect(user).to.be.null;
|
||||
})
|
||||
.catch((error) => {
|
||||
expect(error).to.not.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createEmailConfirmToken', () => {
|
||||
|
||||
it('should create a token for a valid user', () => {
|
||||
|
||||
@@ -34,12 +34,14 @@ describe('/api/v1/comments', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
@@ -81,15 +83,14 @@ describe('/api/v1/comments', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return only the owner’s comments if the user is not an admin', () => {
|
||||
it('should return only the owner’s published comments if the user is not an admin', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?user_id=456')
|
||||
.set(passport.inject({id: '456', roles: []}))
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments).to.have.length(2);
|
||||
expect(res.body.comments).to.have.length(1);
|
||||
expect(res.body.comments[0]).to.have.property('author_id', '456');
|
||||
expect(res.body.comments[1]).to.have.property('author_id', '456');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,6 +191,7 @@ 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', 'premod');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,6 +214,7 @@ describe('/api/v1/comments', () => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status', null);
|
||||
|
||||
return Promise.all([
|
||||
res.body,
|
||||
Action.findByType('flag', 'comments')
|
||||
@@ -250,6 +253,27 @@ describe('/api/v1/comments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with null status if it\'s asset is has post-moderation enabled', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {moderation: 'post'})
|
||||
.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', null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a rejected comment if the body is above the character count', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
@@ -29,6 +30,7 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'premod',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}]
|
||||
@@ -36,6 +38,7 @@ describe('/api/v1/queue', () => {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
@@ -89,6 +92,7 @@ describe('/api/v1/queue', () => {
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments).to.have.length(1);
|
||||
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');
|
||||
|
||||
@@ -24,11 +24,27 @@ describe('/api/v1/stream', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const assets = [
|
||||
{
|
||||
url: 'https://example.com/article/1'
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/article/2',
|
||||
settings: {
|
||||
moderation: 'pre'
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/article/3'
|
||||
}
|
||||
];
|
||||
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
body: 'comment 10',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
@@ -37,6 +53,7 @@ describe('/api/v1/stream', () => {
|
||||
body: 'comment 20',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: null,
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'uio',
|
||||
@@ -44,6 +61,7 @@ describe('/api/v1/stream', () => {
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
parent_id: '',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
@@ -51,9 +69,32 @@ describe('/api/v1/stream', () => {
|
||||
id: 'hij',
|
||||
body: 'comment 40',
|
||||
asset_id: '456',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
status: 'premod',
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
status: 'accepted',
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 70',
|
||||
status: 'rejected',
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 70',
|
||||
status: null,
|
||||
status_history: []
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
@@ -75,42 +116,46 @@ describe('/api/v1/stream', () => {
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Setting.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
User.createLocalUsers(users),
|
||||
Asset.findOrCreateByUrl('http://test.com'),
|
||||
Asset
|
||||
.findOrCreateByUrl('http://coralproject.net/asset2')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {moderation: 'pre'})
|
||||
.then(() => asset);
|
||||
})
|
||||
])
|
||||
.then(([users, asset1, asset2]) => {
|
||||
return Setting.init(settings)
|
||||
.then(() => Promise.all([
|
||||
User.createLocalUsers(users),
|
||||
Promise.all(assets.map((asset) => Asset.create(asset)))
|
||||
]))
|
||||
.then(([mockUsers, mockAssets]) => {
|
||||
|
||||
comments[0].author_id = users[0].id;
|
||||
comments[1].author_id = users[1].id;
|
||||
comments[2].author_id = users[0].id;
|
||||
comments[3].author_id = users[1].id;
|
||||
|
||||
comments[0].asset_id = asset1.id;
|
||||
comments[1].asset_id = asset1.id;
|
||||
comments[2].asset_id = asset2.id;
|
||||
comments[3].asset_id = asset2.id;
|
||||
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
Action.create(actions)
|
||||
]);
|
||||
// Map the id's over.
|
||||
mockAssets.forEach((asset, i) => {
|
||||
assets[i].id = asset.id;
|
||||
});
|
||||
|
||||
mockUsers.forEach((user, i) => {
|
||||
users[i].id = user.id;
|
||||
});
|
||||
|
||||
comments.forEach((comment, i) => {
|
||||
comments[i].author_id = users[(i % 2) === 0 ? 0 : 1].id;
|
||||
});
|
||||
|
||||
comments[0].asset_id = assets[0].id;
|
||||
comments[1].asset_id = assets[0].id;
|
||||
comments[2].asset_id = assets[1].id;
|
||||
comments[3].asset_id = assets[1].id;
|
||||
comments[4].asset_id = assets[2].id;
|
||||
comments[5].asset_id = assets[2].id;
|
||||
comments[6].asset_id = assets[2].id;
|
||||
comments[7].asset_id = assets[2].id;
|
||||
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
Action.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a stream with comments, users and actions for an existing asset', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({'asset_url': 'http://test.com'})
|
||||
.query({asset_url: assets[0].url})
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.assets.length).to.equal(1);
|
||||
@@ -134,15 +179,47 @@ describe('/api/v1/stream', () => {
|
||||
it('should merge the settings when the asset contains settings to override it with', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({'asset_url': 'http://coralproject.net/asset2'})
|
||||
.query({asset_url: assets[1].url})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.assets.length).to.equal(1);
|
||||
expect(res.body.comments.length).to.equal(1);
|
||||
expect(res.body.users.length).to.equal(1);
|
||||
expect(res.body.assets).to.have.length(1);
|
||||
expect(res.body.comments).to.have.length(1);
|
||||
expect(res.body.users).to.have.length(1);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
expect(res.body.settings).to.not.have.property('wordlist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not change the previously displayed comments based on moderation state changes', () => {
|
||||
|
||||
let preComments, postComments;
|
||||
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[2].url})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments.length).to.equal(2);
|
||||
expect(res.body.settings).to.have.property('moderation', 'post');
|
||||
|
||||
preComments = res.body.comments;
|
||||
|
||||
return Asset.overrideSettings(assets[2].id, {moderation: 'pre'});
|
||||
})
|
||||
.then(() => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: assets[2].url});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments.length).to.equal(2);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
|
||||
postComments = res.body.comments;
|
||||
|
||||
expect(preComments).to.deep.equal(postComments);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,8 @@ util.onshutdown = (jobs) => {
|
||||
};
|
||||
|
||||
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
|
||||
// event that we have an external event.
|
||||
process.on('SIGTERM', () => util.shutdown());
|
||||
process.on('SIGINT', () => util.shutdown());
|
||||
// event that we have an external event. SIGUSR2 is called when the app is asked
|
||||
// to be 'killed', same procedure here.
|
||||
process.on('SIGTERM', () => util.shutdown());
|
||||
process.on('SIGINT', () => util.shutdown());
|
||||
process.once('SIGUSR2', () => util.shutdown());
|
||||
|
||||
Reference in New Issue
Block a user