From da3c812dc132f7161b4bb246c2fd6fc52bf264b6 Mon Sep 17 00:00:00 2001 From: gaba Date: Tue, 28 Feb 2017 21:33:01 -0800 Subject: [PATCH] Adds graphql for users flagged in the server and client. --- client/coral-admin/src/actions/community.js | 33 +--- .../Community/CommunityContainer.js | 17 +- .../containers/Community/FlaggedAccounts.js | 33 +++- .../Community/UserModerationList.css | 187 ++++++++++++++++++ .../Community}/components/User.js | 53 +++-- .../coral-admin/src/graphql/queries/index.js | 3 + .../queries/modUserFlaggedQuery.graphql | 19 ++ client/coral-admin/src/translations.json | 16 +- graph/loaders/users.js | 48 +++++ graph/resolvers/root_query.js | 16 ++ graph/typeDefs.graphql | 20 +- 11 files changed, 378 insertions(+), 67 deletions(-) create mode 100644 client/coral-admin/src/containers/Community/UserModerationList.css rename client/coral-admin/src/{ => containers/Community}/components/User.js (51%) create mode 100644 client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 030ae1c0c..92d10fac9 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -7,10 +7,7 @@ import { SORT_UPDATE, COMMENTERS_NEW_PAGE, SET_ROLE, - SET_COMMENTER_STATUS, - FETCH_FLAGGED_COMMENTERS_REQUEST, - FETCH_FLAGGED_COMMENTERS_SUCCESS, - FETCH_FLAGGED_COMMENTERS_FAILURE + SET_COMMENTER_STATUS } from '../constants/community'; import coralApi from '../../../coral-framework/helpers/response'; @@ -18,7 +15,7 @@ import coralApi from '../../../coral-framework/helpers/response'; export const fetchAccounts = (query = {}) => dispatch => { dispatch(requestFetchAccounts()); coralApi(`/users?${qs.stringify(query)}`) - .then(({result, page, count, limit, totalPages}) => + .then(({result, page, count, limit, totalPages}) =>{ dispatch({ type: FETCH_COMMENTERS_SUCCESS, accounts: result, @@ -26,8 +23,8 @@ export const fetchAccounts = (query = {}) => dispatch => { count, limit, totalPages - }) - ) + }); + }) .catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error})); }; @@ -58,25 +55,3 @@ export const setCommenterStatus = (id, status) => (dispatch) => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); }; - -// Fetch flagged accounts to display in the moderation queue of the community. - -export const fetchFlaggedAccounts = (query = {}) => dispatch => { - dispatch(requestFetchFlaggedAccounts()); - coralApi(`/users?${qs.stringify(query)}`) - .then(({result, page, count, limit, totalPages}) => - dispatch({ - type: FETCH_FLAGGED_COMMENTERS_SUCCESS, - flaggedAccounts: result, - page, - count, - limit, - totalPages - }) - ) - .catch(error => dispatch({type: FETCH_FLAGGED_COMMENTERS_FAILURE, error})); -}; - -const requestFetchFlaggedAccounts = () => ({ - type: FETCH_FLAGGED_COMMENTERS_REQUEST -}); diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index abe481677..79bfd2123 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -4,6 +4,7 @@ import { fetchAccounts, updateSorting, newPage, + fetchFlaggedAccounts, } from '../../actions/community'; import CommunityMenu from './components/CommunityMenu'; @@ -12,11 +13,16 @@ import People from './People'; import FlaggedAccounts from './FlaggedAccounts'; class CommunityContainer extends Component { + + // static propTypes = { + // + // // list of actions (approve, reject, ban) associated with the users + // modActions: PropTypes.arrayOf(PropTypes.string).isRequired, + // } + constructor(props) { super(props); - console.log('DEBUG CONSTRUCTOR CommunityContainer ', props); - this.state = { searchValue: '' }; @@ -49,6 +55,11 @@ class CommunityContainer extends Component { asc: community.ascPeople, ...query })); + + } + + componentWillMount() { + this.props.dispatch(fetchFlaggedAccounts()); } componentDidMount() { @@ -88,8 +99,6 @@ class CommunityContainer extends Component { commenters={community.flaggedAccounts} isFetching={community.isFetchingFlagged} error={community.errorFlagged} - totalPages={community.totalPagesFlagged} - page={community.pageFlagged} {...this} /> ); diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js index 9b218de23..28fc0e1f9 100644 --- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js +++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js @@ -6,18 +6,28 @@ const lang = new I18n(translations); import styles from './Community.css'; -// import Loading from './Loading'; +import Loading from './Loading'; import EmptyCard from '../../components/EmptyCard'; +import User from './components/User'; + +// actions={commenter.actions} const FlaggedAccounts = ({...props}) => { - const {commenters, isFetching, error, totalPages, page} = props; - const hasResults = !isFetching && !!commenters.length; + const {commenters, isFetching} = props; + const hasResults = !isFetching && commenters && !!commenters.length; - console.log('debug props', props); - console.log('debug commenters', commenters); - console.log('debug error', error); - console.log('debug totalPages', totalPages); - console.log('debug page', page); + // const menuOptions = { + // 'reject': {status: 'REJECTED', icon: 'close', key: 'r'}, + // 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'}, + // 'ban': {status: 'BANNED', icon: 'not interested'} + // }; + // + // + // onClickAction={this.onClickAction} + // onClickShowBanDialog={this.onClickShowBanDialog} + // acceptCommenter={props.acceptCommenter} + // rejectCommenter={props.rejectCommenter} + // menuOptions={menuOptions} return (
@@ -25,7 +35,12 @@ const FlaggedAccounts = ({...props}) => { { isFetching && } { hasResults - ?
+ ? commenters.map((commenter, index) => { + return ; + }) : {lang.t('community.no-flagged-accounts')} }
diff --git a/client/coral-admin/src/containers/Community/UserModerationList.css b/client/coral-admin/src/containers/Community/UserModerationList.css new file mode 100644 index 000000000..dc0e6b57b --- /dev/null +++ b/client/coral-admin/src/containers/Community/UserModerationList.css @@ -0,0 +1,187 @@ + +@custom-media --big-viewport (min-width: 780px); + +.list { + padding: 8px 0; + list-style: none; + display: block; + + &.singleView .listItem { + display: none; + } + + &.singleView .listItem.activeItem { + display: block; + height: 100%; + font-size: 1.5em; + line-height: 1.5em; + border: none; + + .actions { + position: fixed; + bottom: 60px; + left: 25%; + margin: 0 auto; + display: flex; + justify-content: space-around; + width: 50%; + margin: 0; + } + + .actionButton { + transform: scale(1.4); + } + } +} + +.listItem { + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + width: 100%; + max-width: 660px; + min-width: 400px; + margin: 0 auto; + padding: 16px 14px; + position: relative; + transition: box-shadow 200ms; + + + &:hover { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + } + + &:last-child { + border-bottom: none; + } + + .sideActions { + position: absolute; + right: 0; + height: 100%; + top: 0; + padding: 40px 18px; + box-sizing: border-box; + } + + .itemHeader { + display: block; + + .author { + font-size: 24px; + min-width: 50px; + align-items: left; + margin-bottom: 15px; + } + } + + .itemBody { + display: block; + } + + .created { + color: #666; + font-size: 13px; + margin-left: 40px; + } + + .body { + margin-top: 20px; + flex: 1; + font-size: 0.88em; + color: black; + } + + .flagged { + color: rgba(255, 0, 0, .5); + padding-top: 15px; + padding-left: 10px; + } + + .flagCount{ + font-size: 12px; + color: #d32f2f; + } + +} + +.empty { + color: #444; + margin-top: 50px; + text-align: center; +} + + +@media (--big-viewport) { + .listItem { + border: 1px solid #e0e0e0; + margin-bottom: 30px; + + &:last-child { + border-bottom: 1px solid #e0e0e0; + } + + &.activeItem { + border: 2px solid #333; + } + } + +} + +.hasLinks { + color: #f00; + text-align: right; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.banned { + color: #f00; + text-align: left; + display: flex; + align-items: center; + + i { + margin-right: 5px; + } +} + +.ban { + display: block; + text-align: center; + margin-top: 5px; +} + +.banButton { + width: 114px; + letter-spacing: 1px; + + i { + vertical-align: middle; + margin-right: 10px; + font-size: 14px; + } +} + + +.actionButton { + transform: scale(.8); + margin: 0; +} + +.flaggedByCount { + display: block; + text-align: left; +} + +.flaggedBy { + display: inline; + padding: 3px; +} + +.flaggedByLabel { + font-weight: bold; +} diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/containers/Community/components/User.js similarity index 51% rename from client/coral-admin/src/components/User.js rename to client/coral-admin/src/containers/Community/components/User.js index 129524993..1f18a186f 100644 --- a/client/coral-admin/src/components/User.js +++ b/client/coral-admin/src/containers/Community/components/User.js @@ -1,15 +1,17 @@ import React from 'react'; -import styles from './ModerationList.css'; +import styles from '../UserModerationList.css'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations.json'; +import translations from '../../../translations.json'; -import {Icon} from 'react-mdl'; -import ActionButton from './ActionButton'; +const lang = new I18n(translations); -// Render a single comment for the list +// import {Icon} from 'react-mdl'; +// import ActionButton from './ActionButton'; + +// Render a single user for the list const User = props => { - const {action, user} = props; + const {user} = props; let userStatus = user.status; // Do not display unless the user status is 'pending' or 'banned'. @@ -19,32 +21,45 @@ const User = props => {
{user.username} -
+
+ +
+
+ flagFlags({ user.actions.length }): + { user.action_summaries.map( + (action, i ) => { + return + {lang.t(`community.${action.reason}`)} ({action.count}) + ; + } + )} +
+
+ {user.actions.map( + (action, i) => { + return + {action.reason} + {/* action.user.username */} + ; + } + )} +
- {props.modActions.map( + {/* props.modActions.map( (action, i) => - - )} + )*/}
-
- {userStatus === 'banned' ? - {lang.t('comment.banned_user')} : null} -
-
-
- {`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
; }; export default User; - -const lang = new I18n(translations); diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 3325249e1..81aa24410 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -2,6 +2,7 @@ import {graphql} from 'react-apollo'; import MOST_FLAGS from './mostFlags.graphql'; import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; +import USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql'; export const mostFlags = graphql(MOST_FLAGS, { options: () => { @@ -28,3 +29,5 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { }; } }); + +export const modUserFlaggedQuery = graphql(USER_FLAGGED_QUERY); 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..55090d8e4 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql @@ -0,0 +1,19 @@ +query Users { + usersFlagged { + id + username + status + roles + actions{ + id + reason + user { + username + } + } + action_summaries { + count + reason + } + } +} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 409c47a00..eae4543ee 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -14,9 +14,13 @@ "banned": "Banned", "banned-user": "Banned User", "loading": "Loading results", - "flaggedaccounts": "Account Flags", + "flaggedaccounts": "Flagged Usernames", "people": "People", - "no-flagged-accounts": "The Account Flags queue is currently empty." + "no-flagged-accounts": "The Account Flags queue is currently empty.", + "This user is impersonating": "Impersonation", + "This looks like an ad/marketing": "Spam/Ads", + "This username is offensive": "Offensive", + "Other": "Other" }, "modqueue": { "likes": "likes", @@ -149,9 +153,13 @@ "banned": "Suspendido", "banned-user": "Usuario Suspendido", "loading": "Cargando resultados", - "flaggedaccounts": "Cuentas Reportadas", + "flaggedaccounts": "Nombres de Usuario Reportados", "people": "Gente", - "no-flagged-accounts": "No hay ninguna cuenta reportada." + "no-flagged-accounts": "No hay ninguna cuenta reportada.", + "This user is impersonating": "Suplantación", + "This looks like an ad/marketing": "Spam/Propaganda", + "This username is offensive": "Ofensivo", + "Other": "Otros" }, "modqueue": { "likes": "gustos", diff --git a/graph/loaders/users.js b/graph/loaders/users.js index 90c661f71..2928e41f3 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -3,11 +3,58 @@ 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(); + + // Only administrators can search for users + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + users = users.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 +62,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/resolvers/root_query.js b/graph/resolvers/root_query.js index ced4e65cf..852bc41ad 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -72,6 +72,22 @@ 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. + usersFlagged(_, args, {user, loaders: {Users, Actions}}) { + + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + return Actions.getByTypes({action_type: 'FLAG', item_type: 'USERS'}) + .then((ids) => { + + // Perform the query using the available resolver. + return Users.getByQuery({ids}); + }); } }; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index fbdeb4279..bd032196b 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -31,10 +31,10 @@ type User { username: String! # Action summaries against the user. - action_summaries: [ActionSummary] + action_summaries: [FlagActionSummary] # Actions completed on the parent. - actions: [Action] + actions: [FlagAction] # the current roles of the user. roles: [USER_ROLES] @@ -60,6 +60,19 @@ type Tag { created_at: Date! } +# UsersQuery allows the ability to query users by a specific methods. +input UsersQuery { + + # 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 ################################################################################ @@ -497,6 +510,9 @@ type RootQuery { # Metrics related to user actions are saturated into the assets returned. The # sort will affect if it will allow metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset] + + # Users returned based on a query. + usersFlagged(query: UsersQuery): [User] } ################################################################################