diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index d6b77b61c..24c1b403d 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -49,7 +49,7 @@ export const setRole = (id, role) => dispatch => { }; export const setCommenterStatus = (id, status) => dispatch => { - return fetch(`${base}/user/${id}/status`, getInit('POST', {status})) + return coralApi(`/user/${id}/status`, {method: 'POST', body: {status}}) .then(() => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index b499dc1c6..998b5f774 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -12,17 +12,18 @@ const linkify = new Linkify(); // Render a single comment for the list export default props => { - const links = linkify.getMatches(props.comment.get('body')); - const banned = props.comment.get('banned'); + const author_status = props.author.get('status'); + const {comment, author} = props; + const links = linkify.getMatches(comment.get('body')); return (
  • person - {props.comment.get('name') || lang.t('comment.anon')} - {timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - {props.comment.get('flagged') ?

    {lang.t('comment.flagged')}

    : null} + {author.get('displayName') || lang.t('comment.anon')} + {timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} + {comment.get('flagged') ?

    {lang.t('comment.flagged')}

    : null}
    {links ? @@ -32,14 +33,14 @@ export default props => {
    - {banned ? + {author_status === 'banned' ? Banned User : null}
    - {props.comment.get('body')} + {comment.get('body')}
    @@ -59,6 +60,7 @@ const getActionButton = (action, i, props) => { return ( ); } diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 95b6c4117..2df70209e 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -113,13 +113,15 @@ export default class CommentList extends React.Component { } render () { - const {singleView, commentIds, comments, hideActive, key} = this.props; + const {singleView, commentIds, comments, users, hideActive, key} = this.props; const {active} = this.state; return ( ); } diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js index b1e002549..113a7bc86 100644 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream/CommentStream.js @@ -40,7 +40,7 @@ class CommentStream extends React.Component { } // Render the comment box along with the CommentList - render ({comments}, {snackbar, snackbarMsg}) { + render ({comments, users}, {snackbar, snackbarMsg}) { return (
    @@ -48,6 +48,7 @@ class CommentStream extends React.Component { singleView={false} commentIds={comments.get('ids')} comments={comments.get('byId')} + users={users.get('byId')} onClickAction={this.onClickAction} actions={['flag']} loading={comments.loading} /> @@ -57,4 +58,4 @@ class CommentStream extends React.Component { } } -export default connect(({comments}) => ({comments}))(CommentStream); +export default connect(({comments, users}) => ({comments, users}))(CommentStream); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 2de7d7d13..b1a01c234 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -90,11 +90,11 @@ class ModerationQueue extends React.Component { commentIds={ comments.get('ids') .filter(id => !comments.get('byId') - .get(id) - .get('status')) + .get(id) + .get('status')) } comments={comments.get('byId')} - commenters={commenters} + commenters={commenters.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['reject', 'approve', 'ban']} loading={comments.loading} /> @@ -113,7 +113,7 @@ class ModerationQueue extends React.Component { .get('status') === 'rejected') } comments={comments.get('byId')} - commenters={commenters} + commenters={commenters.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['approve']} loading={comments.loading} /> @@ -127,7 +127,7 @@ class ModerationQueue extends React.Component { return !data.get('status') && data.get('flagged') === true; })} comments={comments.get('byId')} - commenters={commenters} + commenters={commenters.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['reject', 'approve']} loading={comments.loading} /> @@ -140,6 +140,6 @@ class ModerationQueue extends React.Component { } } -export default connect(({comments}) => ({comments}))(ModerationQueue); +export default connect(({comments, commenters}) => ({comments, commenters}))(ModerationQueue); const lang = new I18n(translations); diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 61029539a..1f1b444fc 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -2,6 +2,7 @@ import {combineReducers} from 'redux'; import comments from 'reducers/comments'; import settings from 'reducers/settings'; import community from 'reducers/community'; +import users from 'reducers/users'; import auth from 'reducers/auth'; // Combine all reducers into a main one @@ -9,6 +10,6 @@ export default combineReducers({ settings, comments, community, - auth + auth, + users }); - diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js new file mode 100644 index 000000000..872ae904a --- /dev/null +++ b/client/coral-admin/src/reducers/users.js @@ -0,0 +1,20 @@ +import {Map, List, fromJS} from 'immutable'; + +const initialState = Map({ + byId: Map(), + ids: List() +}); + +export default (state = initialState, action) => { + switch (action.type) { + case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state); + default: return state; + } +}; + +// Replace the comment list with a new one +const replaceUsers = (action, state) => { + const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); + return state.set('byId', users) + .set('ids', List(users.keys())); +}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index b3c460007..8b9fb1a8d 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -15,9 +15,6 @@ export default store => next => action => { case 'COMMENTS_MODERATION_QUEUE_FETCH': fetchModerationQueueComments(store); break; - // case 'COMMENT_STREAM_FETCH': - // fetchCommentStream(store); - // break; case 'COMMENT_UPDATE': updateComment(store, action.comment); break; @@ -41,12 +38,31 @@ Promise.all([ coralApi('/comments?action_type=flag') ]) .then(([pending, rejected, flagged]) => { - flagged.forEach(comment => comment.flagged = true); - return [...pending, ...rejected, ...flagged]; + /* Combine seperate calls into a single object */ + let all = {}; + all.comments = pending.comments + .concat(rejected.comments) + .concat(flagged.comments.map(comment => { + comment.flagged = true; + return comment; + })); + all.users = pending.users + .concat(rejected.users) + .concat(flagged.users); + all.actions = pending.actions + .concat(rejected.actions) + .concat(flagged.actions); + return all; }) -.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', - comments: res})) -.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error})); +.then(all => { + /* Post comments and users to redux store. Actions will be posted when they are needed. */ + store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS', + users: all.users}); + store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', + comments: all.comments}); + +}); +// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error})); // Update a comment. Now to update a comment we need to send back the whole object @@ -71,8 +87,7 @@ const createComment = (store, name, comment) => { // Ban a user const banUser = (store, comment) => { - fetch(`${base}/user/${comment.get('author_id')}/status`, getInit('POST', {status: comment.get('status')})) - .then(handleResp) + coralApi(`/user/${comment.get('author_id')}/status`, {method: 'POST', body: {status: comment.get('status')}}) .then(res => store.dispatch({type: 'USER_BAN_SUCESSS', res})) .catch(error => store.dispatch({type: 'USER_BAN_FAILED', error})); }; diff --git a/models/setting.js b/models/setting.js index f0c6a1abc..e23f654a7 100644 --- a/models/setting.js +++ b/models/setting.js @@ -42,6 +42,7 @@ SettingSchema.statics.getSettings = function () { * @return {Promise} moderation the settings for how to moderate comments */ SettingSchema.statics.getModerationSetting = function () { + console.log('Getting moderation setting'); return this.findOne({id: '1'}).select('moderation'); }; diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index f766760f7..08e8bc967 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -1,7 +1,10 @@ const express = require('express'); const Comment = require('../../../models/comment'); +const User = require('../../../models/user'); +const Action = require('../../../models/action'); const wordlist = require('../../../services/wordlist'); const authorization = require('../../../middleware/authorization'); +const _ = require('lodash'); const router = express.Router(); @@ -16,9 +19,22 @@ router.get('/', authorization.needed('admin'), (req, res, next) => { query = Comment.all(); } - query.then(comments => { - res.json(comments); + 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) + ])) + ]); }) + .then(([comments, users, actions])=> + res.status(200).json({ + comments, + users, + actions + })) .catch((err) => { next(err); }); diff --git a/routes/api/queue/index.js b/routes/api/queue/index.js index f661992f1..448149387 100644 --- a/routes/api/queue/index.js +++ b/routes/api/queue/index.js @@ -1,6 +1,9 @@ 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 _ = require('lodash'); const router = express.Router(); @@ -13,11 +16,24 @@ 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.getModerationSetting().then(function({moderation}){ - Comment.moderationQueue(moderation).then((comments) => { - res.status(200).json(comments); - }); + Setting.getModerationSetting().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); }); diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js index 5a110e2d6..ce8871ab8 100644 --- a/routes/api/stream/index.js +++ b/routes/api/stream/index.js @@ -34,7 +34,7 @@ router.get('/', (req, res, next) => { // 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) { - settings = Object.assign(settings, asset.settings); + settings = Object.assign({}, settings, asset.settings); } // Fetch the appropriate comments stream. diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index 5ac9af7b7..bc8b67bc1 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -37,6 +37,7 @@ describe('/api/v1/comments', () => { id: 'hij', body: 'comment 30', asset_id: '456', + author_id: '456', status: 'accepted' }]; @@ -90,7 +91,7 @@ describe('/api/v1/comments', () => { .set(passport.inject({roles: ['admin']})) .then((res) => { expect(res).to.have.status(200); - expect(res.body[0]).to.have.property('id', 'def-rejected'); + expect(res.body.comments[0]).to.have.property('id', 'def-rejected'); }); }); @@ -100,8 +101,8 @@ describe('/api/v1/comments', () => { .set(passport.inject({roles: ['admin']})) .then((res) => { expect(res).to.have.status(200); - expect(res.body).to.have.length(1); - expect(res.body[0]).to.have.property('id', 'hij'); + expect(res.body.comments).to.have.length(1); + expect(res.body.comments[0]).to.have.property('id', 'hij'); }); }); @@ -111,7 +112,7 @@ describe('/api/v1/comments', () => { .set(passport.inject({roles: ['admin']})) .then((res) => { expect(res).to.have.status(200); - expect(res.body).to.have.length(2); + expect(res.body.comments).to.have.length(2); }); }); @@ -122,8 +123,8 @@ describe('/api/v1/comments', () => { .then((res) => { expect(res).to.have.status(200); - expect(res.body).to.have.length(1); - expect(res.body[0]).to.have.property('id', 'abc'); + expect(res.body.comments).to.have.length(1); + expect(res.body.comments[0]).to.have.property('id', 'abc'); }); }); diff --git a/tests/routes/api/queue/index.js b/tests/routes/api/queue/index.js index 733e5a1be..1e99ae61e 100644 --- a/tests/routes/api/queue/index.js +++ b/tests/routes/api/queue/index.js @@ -15,63 +15,81 @@ const User = require('../../../../models/user'); const Setting = require('../../../../models/setting'); const settings = {id: '1', moderation: 'pre'}; -describe('/api/v1/queue', () => { - const comments = [{ - id: 'abc', - body: 'comment 10', - asset_id: 'asset', - author_id: '123', - status: 'rejected' - }, { - id: 'def', - body: 'comment 20', - asset_id: 'asset', - author_id: '456' - }, { - id: 'hij', - body: 'comment 30', - asset_id: '456', - status: 'accepted' - }]; +beforeEach(() => { + return Setting.create(settings); +}); - const users = [{ - displayName: 'Ana', - email: 'ana@gmail.com', - password: '123' - }, { - displayName: 'Maria', - email: 'maria@gmail.com', - password: '123' - }]; +describe('Get moderation queues rejected, pending, flags', () => { - const actions = [{ - action_type: 'flag', - item_id: 'abc', - item_type: 'comment' - }, { - action_type: 'like', - item_id: 'hij', - item_type: 'comment' - }]; + describe('/api/v1/queue', () => { + let comments; - beforeEach(() => { - return Promise.all([ - Comment.create(comments), - User.createLocalUsers(users), - Action.create(actions), - Setting.create(settings) - ]); - }); + const users = [{ + id: '456', + displayName: 'Ana', + email: 'ana@gmail.com', + password: '123' + }, { + id: '123', + displayName: 'Maria', + email: 'maria@gmail.com', + password: '123' + }]; - describe('#get', () => { - it('should return all the pending comments', function(done){ + 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) + .then((u) => { + comments[0].author_id = u[0].id; + comments[1].author_id = u[1].id; + comments[2].author_id = u[1].id; + + return Comment.create(comments); + }) + .then((c) => { + actions[0].item_id = c[0].id; + actions[1].item_id = c[1].id; + + return Action.create(actions); + }); + }); + + 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[0]).to.have.property('id', 'def'); + 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(); }); }); diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js index ea08ceebb..6484fdc12 100644 --- a/tests/routes/api/stream/index.js +++ b/tests/routes/api/stream/index.js @@ -14,90 +14,90 @@ const Asset = require('../../../../models/asset'); const Setting = require('../../../../models/setting'); describe('/api/v1/stream', () => { + describe('#get', () => { + const settings = { + id: '1', + moderation: 'post' + }; - const settings = { - id: '1', - moderation: 'post' - }; + let comments; - const 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' - }]; + const users = [{ + displayName: 'Ana', + email: 'ana@gmail.com', + password: '123' + }, { + displayName: 'Maria', + email: 'maria@gmail.com', + password: '123' + }]; - 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' + }, { + action_type: 'like', + item_id: 'hij' + }]; - const actions = [{ - action_type: 'flag', - item_id: 'abc' - }, { - action_type: 'like', - item_id: 'hij' - }]; + beforeEach(() => { - beforeEach(() => { - - 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]) => { - - 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; + 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([ - Comment.create(comments), - Action.create(actions), - Setting.create(settings) - ]); - }); - }); + 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]) => { - describe('#get', () => { + 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), + Setting.init().then(() => Setting.updateSettings(settings)) + ]); + }); + }); it('should return a stream with comments, users and actions for an existing asset', () => { return chai.request(app) .get('/api/v1/stream')