From 845d274b9b59638f907c16dc1a2cc86e605899b9 Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 19 Dec 2016 19:54:20 -0500 Subject: [PATCH 01/39] Creatind endpoint to return pending users and action summaries. --- models/action.js | 6 ++++-- models/user.js | 10 ++++++++++ routes/api/queue/index.js | 26 ++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/models/action.js b/models/action.js index 45f828226..c53a777b5 100644 --- a/models/action.js +++ b/models/action.js @@ -126,7 +126,9 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' item_type: { $last: '$item_type' }, - + metadata: { + $push: '$metadata' + }, current_user: { $max: { $cond: { @@ -163,7 +165,7 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' }; /* - * Finds all comments for a specific action. + * Finds all actions for a specific type. * @param {String} action_type type of action * @param {String} item_type type of item the action is on */ diff --git a/models/user.js b/models/user.js index 13e1c0405..f38726ca1 100644 --- a/models/user.js +++ b/models/user.js @@ -20,6 +20,8 @@ const USER_ROLES = [ // USER_STATUSES is the list of statuses that are permitted for the user status. const USER_STATUS = [ 'active', + 'pending', + 'suspended', 'banned' ]; @@ -622,3 +624,11 @@ UserService.addAction = (item_id, user_id, action_type, field, detail) => Action field, detail }); + +/** + * Returns all users with pending moderation actions. + * @return {Promise} + */ +UserService.moderationQueue = () => { + return UserModel.find({status: 'pending'}); +}; diff --git a/routes/api/queue/index.js b/routes/api/queue/index.js index 9c85fbe94..cba0a1d3a 100644 --- a/routes/api/queue/index.js +++ b/routes/api/queue/index.js @@ -12,10 +12,7 @@ const router = express.Router(); // Get Routes //============================================================================== -// Returns back all the comments that are in the moderation queue. The moderation queue is pre or post moderated, -// 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. +// Returns back all the user actions that are in the moderation queue. router.get('/comments/pending', (req, res, next) => { const { @@ -65,4 +62,25 @@ router.get('/comments/pending', (req, res, next) => { }); }); +// Returns back all the users that are in the moderation queue. +router.get('/users/pending', (req, res, next) => { + + User.moderationQueue() + .then((users) => { + return Promise.all([ + users, + Action.getActionSummaries(users.map((user) => user.id)) + ]); + }) + .then(([users, actions]) => { + res.json({ + users, + actions + }); + }) + .catch(error => { + next(error); + }); +}); + module.exports = router; From 20505acfbe2049e82437da01803de75016f22169 Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 19 Dec 2016 20:27:58 -0500 Subject: [PATCH 02/39] Updating tests for pending users endpoint. --- models/user.js | 2 +- tests/routes/api/queue/index.js | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/models/user.js b/models/user.js index f38726ca1..820260bee 100644 --- a/models/user.js +++ b/models/user.js @@ -450,7 +450,7 @@ UserService.setStatus = (id, status, comment_id) => { }); } - if (status === 'active') { + if (status === 'active' || status === 'pending') { return UserModel.update({ id: id }, { diff --git a/tests/routes/api/queue/index.js b/tests/routes/api/queue/index.js index ce55836ab..8da6e0ab0 100644 --- a/tests/routes/api/queue/index.js +++ b/tests/routes/api/queue/index.js @@ -59,6 +59,10 @@ describe('/api/v1/queue', () => { action_type: 'like', item_id: 'hij', item_type: 'comment' + }, { + action_type: 'flag', + item_id: '123', + item_type: 'user' }]; beforeEach(() => { @@ -67,10 +71,14 @@ describe('/api/v1/queue', () => { comments[0].author_id = u[0].id; comments[1].author_id = u[1].id; comments[2].author_id = u[1].id; + actions[2].item_id = u[1].id; - return Comment.create(comments); + return Promise.all([ + Comment.create(comments), + ...u.map((u) => User.setStatus(u.id, 'pending')) + ]); }) - .then((c) => { + .then(([c]) => { actions[0].item_id = c[0].id; actions[1].item_id = c[1].id; @@ -94,4 +102,17 @@ describe('/api/v1/queue', () => { done(); }); }); + + it('should return all pending users and actions', function(done){ + chai.request(app) + .get('/api/v1/queue/users/pending') + .set(passport.inject({roles: ['admin']})) + .end(function(err, res){ + expect(err).to.be.null; + expect(res).to.have.status(200); + expect(res.body.users[0]).to.have.property('displayName'); + expect(res.body.actions[0]).to.have.property('action_type'); + done(); + }); + }); }); From dff84134042fd47323f37393539f6b9732cdc123 Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 19 Dec 2016 20:34:27 -0500 Subject: [PATCH 03/39] Adding pending users and actions to moderation queue call. --- client/coral-admin/src/actions/users.js | 2 +- .../containers/ModerationQueue/ModerationQueue.js | 2 +- client/coral-admin/src/reducers/comments.js | 2 +- client/coral-admin/src/services/talk-adapter.js | 13 ++++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index f2ff37cbd..eb5a02a55 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -9,6 +9,6 @@ export const banUser = (status, userId, commentId) => { return dispatch => { dispatch({type: 'USER_BAN', status, userId, commentId}); - dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + dispatch({type: 'MODERATION_QUEUE_FETCH'}); }; }; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index a76652b18..fd52fefc6 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -32,7 +32,7 @@ class ModerationQueue extends React.Component { // Fetch comments and bind singleView key before render componentWillMount () { - this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + this.props.dispatch({type: 'MODERATION_QUEUE_FETCH'}); key('s', () => this.setState({singleView: !this.state.singleView})); key('shift+/', () => this.setState({modalOpen: true})); key('esc', () => this.setState({modalOpen: false})); diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js index dca726b5e..ec316ba52 100644 --- a/client/coral-admin/src/reducers/comments.js +++ b/client/coral-admin/src/reducers/comments.js @@ -23,7 +23,7 @@ const initialState = Map({ // Handle the comment actions export default (state = initialState, action) => { switch (action.type) { - case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true); + case 'MODERATION_QUEUE_FETCH': return state.set('loading', true); case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state); case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false); case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action); diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 7c9bc968f..f509c8270 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -12,7 +12,7 @@ import coralApi from '../../../coral-framework/helpers/response'; export default store => next => action => { switch (action.type) { - case 'COMMENTS_MODERATION_QUEUE_FETCH': + case 'MODERATION_QUEUE_FETCH': fetchModerationQueueComments(store); break; case 'COMMENT_UPDATE': @@ -34,23 +34,26 @@ const fetchModerationQueueComments = store => Promise.all([ coralApi('/queue/comments/pending'), + coralApi('/queue/users/pending'), coralApi('/comments?status=rejected'), coralApi('/comments?action_type=flag') ]) -.then(([pending, rejected, flagged]) => { +.then(([pendingComments, pendingUsers, rejected, flagged]) => { /* Combine seperate calls into a single object */ let all = {}; - all.comments = pending.comments + all.comments = pendingComments.comments .concat(rejected.comments) .concat(flagged.comments.map(comment => { comment.flagged = true; return comment; })); - all.users = pending.users + all.users = pendingComments.users + .concat(pendingUsers.users) .concat(rejected.users) .concat(flagged.users); - all.actions = pending.actions + all.actions = pendingComments.actions + .concat(pendingUsers.actions) .concat(rejected.actions) .concat(flagged.actions); return all; From dd7bdb8a2635a35c9f96129c0a02cda08b205844 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 3 Jan 2017 15:05:45 -0500 Subject: [PATCH 04/39] Adding actions for fetching user moderation queue. --- client/coral-admin/src/actions/comments.js | 3 +- client/coral-admin/src/actions/users.js | 24 ++++++-- client/coral-admin/src/constants/user.js | 3 + tests/client/coral-admin/actions/users.js | 72 ++++++++++++++++++++++ 4 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 tests/client/coral-admin/actions/users.js diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index 5d2104560..051759b20 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,6 +1,7 @@ import coralApi from '../../../coral-framework/helpers/response'; import * as commentTypes from '../constants/comments'; import * as actionTypes from '../constants/actions'; +import * as userTypes from '../constants/users'; // Get comments to fill each of the three lists on the mod queue export const fetchModerationQueueComments = () => { @@ -24,7 +25,7 @@ export const fetchModerationQueueComments = () => { .then(({comments, users, actions}) => { /* 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: userTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index bc42a7a4c..cd6ba1f31 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,5 +1,6 @@ import coralApi from '../../../coral-framework/helpers/response'; -import * as actions from '../constants/user'; +import * as userTypes from '../constants/user'; +import * as actionTypes from '../constants/actions'; /** * Action disptacher related to users @@ -7,9 +8,24 @@ import * as actions from '../constants/user'; // change status of a user export const userStatusUpdate = (status, userId, commentId) => { return dispatch => { - dispatch({type: actions.UPDATE_STATUS_REQUEST}); + dispatch({type: userTypes.UPDATE_STATUS_REQUEST}); return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) - .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res})) - .catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error})); + .then(res => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res})) + .catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error})); + }; +}; + +// Get users to add to the mod queue +export const fetchModerationQueueUsers = () => { + return dispatch => { + dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_REQUEST}); + return coralApi('/queue/comments/pending') + .then(({users, actions}) => { + + /* Post users and actions to redux store. Actions will be posted when they are needed. */ + dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); + dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); + }) + .catch(error => dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_FAILURE, error})); }; }; diff --git a/client/coral-admin/src/constants/user.js b/client/coral-admin/src/constants/user.js index 8c0563237..78964502f 100644 --- a/client/coral-admin/src/constants/user.js +++ b/client/coral-admin/src/constants/user.js @@ -1,3 +1,6 @@ export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; +export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; +export const USERS_MODERATION_QUEUE_FETCH_FAILURE = 'USERS_MODERATION_QUEUE_FETCH_FAILURE'; +export const USERS_MODERATION_QUEUE_FETCH_REQUEST = 'USERS_MODERATION_QUEUE_FETCH_REQUEST'; diff --git a/tests/client/coral-admin/actions/users.js b/tests/client/coral-admin/actions/users.js new file mode 100644 index 000000000..02ec1cc6d --- /dev/null +++ b/tests/client/coral-admin/actions/users.js @@ -0,0 +1,72 @@ +import 'react'; +import 'redux'; +import {expect} from 'chai'; +import fetchMock from 'fetch-mock'; +import * as userActions from '../../../../client/coral-admin/src/actions/users'; +import {Map} from 'immutable'; + +import configureStore from 'redux-mock-store'; + +const mockStore = configureStore(); + +describe('User actions', () => { + let store; + + const users = [ + { + id: '123', + status: 'suspended' + }, + { + id: '456', + status: 'active' + } + ]; + + const actions = [ + { + itemType: 'user', + itemId: '123' + } + ]; + + beforeEach(() => { + store = mockStore(new Map({})); + fetchMock.restore(); + }); + + describe('fetchModerationQueueUsers', () => { + + it('should fetch users and actions pending moderation', () => { + + fetchMock.get('*', JSON.stringify({ + users, + actions + })); + + return userActions.fetchModerationQueueUsers()(store.dispatch) + .then(() => { + console.log(store.getActions()); + expect(store.getActions()[0]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'); + expect(store.getActions()[1]).to.have.property('users'). + and.to.deep.equal(users); + expect(store.getActions()[2]).to.have.property('type', 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS'); + expect(store.getActions()[2]).to.have.property('actions'). + and.to.deep.equal(actions); + }); + }); + + it('should return an error appropriatly', () => { + + fetchMock.get('*', 404); + + return userActions.fetchModerationQueueUsers()(store.dispatch) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_FAILURE'); + }); + }); + }); + +}); From 5c5a7f5ba56d96854ffec4da92e61843461e1de0 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 3 Jan 2017 15:13:09 -0500 Subject: [PATCH 05/39] Switching to central call for mod queue to support current reducer structure. --- client/coral-admin/src/actions/comments.js | 9 +-- client/coral-admin/src/actions/users.js | 18 +----- client/coral-admin/src/constants/user.js | 6 -- client/coral-admin/src/constants/users.js | 3 + client/coral-admin/src/reducers/comments.js | 2 +- client/coral-framework/actions/user.js | 2 +- client/coral-framework/reducers/user.js | 2 +- tests/client/coral-admin/actions/users.js | 72 --------------------- 8 files changed, 12 insertions(+), 102 deletions(-) delete mode 100644 client/coral-admin/src/constants/user.js create mode 100644 client/coral-admin/src/constants/users.js delete mode 100644 tests/client/coral-admin/actions/users.js diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index 051759b20..6ae699026 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -9,17 +9,18 @@ export const fetchModerationQueueComments = () => { dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); return Promise.all([ coralApi('/queue/comments/pending'), + coralApi('/queue/users/pending'), coralApi('/comments?status=rejected'), coralApi('/comments?action_type=flag') ]) - .then(([pending, rejected, flagged]) => { + .then(([pendingComments, pendingUsers, rejected, flagged]) => { /* Combine seperate calls into a single object */ flagged.comments.forEach(comment => comment.flagged = true); return { - comments: [...pending.comments, ...rejected.comments, ...flagged.comments], - users: [...pending.users, ...rejected.users, ...flagged.users], - actions: [...pending.actions, ...rejected.actions, ...flagged.actions] + comments: [...pendingComments.comments, ...rejected.comments, ...flagged.comments], + users: [...pendingComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users], + actions: [...pendingComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions] }; }) .then(({comments, users, actions}) => { diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index cd6ba1f31..957cb7387 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,6 +1,5 @@ import coralApi from '../../../coral-framework/helpers/response'; -import * as userTypes from '../constants/user'; -import * as actionTypes from '../constants/actions'; +import * as userTypes from '../constants/users'; /** * Action disptacher related to users @@ -14,18 +13,3 @@ export const userStatusUpdate = (status, userId, commentId) => { .catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error})); }; }; - -// Get users to add to the mod queue -export const fetchModerationQueueUsers = () => { - return dispatch => { - dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_REQUEST}); - return coralApi('/queue/comments/pending') - .then(({users, actions}) => { - - /* Post users and actions to redux store. Actions will be posted when they are needed. */ - dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); - dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); - }) - .catch(error => dispatch({type: userTypes.USERS_MODERATION_QUEUE_FETCH_FAILURE, error})); - }; -}; diff --git a/client/coral-admin/src/constants/user.js b/client/coral-admin/src/constants/user.js deleted file mode 100644 index 78964502f..000000000 --- a/client/coral-admin/src/constants/user.js +++ /dev/null @@ -1,6 +0,0 @@ -export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; -export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; -export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; -export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; -export const USERS_MODERATION_QUEUE_FETCH_FAILURE = 'USERS_MODERATION_QUEUE_FETCH_FAILURE'; -export const USERS_MODERATION_QUEUE_FETCH_REQUEST = 'USERS_MODERATION_QUEUE_FETCH_REQUEST'; diff --git a/client/coral-admin/src/constants/users.js b/client/coral-admin/src/constants/users.js new file mode 100644 index 000000000..8c0563237 --- /dev/null +++ b/client/coral-admin/src/constants/users.js @@ -0,0 +1,3 @@ +export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; +export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; +export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js index 6876f91dc..034016088 100644 --- a/client/coral-admin/src/reducers/comments.js +++ b/client/coral-admin/src/reducers/comments.js @@ -1,5 +1,5 @@ import * as actions from '../constants/comments'; -import * as userActions from '../constants/user'; +import * as userActions from '../constants/users'; import {Map, List, fromJS} from 'immutable'; /** diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 0ee8659d8..621723af8 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -1,4 +1,4 @@ -import * as actions from '../constants/user'; +import * as actions from '../constants/users'; import * as assetActions from '../constants/assets'; import {addNotification} from '../actions/notification'; import {addItem} from '../actions/items'; diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index bd5f78e87..0ae9df1ea 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -1,6 +1,6 @@ import {Map} from 'immutable'; import * as authActions from '../constants/auth'; -import * as actions from '../constants/user'; +import * as actions from '../constants/users'; import * as assetActions from '../constants/assets'; const initialState = Map({ diff --git a/tests/client/coral-admin/actions/users.js b/tests/client/coral-admin/actions/users.js deleted file mode 100644 index 02ec1cc6d..000000000 --- a/tests/client/coral-admin/actions/users.js +++ /dev/null @@ -1,72 +0,0 @@ -import 'react'; -import 'redux'; -import {expect} from 'chai'; -import fetchMock from 'fetch-mock'; -import * as userActions from '../../../../client/coral-admin/src/actions/users'; -import {Map} from 'immutable'; - -import configureStore from 'redux-mock-store'; - -const mockStore = configureStore(); - -describe('User actions', () => { - let store; - - const users = [ - { - id: '123', - status: 'suspended' - }, - { - id: '456', - status: 'active' - } - ]; - - const actions = [ - { - itemType: 'user', - itemId: '123' - } - ]; - - beforeEach(() => { - store = mockStore(new Map({})); - fetchMock.restore(); - }); - - describe('fetchModerationQueueUsers', () => { - - it('should fetch users and actions pending moderation', () => { - - fetchMock.get('*', JSON.stringify({ - users, - actions - })); - - return userActions.fetchModerationQueueUsers()(store.dispatch) - .then(() => { - console.log(store.getActions()); - expect(store.getActions()[0]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'); - expect(store.getActions()[1]).to.have.property('users'). - and.to.deep.equal(users); - expect(store.getActions()[2]).to.have.property('type', 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS'); - expect(store.getActions()[2]).to.have.property('actions'). - and.to.deep.equal(actions); - }); - }); - - it('should return an error appropriatly', () => { - - fetchMock.get('*', 404); - - return userActions.fetchModerationQueueUsers()(store.dispatch) - .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'USERS_MODERATION_QUEUE_FETCH_FAILURE'); - }); - }); - }); - -}); From b517adab56ff6a9b83c1dd39a9691b71a3e27a57 Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 3 Jan 2017 16:57:53 -0500 Subject: [PATCH 06/39] Moving commentList to ModerationList --- client/coral-admin/src/components/Comment.js | 2 +- .../{CommentList.css => ModerationList.css} | 0 .../{CommentList.js => ModerationList.js} | 84 ++++++++++++------- .../containers/CommentStream/CommentStream.js | 8 +- .../ModerationQueue/ModerationContainer.js | 3 +- .../ModerationQueue/ModerationQueue.js | 8 +- client/coral-framework/actions/user.js | 2 +- client/coral-framework/reducers/user.js | 2 +- docs/swagger.yaml | 8 ++ models/action.js | 6 ++ tests/e2e/pages/adminPage.js | 12 +-- 11 files changed, 87 insertions(+), 48 deletions(-) rename client/coral-admin/src/components/{CommentList.css => ModerationList.css} (100%) rename client/coral-admin/src/components/{CommentList.js => ModerationList.js} (68%) diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 17bd56714..68db89565 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -2,7 +2,7 @@ import React from 'react'; import timeago from 'timeago.js'; import Linkify from 'react-linkify'; -import styles from './CommentList.css'; +import styles from './ModerationList.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; diff --git a/client/coral-admin/src/components/CommentList.css b/client/coral-admin/src/components/ModerationList.css similarity index 100% rename from client/coral-admin/src/components/CommentList.css rename to client/coral-admin/src/components/ModerationList.css diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/ModerationList.js similarity index 68% rename from client/coral-admin/src/components/CommentList.js rename to client/coral-admin/src/components/ModerationList.js index b5eb40231..9e8d6e0ff 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -1,5 +1,5 @@ import React, {PropTypes} from 'react'; -import styles from './CommentList.css'; +import styles from './ModerationList.css'; import key from 'keymaster'; import Hammer from 'hammerjs'; import Comment from 'components/Comment'; @@ -13,15 +13,17 @@ const modActions = { }; // Renders a comment list and allow performing actions -export default class CommentList extends React.Component { +export default class ModerationList extends React.Component { static propTypes = { isActive: PropTypes.bool, singleView: PropTypes.bool, - commentIds: PropTypes.arrayOf(PropTypes.string).isRequired, - comments: PropTypes.object.isRequired, - users: PropTypes.object.isRequired, + commentIds: PropTypes.arrayOf(PropTypes.string), + actionIds: PropTypes.arrayOf(PropTypes.string), + comments: PropTypes.object, + users: PropTypes.object, + actions: PropTypes.object, onClickAction: PropTypes.func, - + // list of actions (flags, etc) associated with the comments modActions: PropTypes.arrayOf(PropTypes.string).isRequired, loading: PropTypes.bool, @@ -73,11 +75,11 @@ export default class CommentList extends React.Component { // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { this.props.modActions.filter(action => modActions[action].key).forEach(action => { - key(modActions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status)); + key(modActions[action].key, 'moderationList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status)); }); - key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down')); - key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up')); - key.setScope('commentList'); + key('j', 'moderationList', () => this.props.isActive && this.moveKeyHandler('down')); + key('k', 'moderationList', () => this.props.isActive && this.moveKeyHandler('up')); + key.setScope('moderationList'); } // Perform an action using the keys only if the comment is active @@ -111,7 +113,7 @@ export default class CommentList extends React.Component { } unbindKeyHandlers () { - key.deleteScope('commentList'); + key.deleteScope('moderationList'); } // If we are performing an action over a comment (aka removing from the list) we need to select a new active. @@ -135,31 +137,53 @@ export default class CommentList extends React.Component { this.props.onClickShowBanDialog(userId, userName, commentId); } - render () { - const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props; + mapModItems = (itemId, index) => { + const {comments, users, actions, modActions, suspectWords, hideActive} = this.props; const {active} = this.state; + // Because ids are unique, the id will either appear as an action or as a comment. + + const item = comments[itemId] || actions[itemId]; + let modItem; + + if (item.body) { + // If the item is a comment... + const author = users[item.author_id]; + modItem = ; + } else { + // If the item is an action... + modItem =

Action

; + } + return modItem; + } + + render () { + const {singleView, commentIds, actionIds, comments, actions, key} = this.props; + + // Combine moderations and actions into a single stream and sort by most recently updated. + const moderationIds = [ ...commentIds, ...actionIds ].sort((a, b) => { + const itemA = comments[a] || actions[a]; + const itemB = comments[b] || actions[b]; + return itemB.updated_at - itemA.updated_at; + }); + return (
    - {commentIds.map((commentId, index) => { - const comment = comments[commentId]; - const author = users[comment.author_id]; - return ; - })} + {moderationIds.map(this.mapModItems)}
); } diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js index 556e50463..e2fac3702 100644 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream/CommentStream.js @@ -3,11 +3,11 @@ import styles from './CommentStream.css'; import {Snackbar} from 'react-mdl'; import {connect} from 'react-redux'; import {createComment, flagComment} from 'actions/comments'; -import CommentList from 'components/CommentList'; +import ModerationList from 'components/ModerationList'; import CommentBox from 'components/CommentBox'; /** - * Renders a comment stream using a CommentList component + * Renders a comment stream using a ModerationList component * and adds a box for adding a new comment */ @@ -39,12 +39,12 @@ class CommentStream extends React.Component { } } - // Render the comment box along with the CommentList + // Render the comment box along with the ModerationList render ({comments, users}, {snackbar, snackbarMsg}) { return (
- ({ comments: state.comments.toJS(), settings: state.settings.toJS(), - users: state.users.toJS() + users: state.users.toJS(), + actions: state.actions.toJS(), }); const mapDispatchToProps = dispatch => { diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 55a2e705e..058001884 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -2,7 +2,7 @@ import React from 'react'; import styles from './ModerationQueue.css'; import ModerationKeysModal from 'components/ModerationKeysModal'; -import CommentList from 'components/CommentList'; +import ModerationList from 'components/ModerationList'; import BanUserDialog from 'components/BanUserDialog'; import I18n from 'coral-framework/modules/i18n/i18n'; @@ -22,7 +22,7 @@ export default ({onTabClick, ...props}) => ( className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}
- ( />
- ( />
- Date: Wed, 4 Jan 2017 13:39:10 -0500 Subject: [PATCH 07/39] Adding user display. --- client/coral-admin/src/components/User.js | 111 ++++++++++++++++++++++ client/coral-admin/src/constants/users.js | 1 + 2 files changed, 112 insertions(+) create mode 100644 client/coral-admin/src/components/User.js diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/components/User.js new file mode 100644 index 000000000..5fd3fafdc --- /dev/null +++ b/client/coral-admin/src/components/User.js @@ -0,0 +1,111 @@ +import React from 'react'; +import Linkify from 'react-linkify'; + +import styles from './ModerationList.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations.json'; + +import {Icon} from 'react-mdl'; +import Highlighter from 'react-highlight-words'; +import {FabButton, Button} from 'coral-ui'; + +const linkify = new Linkify(); + +// Render a single comment for the list +export default props => { + const {action, user} = props; + let userStatus = user.status; + const links = linkify.getMatches(user.bio); + + return ( +
  • +
    +
    + person + {user.displayName} +
    +
    + {links ? + Contains Link : null} +
    + {props.modActions.map((action, i) => getActionButton(action, i, props))} +
    +
    +
    + {userStatus === 'banned' ? + {lang.t('comment.banned_user')} : null} +
    +
    +
    + + + + + +
    +
    + { + action.metadata.map(metadata => { + return
    + + { + metadata.field === 'bio' ? + lang.t('user.flagged_bio') + : lang.t('user.flagged_username') + }: + + + { + metadata.reason + } + +
    ; + }) + } +
    +
  • + ); +}; + +// Get the button of the action performed over a comment if any +const getActionButton = (action, i, props) => { + const {user} = props; + const status = user.status; + const banned = (user.status === 'banned'); + + if (action === 'flag' && status) { + return null; + } + if (action === 'ban') { + return ( + + ); + } + return ( + props.onClickAction(props.actionsMap[action].status, user.id)} + /> + ); +}; + +const linkStyles = { + backgroundColor: 'rgb(255, 219, 135)', + padding: '1px 2px' +}; + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/constants/users.js b/client/coral-admin/src/constants/users.js index 8c0563237..813b5794d 100644 --- a/client/coral-admin/src/constants/users.js +++ b/client/coral-admin/src/constants/users.js @@ -1,3 +1,4 @@ export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; +export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; From 3fa0460f34d3ba801032234bbe8532097627db57 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 4 Jan 2017 14:20:00 -0500 Subject: [PATCH 08/39] Sending actions to ModerationList. --- .../src/components/ModerationList.js | 28 +++++++++++-------- .../src/containers/LayoutContainer.js | 1 - .../ModerationQueue/ModerationQueue.js | 14 +++++++--- client/coral-admin/src/reducers/actions.js | 6 ++-- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 9e8d6e0ff..9f92c9b1d 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -5,7 +5,7 @@ import Hammer from 'hammerjs'; import Comment from 'components/Comment'; // Each action has different meaning and configuration -const modActions = { +const actionsMap = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, @@ -17,12 +17,12 @@ export default class ModerationList extends React.Component { static propTypes = { isActive: PropTypes.bool, singleView: PropTypes.bool, - commentIds: PropTypes.arrayOf(PropTypes.string), - actionIds: PropTypes.arrayOf(PropTypes.string), - comments: PropTypes.object, - users: PropTypes.object, - actions: PropTypes.object, - onClickAction: PropTypes.func, + commentIds: PropTypes.arrayOf(PropTypes.string).isRequired, + actionIds: PropTypes.arrayOf(PropTypes.string).isRequired, + comments: PropTypes.object.isRequired, + users: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, + onClickAction: PropTypes.func.isRequired, // list of actions (flags, etc) associated with the comments modActions: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -74,11 +74,12 @@ export default class ModerationList extends React.Component { // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { - this.props.modActions.filter(action => modActions[action].key).forEach(action => { - key(modActions[action].key, 'moderationList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status)); + const {modActions, isActive} = this.props; + modActions.filter(action => modActions[action].key).forEach(action => { + key(modActions[action].key, 'moderationList', () => isActive && this.actionKeyHandler(modActions[action].status)); }); - key('j', 'moderationList', () => this.props.isActive && this.moveKeyHandler('down')); - key('k', 'moderationList', () => this.props.isActive && this.moveKeyHandler('up')); + key('j', 'moderationList', () => isActive && this.moveKeyHandler('down')); + key('k', 'moderationList', () => isActive && this.moveKeyHandler('up')); key.setScope('moderationList'); } @@ -138,6 +139,7 @@ export default class ModerationList extends React.Component { } mapModItems = (itemId, index) => { + const {comments, users, actions, modActions, suspectWords, hideActive} = this.props; const {active} = this.state; @@ -147,6 +149,7 @@ export default class ModerationList extends React.Component { let modItem; if (item.body) { + // If the item is a comment... const author = users[item.author_id]; modItem = ; } else { + // If the item is an action... modItem =

    Action

    ; } diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index f263c33f5..225e92884 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -32,4 +32,3 @@ export default connect( mapStateToProps, mapDispatchToProps )(LayoutContainer); - diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 058001884..480acbbb3 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -10,15 +10,15 @@ import translations from '../../translations.json'; const lang = new I18n(translations); -export default ({onTabClick, ...props}) => ( +export default (props) => (
    @@ -29,6 +29,8 @@ export default ({onTabClick, ...props}) => ( commentIds={props.premodIds} comments={props.comments.byId} users={props.users.byId} + actionIds={props.actions.ids} + actions={props.actions.byId} onClickAction={props.updateStatus} onClickShowBanDialog={props.showBanUserDialog} modActions={['reject', 'approve', 'ban']} @@ -48,6 +50,8 @@ export default ({onTabClick, ...props}) => ( commentIds={props.rejectedIds} comments={props.comments.byId} users={props.users.byId} + actionIds={props.actions.ids} + actions={props.actions.byId} onClickAction={props.updateStatus} modActions={['approve']} loading={props.comments.loading} @@ -61,6 +65,8 @@ export default ({onTabClick, ...props}) => ( commentIds={props.flaggedIds} comments={props.comments.byId} users={props.users.byId} + actionIds={props.actions.ids} + actions={props.actions.byId} onClickAction={props.updateStatus} modActions={['reject', 'approve']} loading={props.comments.loading}/> diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js index 709de5257..22b3cba34 100644 --- a/client/coral-admin/src/reducers/actions.js +++ b/client/coral-admin/src/reducers/actions.js @@ -2,7 +2,8 @@ import {Map, Set} from 'immutable'; import * as types from '../constants/actions'; const initialState = Map({ - ids: Set() + ids: Set(), + byId: Map() }); export default (state = initialState, action) => { @@ -19,6 +20,5 @@ const addActions = (state, action) => { memo[action.item_id] = action; return memo; }, {}); - const newIds = state.get('ids').concat(ids); - return state.merge(map).set('ids', newIds); + return state.set('byId', map).set('ids', ids); }; From c1e3580ef7020bf7d487b2be8d6735a5dedfaef7 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 4 Jan 2017 14:46:06 -0500 Subject: [PATCH 09/39] Resolving error in moderation list. --- client/coral-admin/src/components/ModerationList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 9f92c9b1d..99d48e135 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -75,8 +75,8 @@ export default class ModerationList extends React.Component { // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { const {modActions, isActive} = this.props; - modActions.filter(action => modActions[action].key).forEach(action => { - key(modActions[action].key, 'moderationList', () => isActive && this.actionKeyHandler(modActions[action].status)); + modActions.filter(action => actionsMap[action].key).forEach(action => { + key(actionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(actionsMap[action].status)); }); key('j', 'moderationList', () => isActive && this.moveKeyHandler('down')); key('k', 'moderationList', () => isActive && this.moveKeyHandler('up')); From 216b21251d1e4b6aeb942cf6888d98c2c9be082f Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 4 Jan 2017 15:33:41 -0500 Subject: [PATCH 10/39] Styling UserActions Component. --- client/coral-admin/src/components/Comment.js | 4 +- .../src/components/ModerationList.css | 7 +++ .../src/components/ModerationList.js | 17 ++++++- .../src/components/{User.js => UserAction.js} | 51 ++++++++----------- client/coral-admin/src/translations.json | 10 ++++ 5 files changed, 56 insertions(+), 33 deletions(-) rename client/coral-admin/src/components/{User.js => UserAction.js} (70%) diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 68db89565..dc350e815 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -14,7 +14,7 @@ import {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); // Render a single comment for the list -export default props => { +const Comment = props => { const {comment, author} = props; let authorStatus = author.status; const links = linkify.getMatches(comment.body); @@ -87,6 +87,8 @@ const getActionButton = (action, i, props) => { ); }; +export default Comment; + const linkStyles = { backgroundColor: 'rgb(255, 219, 135)', padding: '1px 2px' diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css index fddee7553..3f695095f 100644 --- a/client/coral-admin/src/components/ModerationList.css +++ b/client/coral-admin/src/components/ModerationList.css @@ -49,6 +49,7 @@ justify-content: space-between; .author { + min-width: 230px; display: flex; align-items: center; } @@ -97,6 +98,12 @@ padding-top: 15px; padding-left: 10px; } + + .flagCount{ + font-size: 12px; + color: #d32f2f; + } + } .empty { diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 99d48e135..dc576717c 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -2,7 +2,8 @@ import React, {PropTypes} from 'react'; import styles from './ModerationList.css'; import key from 'keymaster'; import Hammer from 'hammerjs'; -import Comment from 'components/Comment'; +import Comment from './Comment'; +import UserAction from './UserAction'; // Each action has different meaning and configuration const actionsMap = { @@ -167,7 +168,19 @@ export default class ModerationList extends React.Component { } else { // If the item is an action... - modItem =

    Action

    ; + const user = users[item.item_id]; + modItem = ; } return modItem; } diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/components/UserAction.js similarity index 70% rename from client/coral-admin/src/components/User.js rename to client/coral-admin/src/components/UserAction.js index 5fd3fafdc..8617562c9 100644 --- a/client/coral-admin/src/components/User.js +++ b/client/coral-admin/src/components/UserAction.js @@ -13,10 +13,10 @@ import {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); // Render a single comment for the list -export default props => { +const UserAction = props => { const {action, user} = props; let userStatus = user.status; - const links = linkify.getMatches(user.bio); + const links = user.settings.bio ? linkify.getMatches(user.settings.bio) : []; return (
  • @@ -37,39 +37,30 @@ export default props => { {lang.t('comment.banned_user')} : null}
  • -
    - - - - - -
    -
    - { - action.metadata.map(metadata => { - return
    - - { - metadata.field === 'bio' ? - lang.t('user.flagged_bio') - : lang.t('user.flagged_username') - }: - - - { - metadata.reason - } - -
    ; - }) - } + { + user.settings.bio && +
    +
    +
    {lang.t('user.user_bio')}:
    + + + + + +
    +
    + } +
    + {`${action.count} ${lang.t('user.bio_flags')}`}
    ); }; +export default UserAction; + // Get the button of the action performed over a comment if any const getActionButton = (action, i, props) => { const {user} = props; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 3397a521b..d184839e4 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -37,6 +37,11 @@ "ban_user": "Ban User", "banned_user": "Banned User" }, + "user": { + "user_bio": "User Bio", + "bio_flags": "flags for this bio", + "username_flags": "flags for this username" + }, "embedlink": { "copy": "Copy to Clipboard" }, @@ -126,6 +131,11 @@ "ban_user": "Suspender Usuario", "banned_user": "Usuario Suspendido" }, + "user": { + "user_bio": "", + "bio_flags": "", + "username_flags": "" + }, "configure": { "enable-pre-moderation": "Habilitar pre-moderación", "enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.", From 7a239c02abed223b3572d4c83742f8bd644bd730 Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 4 Jan 2017 15:56:52 -0500 Subject: [PATCH 11/39] Limiting to user actions. --- client/coral-admin/src/components/ModerationList.js | 12 ++++++------ .../ModerationQueue/ModerationContainer.js | 5 ++++- .../containers/ModerationQueue/ModerationQueue.js | 6 +----- client/coral-admin/src/reducers/actions.js | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index dc576717c..02f873c5b 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -18,11 +18,11 @@ export default class ModerationList extends React.Component { static propTypes = { isActive: PropTypes.bool, singleView: PropTypes.bool, - commentIds: PropTypes.arrayOf(PropTypes.string).isRequired, - actionIds: PropTypes.arrayOf(PropTypes.string).isRequired, - comments: PropTypes.object.isRequired, + commentIds: PropTypes.arrayOf(PropTypes.string), + actionIds: PropTypes.arrayOf(PropTypes.string), + comments: PropTypes.object, users: PropTypes.object.isRequired, - actions: PropTypes.object.isRequired, + actions: PropTypes.object, onClickAction: PropTypes.func.isRequired, // list of actions (flags, etc) associated with the comments @@ -141,7 +141,7 @@ export default class ModerationList extends React.Component { mapModItems = (itemId, index) => { - const {comments, users, actions, modActions, suspectWords, hideActive} = this.props; + const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props; const {active} = this.state; // Because ids are unique, the id will either appear as an action or as a comment. @@ -186,7 +186,7 @@ export default class ModerationList extends React.Component { } render () { - const {singleView, commentIds, actionIds, comments, actions, key} = this.props; + const {singleView, commentIds = [], actionIds = [], comments, actions, key} = this.props; // Combine moderations and actions into a single stream and sort by most recently updated. const moderationIds = [ ...commentIds, ...actionIds ].sort((a, b) => { diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index b58500d71..7e0c68a03 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -61,16 +61,19 @@ class ModerationContainer extends React.Component { } render () { - const {comments} = this.props; + const {comments, actions} = this.props; + const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod'); const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected'); const flaggedIds = comments.ids.filter(id => comments.byId[id].flagged === true); + const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'user'); return ( ( commentIds={props.premodIds} comments={props.comments.byId} users={props.users.byId} - actionIds={props.actions.ids} + actionIds={props.userActionIds} actions={props.actions.byId} onClickAction={props.updateStatus} onClickShowBanDialog={props.showBanUserDialog} @@ -50,8 +50,6 @@ export default (props) => ( commentIds={props.rejectedIds} comments={props.comments.byId} users={props.users.byId} - actionIds={props.actions.ids} - actions={props.actions.byId} onClickAction={props.updateStatus} modActions={['approve']} loading={props.comments.loading} @@ -65,8 +63,6 @@ export default (props) => ( commentIds={props.flaggedIds} comments={props.comments.byId} users={props.users.byId} - actionIds={props.actions.ids} - actions={props.actions.byId} onClickAction={props.updateStatus} modActions={['reject', 'approve']} loading={props.comments.loading}/> diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js index 22b3cba34..e6878b50f 100644 --- a/client/coral-admin/src/reducers/actions.js +++ b/client/coral-admin/src/reducers/actions.js @@ -1,4 +1,4 @@ -import {Map, Set} from 'immutable'; +import {Map, Set, fromJS} from 'immutable'; import * as types from '../constants/actions'; const initialState = Map({ @@ -20,5 +20,5 @@ const addActions = (state, action) => { memo[action.item_id] = action; return memo; }, {}); - return state.set('byId', map).set('ids', ids); + return state.set('byId', fromJS(map)).set('ids', new Set(ids)); }; From 9ba0176b3c0f644f2e65ca07dad9bed7538370aa Mon Sep 17 00:00:00 2001 From: David Jay Date: Wed, 4 Jan 2017 16:11:31 -0500 Subject: [PATCH 12/39] Seperating username and bio flags --- client/coral-admin/src/components/UserAction.js | 2 +- client/coral-admin/src/reducers/actions.js | 7 +++++-- client/coral-plugin-flags/FlagButton.js | 2 +- models/action.js | 3 --- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/components/UserAction.js b/client/coral-admin/src/components/UserAction.js index 8617562c9..7c072f3b1 100644 --- a/client/coral-admin/src/components/UserAction.js +++ b/client/coral-admin/src/components/UserAction.js @@ -53,7 +53,7 @@ const UserAction = props => {
    }
    - {`${action.count} ${lang.t('user.bio_flags')}`} + {`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
    ); diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js index e6878b50f..284e41c72 100644 --- a/client/coral-admin/src/reducers/actions.js +++ b/client/coral-admin/src/reducers/actions.js @@ -15,9 +15,12 @@ export default (state = initialState, action) => { }; const addActions = (state, action) => { - const ids = action.actions.map(action => action.item_id); + + // Make ids that are unique by item_id and by action type + const typeId = (action) => `${action.action_type}_${action.item_id}`; + const ids = action.actions.map(action => typeId(action)); const map = action.actions.reduce((memo, action) => { - memo[action.item_id] = action; + memo[typeId(action)] = action; return memo; }, {}); return state.set('byId', fromJS(map)).set('ids', new Set(ids)); diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 097ac9d5b..3adfc5d92 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -52,7 +52,7 @@ class FlagButton extends Component { break; } const action = { - action_type: 'flag', + action_type: `flag_${field}`, metadata: { field, reason, diff --git a/models/action.js b/models/action.js index 0778e2820..476e4de95 100644 --- a/models/action.js +++ b/models/action.js @@ -134,9 +134,6 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' item_type: { $last: '$item_type' }, - metadata: { - $push: '$metadata' - }, created_at: { $min: '$created_at' }, From 890ab810f3b6fd3ddd1de945ae395676ece14d41 Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 5 Jan 2017 14:50:21 -0500 Subject: [PATCH 13/39] Activating SuspendUserModal and updating language for clarity. --- client/coral-admin/src/actions/auth.js | 5 +- client/coral-admin/src/components/Comment.js | 17 ++-- .../src/components/ModerationList.js | 88 +++++++++++++------ .../src/components/SuspendUserModal.css | 0 .../src/components/SuspendUserModal.js | 43 +++++++++ .../coral-admin/src/components/UserAction.js | 19 ++-- .../ModerationQueue/ModerationQueue.js | 6 +- client/coral-framework/actions/auth.js | 5 +- models/action.js | 3 + 9 files changed, 137 insertions(+), 49 deletions(-) create mode 100644 client/coral-admin/src/components/SuspendUserModal.css create mode 100644 client/coral-admin/src/components/SuspendUserModal.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8cc094d5f..e681e1311 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -18,7 +18,10 @@ export const checkLogin = () => dispatch => { const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(result.user, isAdmin)); }) - .catch(error => dispatch(checkLoginFailure(error))); + .catch(error => { + console.error(error); + dispatch(checkLoginFailure(`${error.message}`)); + }); }; // Set CSRF Token diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index dc350e815..2304e47c5 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -54,16 +54,16 @@ const Comment = props => { }; // Get the button of the action performed over a comment if any -const getActionButton = (action, i, props) => { - const {comment, author} = props; +const getActionButton = (option, i, props) => { + const {comment, author, menuOptionsMap, action} = props; const status = comment.status; const flagged = comment.flagged; const banned = (author.status === 'banned'); - if (action === 'flag' && (status || flagged === true)) { + if (option === 'flag' && (status || flagged === true)) { return null; } - if (action === 'ban') { + if (option === 'ban') { return ( ); } + const menuOption = menuOptionsMap[option]; return ( props.onClickAction(props.actionsMap[action].status, comment)} + onClick={() => props.onClickAction(menuOption.status, comment, action)} /> ); }; diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 02f873c5b..669d821ff 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -4,9 +4,10 @@ import key from 'keymaster'; import Hammer from 'hammerjs'; import Comment from './Comment'; import UserAction from './UserAction'; +import SuspendUserModal from './SuspendUserModal'; // Each action has different meaning and configuration -const actionsMap = { +const menuOptionsMap = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, @@ -23,7 +24,7 @@ export default class ModerationList extends React.Component { comments: PropTypes.object, users: PropTypes.object.isRequired, actions: PropTypes.object, - onClickAction: PropTypes.func.isRequired, + updateCommentStatus: PropTypes.func.isRequired, // list of actions (flags, etc) associated with the comments modActions: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -32,13 +33,7 @@ export default class ModerationList extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired } - constructor (props) { - super(props); - - this.state = {active: null}; - this.onClickAction = this.onClickAction.bind(this); - this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this); - } + state = {active: null, suspendUserModal: null}; // remove key handlers before leaving componentWillUnmount () { @@ -76,8 +71,8 @@ export default class ModerationList extends React.Component { // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { const {modActions, isActive} = this.props; - modActions.filter(action => actionsMap[action].key).forEach(action => { - key(actionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(actionsMap[action].status)); + modActions.filter(action => menuOptionsMap[action].key).forEach(action => { + key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status)); }); key('j', 'moderationList', () => isActive && this.moveKeyHandler('down')); key('k', 'moderationList', () => isActive && this.moveKeyHandler('up')); @@ -121,21 +116,52 @@ export default class ModerationList extends React.Component { // If we are performing an action over a comment (aka removing from the list) we need to select a new active. // TODO: In the future this can be improved and look at the actual state to // resolve since the content of the list could change externally. For now it works as expected - onClickAction (action, id, author_id) { + onClickAction = (menuOption, id, action) => { // activate the next comment if (id === this.state.active) { - const {commentIds} = this.props; - if (commentIds[commentIds.length - 1] === this.state.active) { - this.setState({active: commentIds[commentIds.length - 2]}); + const moderationIds = this.getModerationIds(); + if (moderationIds[moderationIds.length - 1] === this.state.active) { + this.setState({active: moderationIds[moderationIds.length - 2]}); } else { - this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]}); + this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]}); + } + } + + // Update the status right away if this is a comment + if (action.item_type === 'comment') { + this.props.updateCommentStatus(menuOption, id); + } else if (action.item_type === 'user') { + + // If a user bio or name is rejected, bring up a dialog before suspending them. + if (menuOption === 'rejected') { + this.setState({suspendUserModal: {stage: 0, actionType: action.action_type}}); + } else { + this.props.updateUserStatus(menuOption, action.item_id); } } - this.props.onClickAction(action, id, author_id); } - onClickShowBanDialog(userId, userName, commentId) { + /* + * When an admin clicks to suspend a user a dialog is shown, this function + * handles the possible actions for that dialog. + */ + onClickSuspendModalButton = (stage, menuOption) => () => { + const {updateUserStatus, action} = this.props; + const cancel = () => this.setState({suspendUserModal: null}); + const next = () => this.setState((prev) => { + prev.suspendUserModal.stage++; + return prev; + }); + const suspend = () => updateUserStatus('suspend', action.item_id); + const suspendModalActions = [ + [ cancel, next ], + [ cancel, suspend ] + ]; + return suspendModalActions[stage][menuOption](); + } + + onClickShowBanDialog = (userId, userName, commentId) => { this.props.onClickShowBanDialog(userId, userName, commentId); } @@ -162,7 +188,7 @@ export default class ModerationList extends React.Component { onClickAction={this.onClickAction} onClickShowBanDialog={this.onClickShowBanDialog} modActions={modActions} - actionsMap={actionsMap} + menuOptionsMap={menuOptionsMap} isActive={itemId === active} hideActive={hideActive} />; } else { @@ -178,29 +204,37 @@ export default class ModerationList extends React.Component { onClickAction={this.onClickAction} onClickShowBanDialog={this.onClickShowBanDialog} modActions={modActions} - actionsMap={actionsMap} + menuOptionsMap={menuOptionsMap} isActive={itemId === active} hideActive={hideActive} />; } return modItem; } - render () { - const {singleView, commentIds = [], actionIds = [], comments, actions, key} = this.props; - - // Combine moderations and actions into a single stream and sort by most recently updated. - const moderationIds = [ ...commentIds, ...actionIds ].sort((a, b) => { + getModerationIds = () => { + const {commentIds = [], actionIds = [], comments, actions} = this.props; + return [ ...commentIds, ...actionIds ].sort((a, b) => { const itemA = comments[a] || actions[a]; const itemB = comments[b] || actions[b]; return itemB.updated_at - itemA.updated_at; }); + } + + render () { + const {singleView, key} = this.props; + + // Combine moderations and actions into a single stream and sort by most recently updated. + const moderationIds = this.getModerationIds(); return (
      + id='moderationList'> {moderationIds.map(this.mapModItems)} + this.setState({suspendUserModal:null})} + onButtonClick={this.onClickSuspendModalButton} />
    ); } diff --git a/client/coral-admin/src/components/SuspendUserModal.css b/client/coral-admin/src/components/SuspendUserModal.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-admin/src/components/SuspendUserModal.js b/client/coral-admin/src/components/SuspendUserModal.js new file mode 100644 index 000000000..2204d797f --- /dev/null +++ b/client/coral-admin/src/components/SuspendUserModal.js @@ -0,0 +1,43 @@ +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations.json'; +import React from 'react'; +import Modal from 'components/Modal'; +import styles from './SuspendUserModal.css'; +import {Button} from 'coral-ui'; + +const stages = [ + { + title: 'suspenduser.title_0', + description: 'suspenduser.description_0', + options: { + 'j': 'suspenduser.no_cancel', + 'k': 'suspenduser.yes_suspend' + } + }, + { + title: 'suspenduser.title_1', + description: 'suspenduser.description_1', + options: { + 'j': 'suspenduser.cancel', + 'k': 'suspenduser.send' + } + } +]; + +export default ({stage, actionType, onClose, onButtonClick}) => + actionType ? +

    {lang.t(stages[stage].title)}

    +
    +
    + {lang.t(stages[stage].description)} +
    + {Object.keys(stages[stage].options).map((key, i) => ( +
    +
    + : null; + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/components/UserAction.js b/client/coral-admin/src/components/UserAction.js index 7c072f3b1..356628e7a 100644 --- a/client/coral-admin/src/components/UserAction.js +++ b/client/coral-admin/src/components/UserAction.js @@ -62,34 +62,35 @@ const UserAction = props => { export default UserAction; // Get the button of the action performed over a comment if any -const getActionButton = (action, i, props) => { - const {user} = props; +const getActionButton = (option, i, props) => { + const {user, onClickShowBanDialog, onClickAction, menuOptionsMap, action} = props; const status = user.status; const banned = (user.status === 'banned'); - if (action === 'flag' && status) { + if (option === 'flag' && status) { return null; } - if (action === 'ban') { + if (option === 'ban') { return ( ); } + const menuOption = menuOptionsMap[option]; return ( props.onClickAction(props.actionsMap[action].status, user.id)} + onClick={() => onClickAction(menuOption.status, user.id, action)} /> ); }; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 3866bc641..0aeb6f79b 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -31,7 +31,7 @@ export default (props) => ( users={props.users.byId} actionIds={props.userActionIds} actions={props.actions.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} onClickShowBanDialog={props.showBanUserDialog} modActions={['reject', 'approve', 'ban']} loading={props.comments.loading}/> @@ -50,7 +50,7 @@ export default (props) => ( commentIds={props.rejectedIds} comments={props.comments.byId} users={props.users.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} modActions={['approve']} loading={props.comments.loading} /> @@ -63,7 +63,7 @@ export default (props) => ( commentIds={props.flaggedIds} comments={props.comments.byId} users={props.users.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} modActions={['reject', 'approve']} loading={props.comments.loading}/>
    diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 581fb729c..bb442427f 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -141,5 +141,8 @@ export const checkLogin = () => dispatch => { const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(result.user, isAdmin)); }) - .catch(error => dispatch(checkLoginFailure(error))); + .catch(error => { + console.error(error); + dispatch(checkLoginFailure(`${error.message}`)); + }); }; diff --git a/models/action.js b/models/action.js index 476e4de95..0778e2820 100644 --- a/models/action.js +++ b/models/action.js @@ -134,6 +134,9 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' item_type: { $last: '$item_type' }, + metadata: { + $push: '$metadata' + }, created_at: { $min: '$created_at' }, From a468fe4c2814da3399d138f184cb05ff2630d76d Mon Sep 17 00:00:00 2001 From: David Jay Date: Thu, 5 Jan 2017 16:51:37 -0500 Subject: [PATCH 14/39] Adding language and e-mail input to suspend user modal. --- .../src/components/ModerationList.js | 2 +- .../src/components/SuspendUserModal.js | 63 ++++++++++++++----- client/coral-admin/src/translations.json | 12 ++++ 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 669d821ff..977407849 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -33,7 +33,7 @@ export default class ModerationList extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired } - state = {active: null, suspendUserModal: null}; + state = {active: null, suspendUserModal: null, email: null}; // remove key handlers before leaving componentWillUnmount () { diff --git a/client/coral-admin/src/components/SuspendUserModal.js b/client/coral-admin/src/components/SuspendUserModal.js index 2204d797f..c3725271c 100644 --- a/client/coral-admin/src/components/SuspendUserModal.js +++ b/client/coral-admin/src/components/SuspendUserModal.js @@ -1,6 +1,6 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations.json'; -import React from 'react'; +import React, {Component, PropTypes} from 'react'; import Modal from 'components/Modal'; import styles from './SuspendUserModal.css'; import {Button} from 'coral-ui'; @@ -18,26 +18,57 @@ const stages = [ title: 'suspenduser.title_1', description: 'suspenduser.description_1', options: { - 'j': 'suspenduser.cancel', + 'j': 'bandialog.cancel', 'k': 'suspenduser.send' } } ]; -export default ({stage, actionType, onClose, onButtonClick}) => - actionType ? -

    {lang.t(stages[stage].title)}

    -
    -
    - {lang.t(stages[stage].description)} +class SuspendUserModal extends Component { + + state = {email: ''} + + static propTypes = { + stage: PropTypes.number, + actionType: PropTypes.string, + onClose: PropTypes.func.isRequired, + onButtonClick: PropTypes.func.isRequired + } + + componentDidMount() { + const about = this.props.actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username'); + this.setState({email: lang.t('suspenduser.email', about)}); + } + + onEmailChange = (e) => this.setState({email: e.target.value}) + + render () { + const {stage, actionType, onClose, onButtonClick} = this.props; + const about = actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username'); + return actionType ? +

    {lang.t(stages[stage].title, about)}

    +
    +
    + {lang.t(stages[stage].description, about)} +
    + { + stage === 1 && +