+const ModerationMenu = ({asset, premodCount, rejectedCount, flaggedCount, selectSort, sort}) => {
+ const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
+ const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
+ const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
+ return (
+
+
+
+
+
+ {lang.t('modqueue.premod')}
+
+
+ {lang.t('modqueue.rejected')}
+
+
+ {lang.t('modqueue.flagged')}
+
+
selectSort(sort)}>
+
+
+
- );
- }
-}
+
+ );
+};
+
+ModerationMenu.propTypes = {
+ premodCount: PropTypes.number.isRequired,
+ rejectedCount: PropTypes.number.isRequired,
+ flaggedCount: PropTypes.number.isRequired,
+ asset: PropTypes.shape({
+ id: PropTypes.string
+ })
+};
export default ModerationMenu;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
index 59f47faad..9576045de 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/styles.css
+++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
@@ -84,11 +84,10 @@ span {
margin-bottom: -1px;
.settingsButton {
- i {
- vertical-align: middle;
- margin-left: 10px;
- margin-top: -4px;
- }
+ vertical-align: middle;
+ margin-left: 10px;
+ margin-top: -4px;
+ font-size: 16px;
}
.moderateAsset {
@@ -114,7 +113,15 @@ span {
}
&:nth-child(2) {
- text-align: center;
+ span {
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ max-width: 344px;
+ display: inline-block;
+ vertical-align: top;
+ }
}
&:last-child {
@@ -387,3 +394,23 @@ span {
cursor: pointer;
}
}
+
+.loadMoreContainer {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+};
+
+.loadMore {
+ width: 100%;
+ text-align: center;
+ color: #FFF;
+ max-width: 660px;
+ margin-bottom: 30px;
+ background-color: #2376D8;
+ cursor: pointer;
+}
+
+.loadMore:hover {
+ background-color: #4399FF;
+}
diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js
index fe3a1faf9..4a574cc93 100644
--- a/client/coral-admin/src/graphql/mutations/index.js
+++ b/client/coral-admin/src/graphql/mutations/index.js
@@ -1,6 +1,7 @@
import {graphql} from 'react-apollo';
import SET_USER_STATUS from './setUserStatus.graphql';
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
+import SUSPEND_USER from './suspendUser.graphql';
export const banUser = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
@@ -9,11 +10,39 @@ export const banUser = graphql(SET_USER_STATUS, {
variables: {
userId,
status: 'BANNED'
- }
+ },
+ refetchQueries: ['Users']
});
}}),
});
+export const setUserStatus = graphql(SET_USER_STATUS, {
+ props: ({mutate}) => ({
+ approveUser: ({userId}) => {
+ return mutate({
+ variables: {
+ userId,
+ status: 'APPROVED'
+ },
+ refetchQueries: ['Users']
+ });
+ }
+ })
+});
+
+export const suspendUser = graphql(SUSPEND_USER, {
+ props: ({mutate}) => ({
+ suspendUser: ({userId}) => {
+ return mutate({
+ variables: {
+ userId
+ },
+ refetchQueries: ['Users']
+ });
+ }
+ })
+});
+
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
props: ({mutate}) => ({
acceptComment: ({commentId}) => {
@@ -22,7 +51,26 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'ACCEPTED'
},
- refetchQueries: ['ModQueue']
+ updateQueries: {
+ ModQueue: (oldData) => {
+ const premod = oldData.premod.filter(c => c.id !== commentId);
+ const flagged = oldData.flagged.filter(c => c.id !== commentId);
+ const rejected = oldData.rejected.filter(c => c.id !== commentId);
+ const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
+ const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
+ const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
+
+ return {
+ ...oldData,
+ premodCount,
+ flaggedCount,
+ rejectedCount,
+ premod,
+ flagged,
+ rejected,
+ };
+ }
+ }
});
},
rejectComment: ({commentId}) => {
@@ -31,7 +79,27 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'REJECTED'
},
- refetchQueries: ['ModQueue']
+ updateQueries: {
+ ModQueue: (oldData) => {
+ const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
+ const rejected = [comment].concat(oldData.rejected);
+ const premod = oldData.premod.filter(c => c.id !== commentId);
+ const flagged = oldData.flagged.filter(c => c.id !== commentId);
+ const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
+ const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
+ const rejectedCount = oldData.rejectedCount + 1;
+
+ return {
+ ...oldData,
+ premodCount,
+ flaggedCount,
+ rejectedCount,
+ premod,
+ flagged,
+ rejected
+ };
+ }
+ }
});
}
})
diff --git a/client/coral-admin/src/graphql/mutations/suspendUser.graphql b/client/coral-admin/src/graphql/mutations/suspendUser.graphql
new file mode 100644
index 000000000..f34d93370
--- /dev/null
+++ b/client/coral-admin/src/graphql/mutations/suspendUser.graphql
@@ -0,0 +1,7 @@
+mutation suspendUser($userId: ID!) {
+ suspendUser(id: $userId) {
+ errors {
+ translation_key
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index c4a30ad44..291ea6bc5 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -1,6 +1,8 @@
import {graphql} from 'react-apollo';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
+import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
+import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
@@ -14,7 +16,8 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
},
props: ({ownProps: {params: {id = null}}, data}) => ({
data,
- modQueueResort: modQueueResort(id, data.fetchMore)
+ modQueueResort: modQueueResort(id, data.fetchMore),
+ loadMore: loadMore(data.fetchMore)
})
});
@@ -30,6 +33,42 @@ export const getMetrics = graphql(METRICS, {
}
});
+export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
+ let statuses;
+ switch(tab) {
+ case 'premod':
+ statuses = ['PREMOD'];
+ break;
+ case 'flagged':
+ statuses = ['NONE', 'PREMOD'];
+ break;
+ case 'rejected':
+ statuses = ['REJECTED'];
+ break;
+ }
+ return fetchMore({
+ query: MOD_QUEUE_LOAD_MORE,
+ variables: {
+ limit,
+ cursor,
+ sort,
+ statuses,
+ asset_id
+ },
+ updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => {
+ return {
+ ...oldData,
+ [tab]: [
+ ...oldData[tab],
+ ...comments
+ ]
+ };
+ }
+ });
+};
+
+export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
+
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
diff --git a/client/coral-admin/src/graphql/queries/loadMore.graphql b/client/coral-admin/src/graphql/queries/loadMore.graphql
new file mode 100644
index 000000000..56966a804
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/loadMore.graphql
@@ -0,0 +1,13 @@
+#import "../fragments/commentView.graphql"
+
+query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
+ comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
+ ...commentView
+ action_summaries {
+ count
+ ... on FlagActionSummary {
+ reason
+ }
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
new file mode 100644
index 000000000..1e6a6e53b
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
@@ -0,0 +1,22 @@
+query Users ($n: ACTION_TYPE) {
+ users (query:{action_type: $n}){
+ id
+ username
+ status
+ roles
+ actions{
+ id
+ created_at
+ ... on FlagAction {
+ reason
+ }
+ user {
+ username
+ }
+ }
+ action_summaries {
+ count
+ reason
+ }
+ }
+}
diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js
index 367e67b2a..d051ae0ff 100644
--- a/client/coral-admin/src/reducers/community.js
+++ b/client/coral-admin/src/reducers/community.js
@@ -6,58 +6,88 @@ import {
FETCH_COMMENTERS_SUCCESS,
SORT_UPDATE,
SET_ROLE,
- SET_COMMENTER_STATUS
+ SET_COMMENTER_STATUS,
+ SHOW_BANUSER_DIALOG,
+ HIDE_BANUSER_DIALOG,
+ SHOW_SUSPENDUSER_DIALOG,
+ HIDE_SUSPENDUSER_DIALOG
} from '../constants/community';
const initialState = Map({
community: Map(),
- isFetching: false,
- error: '',
- commenters: [],
- field: 'created_at',
- asc: false,
- totalPages: 0,
- page: 0
+ isFetchingPeople: false,
+ errorPeople: '',
+ accounts: [],
+ fieldPeople: 'created_at',
+ ascPeople: false,
+ totalPagesPeople: 0,
+ pagePeople: 0,
+ user: Map({}),
+ banDialog: false,
+ suspendDialog: false
});
export default function community (state = initialState, action) {
switch (action.type) {
case FETCH_COMMENTERS_REQUEST :
return state
- .set('isFetching', true);
+ .set('isFetchingPeople', true);
case FETCH_COMMENTERS_FAILURE :
return state
- .set('isFetching', false)
- .set('error', action.error);
+ .set('isFetchingPeople', false)
+ .set('errorPeople', action.error);
+
case FETCH_COMMENTERS_SUCCESS : {
- const {commenters, type, ...rest} = action; // eslint-disable-line
+ const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return state
.merge({
- isFetching: false,
- error: '',
+ isFetchingPeople: false,
+ errorPeople: '',
+ pagePeople: page,
+ countPeople: count,
+ limitPeople: limit,
+ totalPagesPeople: totalPages,
...rest
})
- .set('commenters', commenters); // Sets to normal array
+ .set('accounts', accounts); // Sets to normal array
}
case SET_ROLE : {
- const commenters = state.get('commenters');
+ const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].roles[0] = action.role;
- return state.set('commenters', commenters.map(id => id));
+ return state.set('accounts', commenters.map(id => id));
}
case SET_COMMENTER_STATUS: {
- const commenters = state.get('commenters');
+ const commenters = state.get('accounts');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].status = action.status;
- return state.set('commenters', commenters.map(id => id));
+ return state.set('accounts', commenters.map(id => id));
}
case SORT_UPDATE :
return state
- .set('field', action.sort.field)
- .set('asc', !state.get('asc'));
+ .set('fieldPeople', action.sort.field)
+ .set('ascPeople', !state.get('ascPeople'));
+ case HIDE_BANUSER_DIALOG:
+ return state
+ .set('banDialog', false);
+ case SHOW_BANUSER_DIALOG:
+ return state
+ .merge({
+ user: Map(action.user),
+ banDialog: true
+ });
+ case HIDE_SUSPENDUSER_DIALOG:
+ return state
+ .set('suspendDialog', false);
+ case SHOW_SUSPENDUSER_DIALOG:
+ return state
+ .merge({
+ user: Map(action.user),
+ suspendDialog: true
+ });
default :
return state;
}
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index ac9131685..321ace66b 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -16,7 +16,20 @@
"active": "Active",
"banned": "Banned",
"banned-user": "Banned User",
- "loading": "Loading results"
+ "loading": "Loading results",
+ "flaggedaccounts": "Flagged Usernames",
+ "people": "People",
+ "no-flagged-accounts": "The Account Flags queue is currently empty.",
+ "I don't like this username": "I don't like this username",
+ "This user is impersonating": "Impersonation",
+ "This looks like an ad/marketing": "Spam/Ads",
+ "This username is offensive": "Offensive",
+ "Other": "Other",
+ "ban_user": "Ban User?",
+ "are_you_sure": "Are you sure you would like to ban {0}?",
+ "note": "Note: Banning this user will not let them edit, comment or remove anything.",
+ "cancel": "Cancel",
+ "yes_ban_user": "Yes, Ban User"
},
"modqueue": {
"likes": "likes",
@@ -103,7 +116,8 @@
"yes_ban_user": "Yes, Ban User"
},
"suspenduser": {
- "title_0": "We noticed you rejected a {0}",
+ "title": "Suspend a user",
+ "title_0": "We noticed you rejected a username",
"description_0": "Would you like to temporarily ban this user becuase of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
"title_1": "Notify the user of their temporary suspension",
"description_1": "Suspending this user will temporarily disable their account and hide all of their comments on the site.",
@@ -113,7 +127,7 @@
"bio": "bio",
"username": "username",
"email_subject": "Your account has been suspended",
- "email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
+ "email": "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
"write_message": "Write a message"
},
"dashboard": {
@@ -157,7 +171,34 @@
"active": "Activa",
"banned": "Suspendido",
"banned-user": "Usuario Suspendido",
- "loading": "Cargando resultados"
+ "loading": "Cargando resultados",
+ "flaggedaccounts": "Nombres de Usuario Reportados",
+ "people": "Gente",
+ "no-flagged-accounts": "No hay ninguna cuenta reportada.",
+ "I don't like this username": "No me gusta ese nombre de usuario",
+ "This user is impersonating": "Suplantación",
+ "This looks like an ad/marketing": "Spam/Propaganda",
+ "This username is offensive": "Ofensivo",
+ "Other": "Otros",
+ "ban_user": "Quieres suspender el Usuario?",
+ "are_you_sure": "Estas segura que quieres suspender a {0}?",
+ "note": "Nota: Suspender a este usuario no le va a permitir borrar ni editar ni comentar.",
+ "cancel": "Cancelar",
+ "yes_ban_user": "Si, Suspendan el usuario"
+ },
+ "suspenduser": {
+ "title": "Suspendiendo un usuario",
+ "title_0": "Esta queriendo suspender un usuario?",
+ "description_0": "Le gustaria suspender a esta usuaria temporarianmente por su nombre de usuario? Si lo hace sus comentarios serán escondidos temporariamente hasta que puedan reescribir su nombre de usuario.",
+ "title_1": "Enviarle una nota al usuario sobre su cuenta suspendida",
+ "description_1": "Si suspende a este usuario, su cuenta va a ser deshabilitada y todos sus comentarios escondidos del sitio.",
+ "no_cancel": "No, cancelar",
+ "yes_suspend": "Si, suspender",
+ "send": "Enviar",
+ "username": "nombre de usuario",
+ "email_subject": "Su cuenta ha sido suspendida temporariamente",
+ "email": "Otra persona de la comunidad recientemente marcó su nombre de usuario para ser revisado. Por su contenido, el nombre de usuario ha sido rechazado. Esto quiere decir que no puede comentar, gustar o marcar contenido hasta que modifique su nombre de usuario. Por favor, envienos un correo a moderator@newsorg.com si tiene alguna pregunta o preocupación",
+ "write_message": "Escribir un mensaje"
},
"modqueue": {
"likes": "gustos",
diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css
index 7bc1a21f4..6966d82ab 100644
--- a/client/coral-embed-stream/src/Comment.css
+++ b/client/coral-embed-stream/src/Comment.css
@@ -3,5 +3,10 @@
}
.Comment {
-
+
+}
+
+.pendingComment {
+ filter: blur(2px);
+ pointer-events: none;
}
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index 72449c906..0a3cdb78b 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -116,6 +116,7 @@ class Comment extends React.Component {
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
+ commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) => async () => {
diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js
index 7dd8bf12b..04dba8402 100644
--- a/client/coral-framework/graphql/mutations/index.js
+++ b/client/coral-framework/graphql/mutations/index.js
@@ -35,7 +35,7 @@ export const postComment = graphql(POST_COMMENT, {
action_summaries: [],
tags: [],
status: null,
- id: `${Date.now()}_temp_id`
+ id: 'pending'
}
}
},
diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js
index 537d899cd..45110f407 100644
--- a/client/coral-plugin-flags/FlagButton.js
+++ b/client/coral-plugin-flags/FlagButton.js
@@ -32,18 +32,30 @@ class FlagButton extends Component {
if (flagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current_user.id);
+ } else if (this.state.showMenu){
+ this.closeMenu();
} else {
- this.setState({showMenu: !this.state.showMenu});
+ this.setState({showMenu: true});
}
}
+ closeMenu = () => {
+ this.setState({
+ showMenu: false,
+ itemType: '',
+ reason: '',
+ message: '',
+ step: 0
+ });
+ }
+
onPopupContinue = () => {
const {postFlag, postDontAgree, id, author_id} = this.props;
const {itemType, reason, step, posted, message} = this.state;
// Proceed to the next step or close the menu if we've reached the end
if (step + 1 >= this.props.getPopupMenu.length) {
- this.setState({showMenu: false});
+ this.closeMenu();
} else {
this.setState({step: step + 1});
}
@@ -114,7 +126,7 @@ class FlagButton extends Component {
}
handleClickOutside () {
- this.setState({showMenu: false});
+ this.closeMenu();
}
render () {
diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/FlagComment.js
index 24fde6b0f..3f3051f28 100644
--- a/client/coral-plugin-flags/FlagComment.js
+++ b/client/coral-plugin-flags/FlagComment.js
@@ -23,14 +23,14 @@ const getPopupMenu = [
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
- {val: 'other', text: lang.t('other')}
+ {val: 'Other', text: lang.t('other')}
]
: [
{val: 'This username is offensive', text: lang.t('username-offensive')},
{val: 'I don\'t like this username', text: lang.t('no-like-username')},
{val: 'This user is impersonating', text: lang.t('user-impersonating')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
- {val: 'other', text: lang.t('other')}
+ {val: 'Other', text: lang.t('other')}
];
return {
header: lang.t('step-2-header'),
diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js
index 65bf52f72..11432c753 100644
--- a/client/coral-ui/components/Icon.js
+++ b/client/coral-ui/components/Icon.js
@@ -1,7 +1,7 @@
import React from 'react';
import {Icon as IconMDL} from 'react-mdl';
-const Icon = ({className, name}) => (
+const Icon = ({className = '', name}) => (
);
diff --git a/graph/loaders/users.js b/graph/loaders/users.js
index 90c661f71..145b0dfdb 100644
--- a/graph/loaders/users.js
+++ b/graph/loaders/users.js
@@ -3,11 +3,51 @@ const DataLoader = require('dataloader');
const util = require('./util');
const UsersService = require('../../services/users');
+const UserModel = require('../../models/user');
const genUserByIDs = (context, ids) => UsersService
.findByIdArray(ids)
.then(util.singleJoinBy(ids, 'id'));
+/**
+ * Retrieves users based on the passed in query that is filtered by the
+ * current used passed in via the context.
+ * @param {Object} context graph context
+ * @param {Object} query query terms to apply to the users query
+ */
+const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
+
+ let users = UserModel.find();
+
+ if (ids) {
+ users = users.find({
+ id: {
+ $in: ids
+ }
+ });
+ }
+
+ if (cursor) {
+ if (sort === 'REVERSE_CHRONOLOGICAL') {
+ users = users.where({
+ created_at: {
+ $lt: cursor
+ }
+ });
+ } else {
+ users = users.where({
+ created_at: {
+ $gt: cursor
+ }
+ });
+ }
+ }
+
+ return users
+ .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
+ .limit(limit);
+};
+
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
@@ -15,6 +55,7 @@ const genUserByIDs = (context, ids) => UsersService
*/
module.exports = (context) => ({
Users: {
+ getByQuery: (query) => getUsersByQuery(context, query),
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
}
});
diff --git a/graph/mutators/user.js b/graph/mutators/user.js
index 3f87c1fb5..6f94ae9b1 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -6,18 +6,26 @@ const setUserStatus = ({user}, {id, status}) => {
.then(res => res);
};
-module.exports = (context) => {
- if (context.user && context.user.can('mutation:setUserStatus')) {
- return {
- User: {
- setUserStatus: (action) => setUserStatus(context, action)
- }
- };
- }
+const suspendUser = ({user}, {id}) => {
+ return UsersService.suspendUser(id)
+ .then(res => res);
+};
- return {
+module.exports = (context) => {
+ let mutators = {
User: {
- setUserStatus: () => Promise.reject(errors.ErrNotAuthorized)
+ setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
+ suspendUser: () => Promise.reject(errors.ErrNotAuthorized)
}
};
+
+ if (context.user && context.user.can('mutation:setUserStatus')) {
+ mutators.User.setUserStatus = (action) => setUserStatus(context, action);
+ }
+
+ if (context.user && context.user.can('mutation:suspendUser')) {
+ mutators.User.suspendUser = (action) => suspendUser(context, action);
+ }
+
+ return mutators;
};
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index dc540b202..f819cc586 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -47,6 +47,9 @@ const RootMutation = {
setUserStatus(_, {id, status}, {mutators: {User}}) {
return wrapResponse(null)(User.setUserStatus({id, status}));
},
+ suspendUser(_, {id}, {mutators: {User}}) {
+ return wrapResponse(null)(User.suspendUser({id}));
+ },
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
return wrapResponse(null)(Comment.setCommentStatus({id, status}));
},
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index bdcdcef78..d577ddddb 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -82,6 +82,28 @@ const RootQuery = {
}
return user;
+ },
+
+ // This endpoint is used for loading the user moderation queues (users whose username has been flagged),
+ // so hide it in the event that we aren't an admin.
+ users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) {
+
+ if (user == null || !user.hasRoles('ADMIN')) {
+ return null;
+ }
+
+ const query = {limit, cursor, sort};
+
+ if (action_type) {
+ return Actions.getByTypes({action_type, item_type: 'USERS'})
+ .then((ids) => {
+
+ // Perform the query using the available resolver.
+ return Users.getByQuery({ids, limit, cursor, sort});
+ });
+ }
+
+ return Users.getByQuery(query);
}
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 6cb4cb046..29000c84d 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -31,7 +31,7 @@ type User {
username: String!
# Action summaries against the user.
- action_summaries: [ActionSummary]
+ action_summaries: [FlagActionSummary]
# Actions completed on the parent.
actions: [Action]
@@ -45,6 +45,9 @@ type User {
# returns all comments based on a query.
comments(query: CommentsQuery): [Comment]
+ # returns all users based on a query.
+ users(query: UsersQuery): [User]
+
# returns user status
status: USER_STATUS
}
@@ -60,6 +63,20 @@ type Tag {
created_at: Date!
}
+# UsersQuery allows the ability to query users by a specific fields.
+input UsersQuery {
+ action_type: ACTION_TYPE
+
+ # Limit the number of results to be returned.
+ limit: Int = 10
+
+ # Skip results from the last created_at timestamp.
+ cursor: Date
+
+ # Sort the results by created_at.
+ sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
+}
+
################################################################################
## Comments
################################################################################
@@ -501,6 +518,9 @@ type RootQuery {
# role.
me: User
+ # Users returned based on a query.
+ users(query: UsersQuery): [User]
+
# Asset metrics related to user actions are saturated into the assets
# returned.
assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
@@ -634,6 +654,14 @@ type SetUserStatusResponse implements Response {
errors: [UserError]
}
+# SuspendUserResponse is the response returned with possibly some errors
+# relating to the suspend action attempt.
+type SuspendUserResponse implements Response {
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError]
+}
+
# SetCommentStatusResponse is the response returned with possibly some errors
# relating to the delete action attempt.
type SetCommentStatusResponse implements Response {
@@ -646,14 +674,14 @@ type SetCommentStatusResponse implements Response {
type AddCommentTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
- errors: [UserError]
+ errors: [UserError]
}
# Response to removeCommentTag mutation
type RemoveCommentTagResponse implements Response {
# An array of errors relating to the mutation that occured.
comment: Comment
- errors: [UserError]
+ errors: [UserError]
}
# All mutations for the application are defined on this object.
@@ -677,6 +705,9 @@ type RootMutation {
# Sets User status. Requires the `ADMIN` role.
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
+ # Sets User status to BANNED and canEditName to true. Requires the `ADMIN` role.
+ suspendUser(id: ID!): SuspendUserResponse
+
# Sets Comment status. Requires the `ADMIN` role.
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
diff --git a/models/user.js b/models/user.js
index 2b65bd300..ea4195b8a 100644
--- a/models/user.js
+++ b/models/user.js
@@ -156,9 +156,10 @@ const USER_GRAPH_OPERATIONS = [
'mutation:deleteAction',
'mutation:editName',
'mutation:setUserStatus',
+ 'mutation:suspendUser',
'mutation:setCommentStatus',
'mutation:addCommentTag',
- 'mutation:removeCommentTag',
+ 'mutation:removeCommentTag'
];
/**
@@ -174,7 +175,7 @@ UserSchema.method('can', function(...actions) {
return false;
}
- if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
+ if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
return false;
}
diff --git a/services/users.js b/services/users.js
index b58776a75..90ca2fae2 100644
--- a/services/users.js
+++ b/services/users.js
@@ -378,6 +378,22 @@ module.exports = class UsersService {
});
}
+ /**
+ * Suspend a user. It changes the status to BANNED and canEditName to True.
+ * @param {String} id id of a user
+ * @param {Function} done callback after the operation is complete
+ */
+ static suspendUser(id) {
+ return UserModel.update({
+ id
+ }, {
+ $set: {
+ status: 'BANNED',
+ canEditName: true
+ }
+ });
+ }
+
/**
* Finds a user with the id.
* @param {String} id user id (uuid)
diff --git a/test/graph/mutations/addCommentTag.js b/test/graph/mutations/addCommentTag.js
index 14a746705..0835e81bc 100644
--- a/test/graph/mutations/addCommentTag.js
+++ b/test/graph/mutations/addCommentTag.js
@@ -54,7 +54,7 @@ describe('graph.mutations.addCommentTag', () => {
}
expect(response.errors).to.be.empty;
expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
- expect(response.data.addCommentTag.comment).to.be.null;
+ expect(response.data.addCommentTag.comment).to.be.null;
});
});
});