Merge branch 'master' of github.com:coralproject/talk into wordlist

This commit is contained in:
David Jay
2016-12-12 13:37:43 -05:00
67 changed files with 1550 additions and 207 deletions
+46
View File
@@ -219,6 +219,7 @@ function listUsers() {
'Display Name',
'Profiles',
'Roles',
'Status',
'State'
]
});
@@ -229,6 +230,7 @@ function listUsers() {
user.displayName,
user.profiles.map((p) => p.provider).join(', '),
user.roles.join(', '),
user.status,
user.disabled ? 'Disabled' : 'Enabled'
]);
});
@@ -296,6 +298,40 @@ function removeRole(userID, role) {
});
}
/**
* Ban a user
* @param {String} userID id of the user to ban
*/
function ban(userID) {
User
.setStatus(userID, 'banned', '')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Unban a user
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
User
.setStatus(userID, 'active', '')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Disable a given user.
* @param {String} userID the ID of a user to disable
@@ -384,6 +420,16 @@ program
.description('removes a role from a given user')
.action(removeRole);
program
.command('ban <userID>')
.description('ban a given user')
.action(ban);
program
.command('uban <userID>')
.description('unban a given user')
.action(unban);
program
.command('disable <userID>')
.description('disable a given user from logging in')
+13 -1
View File
@@ -1,4 +1,3 @@
/**
* Action disptacher related to comments
*/
@@ -16,3 +15,16 @@ export const flagComment = id => (dispatch, getState) => {
export const createComment = (name, body) => dispatch => {
dispatch({type: 'COMMENT_CREATE', name, body});
};
// Dialog Actions
export const showBanUserDialog = (userId, userName, commentId) => {
return dispatch => {
dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId});
};
};
export const hideBanUserDialog = (showDialog) => {
return dispatch => {
dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog});
};
};
+11 -3
View File
@@ -6,14 +6,15 @@ import {
FETCH_COMMENTERS_FAILURE,
SORT_UPDATE,
COMMENTERS_NEW_PAGE,
SET_ROLE
SET_ROLE,
SET_COMMENTER_STATUS
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/response';
export const fetchCommenters = (query = {}) => dispatch => {
dispatch(requestFetchCommenters());
coralApi(`/user?${qs.stringify(query)}`)
coralApi(`/users?${qs.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({
type: FETCH_COMMENTERS_SUCCESS,
@@ -41,8 +42,15 @@ export const newPage = () => ({
});
export const setRole = (id, role) => dispatch => {
return coralApi(`/user/${id}/role`, {method: 'POST', body: {role}})
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => dispatch => {
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
+14
View File
@@ -0,0 +1,14 @@
/**
* Action disptacher related to users
*/
//
// export const banUser = (status, author_id) => (dispatch) => {
// dispatch({type: 'USER_STATUS_UPDATE', author_id, status});
// };
export const banUser = (status, userId, commentId) => {
return dispatch => {
dispatch({type: 'USER_BAN', status, userId, commentId});
dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
};
};
@@ -0,0 +1,147 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 280px;
top: 10px;
}
.header {
margin-bottom: 20px;
}
.header h1, .separator h1{
text-align: center;
font-size: 1.2em;
}
.formField {
margin-top: 15px;
}
.formField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.formField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
}
.footer span {
display: block;
margin-bottom: 5px;
}
.footer a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
}
.socialConnections {
margin-bottom: 20px;
}
.signInButton {
margin-top: 10px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.close:hover {
color: #6b6b6b;
}
input.error{
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
}
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
.action {
margin-top: 15px;
}
.passwordRequestSuccess {
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
}
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
}
.cancel {
margin: 10px 0;
}
@@ -0,0 +1,45 @@
import React from 'react';
import {Dialog} from 'coral-ui';
import Button from 'coral-ui/components/Button';
import styles from './BanUserDialog.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => {
const {userName = '', userId = '', commentId = ''} = user;
return (
<Dialog className={styles.dialog} open={open} onClose={() => handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}>
<span className={styles.close} onClick={() => handleClose()}>×</span>
<div>
<div className={styles.header}>
<h3>
{lang.t('bandialog.ban_user')}
</h3>
</div>
<div className={styles.separator}>
<h4>
{lang.t('bandialog.are_you_sure', userName)}
</h4>
<i>
{lang.t('bandialog.note')}
</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} full>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" onClick={() => onClickBanUser(userId, commentId)} full>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
</div>
</Dialog>
);
};
export default BanUserDialog;
+35 -16
View File
@@ -1,17 +1,20 @@
import React from 'react';
import timeago from 'timeago.js';
import Linkify from 'react-linkify';
import styles from './CommentList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import Linkify from 'react-linkify';
import {FabButton} from 'coral-ui';
import {Icon} from 'react-mdl';
import {FabButton, Button} from 'coral-ui';
const linkify = new Linkify();
// Render a single comment for the list
export default props => {
const authorStatus = props.author.get('status');
const {comment, author} = props;
const links = linkify.getMatches(comment.get('body'));
@@ -28,15 +31,13 @@ export default props => {
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={styles.actions}>
{props.actions.map((action, i) => canShowAction(action, comment) ? (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.get('id'))}
/>
) : null)}
{props.actions.map((action, i) => getActionButton(action, i, props))}
</div>
</div>
<div>
{authorStatus === 'banned' ?
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
</div>
</div>
<div className={styles.itemBody}>
<span className={styles.body}>
@@ -49,15 +50,33 @@ export default props => {
);
};
// Check if an action can be performed over a comment
const canShowAction = (action, comment) => {
const status = comment.get('status');
const flagged = comment.get('flagged');
// Get the button of the action performed over a comment if any
const getActionButton = (action, i, props) => {
const status = props.comment.get('status');
const flagged = props.comment.get('flagged');
const banned = (props.author.get('status') === 'banned');
if (action === 'flag' && (status || flagged === true)) {
return false;
return null;
}
return true;
if (action === 'ban') {
return (
<Button
disabled={banned ? 'disabled' : ''}
cStyle='black'
onClick={() => props.onClickShowBanDialog(props.author.get('id'), props.author.get('displayName'), props.comment.get('id'))}
key={i} >
{lang.t('comment.ban_user')}
</Button>
);
}
return (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
/>
);
};
const linkStyles = {
@@ -122,7 +122,6 @@
}
.hasLinks {
color: #f00;
text-align: right;
@@ -133,3 +132,14 @@
margin-right: 5px;
}
}
.banned {
color: #f00;
text-align: left;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
@@ -9,7 +9,8 @@ import Comment from 'components/Comment';
const actions = {
'reject': {status: 'rejected', icon: 'close', key: 'r'},
'approve': {status: 'accepted', icon: 'done', key: 't'},
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'},
'ban': {status: 'banned', icon: 'not interested'}
};
// Renders a comment list and allow performing actions
@@ -19,6 +20,7 @@ export default class CommentList extends React.Component {
this.state = {active: null};
this.onClickAction = this.onClickAction.bind(this);
this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this);
}
// remove key handlers before leaving
@@ -99,7 +101,8 @@ export default class CommentList 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) {
onClickAction (action, id, author_id) {
// activate the next comment
if (id === this.state.active) {
const {commentIds} = this.props;
if (commentIds.last() === this.state.active) {
@@ -108,7 +111,11 @@ export default class CommentList extends React.Component {
this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))});
}
}
this.props.onClickAction(action, id);
this.props.onClickAction(action, id, author_id);
}
onClickShowBanDialog(userId, userName, commentId) {
this.props.onClickShowBanDialog(userId, userName, commentId);
}
render () {
@@ -125,6 +132,7 @@ export default class CommentList extends React.Component {
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
actions={this.props.actions}
actionsMap={actions}
isActive={commentId === active}
@@ -0,0 +1,3 @@
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const USER_BAN_SUCESS = 'USER_BAN_SUCESS';
@@ -4,3 +4,4 @@ export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
export const SORT_UPDATE = 'SORT_UPDATE';
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
export const SET_ROLE = 'SET_ROLE';
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
@@ -1,7 +1,3 @@
.dataTable {
width: 100%;
}
.roleButton {
display: block;
}
@@ -9,14 +5,13 @@
.searchInput {
display: block;
padding-left: 40px;
/*border: none;*/
width: auto;
}
.searchBox {
/*border: 1px solid rgba(0,0,0,.12);*/
background: white;
}
.email {
display: block;
}
}
@@ -20,6 +20,10 @@ const tableHeaders = [
title: lang.t('community.account_creation_date'),
field: 'created_at'
},
{
title: lang.t('community.status'),
field: 'status'
},
{
title: lang.t('community.newsroom_role'),
field: 'role'
@@ -30,7 +34,7 @@ const Community = ({isFetching, commenters, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
return (
<Grid>
<Cell col={4}>
<Cell col={2}>
<form action="">
<div className={`mdl-textfield ${styles.searchBox}`}>
<label className="mdl-button mdl-js-button mdl-button--icon" htmlFor="commenters-search">
@@ -49,7 +53,7 @@ const Community = ({isFetching, commenters, ...props}) => {
</div>
</form>
</Cell>
<Cell col={8}>
<Cell col={6}>
{ isFetching && <Loading /> }
{ !hasResults && <NoResults /> }
{ hasResults &&
@@ -40,8 +40,8 @@ class CommunityContainer extends Component {
this.props.dispatch(fetchCommenters({
value: this.state.searchValue,
field: community.get('field'),
asc: community.get('asc'),
field: community.field,
asc: community.asc,
...query
}));
}
@@ -66,15 +66,19 @@ class CommunityContainer extends Component {
return (
<Community
searchValue={searchValue}
commenters={community.get('commenters')}
isFetching={community.get('isFetching')}
error={community.get('error')}
totalPages={community.get('totalPages')}
page={community.get('page')}
commenters={community.commenters}
isFetching={community.isFetching}
error={community.error}
totalPages={community.totalPages}
page={community.page}
{...this}
/>
);
}
}
export default connect(({community}) => ({community}))(CommunityContainer);
const mapStateToProps = state => ({
community: state.community.toJS()
});
export default connect(mapStateToProps)(CommunityContainer);
@@ -4,7 +4,7 @@ import {SelectField, Option} from 'react-mdl-selectfield';
import styles from './Community.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations';
import {setRole} from '../../actions/community';
import {setRole, setCommenterStatus} from '../../actions/community';
const lang = new I18n(translations);
@@ -19,6 +19,10 @@ class Table extends Component {
this.props.dispatch(setRole(id, role));
}
onCommenterStatusChange (id, status) {
this.props.dispatch(setCommenterStatus(id, status));
}
render () {
const {headers, commenters, onHeaderClickHandler} = this.props;
@@ -46,6 +50,14 @@ class Table extends Component {
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.status || ''}
label={lang.t('community.status')}
onChange={status => this.onCommenterStatusChange(row.id, status)}>
<Option value={'active'}>{lang.t('community.active')}</Option>
<Option value={'banned'}>{lang.t('community.banned')}</Option>
</SelectField>
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.roles[0] || ''}
label={lang.t('community.role')}
@@ -4,8 +4,10 @@ import key from 'keymaster';
import ModerationKeysModal from 'components/ModerationKeysModal';
import CommentList from 'components/CommentList';
import BanUserDialog from 'components/BanUserDialog';
import {updateStatus} from 'actions/comments';
import {updateStatus, showBanUserDialog, hideBanUserDialog} from 'actions/comments';
import {banUser} from 'actions/users';
import styles from './ModerationQueue.css';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -51,8 +53,21 @@ class ModerationQueue extends React.Component {
}
// Dispatch the update status action
onCommentAction (status, id) {
this.props.dispatch(updateStatus(status, id));
onCommentAction (action, id) {
// If not banning then change the status to approved or flagged as action = status
this.props.dispatch(updateStatus(action, id));
}
showBanUserDialog (userId, userName, commentId) {
this.props.dispatch(showBanUserDialog(userId, userName, commentId));
}
hideBanUserDialog () {
this.props.dispatch(hideBanUserDialog(false));
}
banUser (userId, commentId) {
this.props.dispatch(banUser('banned', userId, commentId));
}
onTabClick (activeTab) {
@@ -89,10 +104,16 @@ class ModerationQueue extends React.Component {
}
comments={comments.get('byId')}
users={users.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
onClickAction={(action, commentId) => this.onCommentAction(action, commentId)}
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
actions={['reject', 'approve', 'ban']}
loading={comments.loading} />
</div>
<BanUserDialog
open={comments.get('showBanUserDialog')}
handleClose={() => this.hideBanUserDialog()}
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
user={comments.get('banUser')}/>
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<CommentList
isActive={activeTab === 'rejected'}
+19 -2
View File
@@ -1,4 +1,4 @@
import * as actions from '../constants/comments';
import {Map, List, fromJS} from 'immutable';
/**
@@ -11,7 +11,13 @@ import {Map, List, fromJS} from 'immutable';
const initialState = Map({
byId: Map(),
ids: List(),
loading: false
loading: false,
showBanUserDialog: false,
banUser: {
'userName': '',
'userId': '',
'commentId': ''
}
});
// Handle the comment actions
@@ -24,10 +30,21 @@ export default (state = initialState, action) => {
case 'COMMENT_FLAG': return flag(state, action);
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action);
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state);
case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
case actions.USER_BAN_SUCESS: return setBanUser(state, false, action);
default: return state;
}
};
// hide or show the UI for the dialog confirming the ban
// set the user that is going to set and the comment that is the reason
const setBanUser = (state, showBanUser, action) => {
const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId};
return state.set('showBanUserDialog', showBanUser)
.set('banUser', banUser);
};
// Update a comment status
const updateStatus = (state, action) => {
const byId = state.get('byId');
+10 -1
View File
@@ -5,7 +5,8 @@ import {
FETCH_COMMENTERS_FAILURE,
FETCH_COMMENTERS_SUCCESS,
SORT_UPDATE,
SET_ROLE
SET_ROLE,
SET_COMMENTER_STATUS
} from '../constants/community';
const initialState = Map({
@@ -45,6 +46,14 @@ export default function community (state = initialState, action) {
commenters[idx].roles[0] = action.role;
return state.set('commenters', commenters.map(id => id));
}
case SET_COMMENTER_STATUS: {
const commenters = state.get('commenters');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].status = action.status;
return state.set('commenters', commenters.map(id => id));
}
case SORT_UPDATE :
return state
.set('field', action.sort.field)
+8
View File
@@ -8,6 +8,7 @@ const initialState = Map({
export default (state = initialState, action) => {
switch (action.type) {
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
case 'USER_STATUS_UPDATE': return updateUserStatus(state, action);
default: return state;
}
};
@@ -18,3 +19,10 @@ const replaceUsers = (action, state) => {
return state.set('byId', users)
.set('ids', List(users.keys()));
};
// Update a user status
const updateUserStatus = (state, action) => {
const byId = state.get('byId');
const data = byId.get(action.author_id).set('status', action.status.toLowerCase());
return state.set('byId', byId.set(action.author_id, data));
};
@@ -21,6 +21,9 @@ export default store => next => action => {
case 'COMMENT_CREATE':
createComment(store, action.name, action.body);
break;
case 'USER_BAN':
userStatusUpdate(store, action.status, action.userId, action.commentId);
break;
}
next(action);
@@ -81,3 +84,10 @@ const createComment = (store, name, comment) => {
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
};
// Ban a user
const userStatusUpdate = (store, status, userId, commentId) => {
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then(res => store.dispatch({type: 'USER_BAN_SUCESS', res}))
.catch(error => store.dispatch({type: 'USER_BAN_FAILED', error}));
};
+40 -8
View File
@@ -6,7 +6,14 @@
"newsroom_role": "Newsroom Role",
"admin": "Administrator",
"moderator": "Moderator",
"role": "Select role..."
"role": "Select role...",
"no-results": "No users found with that user name or email address.",
"status": "Status",
"select-status": "Select status...",
"active": "Active",
"banned": "Banned",
"banned-user": "Banned User",
"loading": "Loading results"
},
"modqueue": {
"pending": "pending",
@@ -25,7 +32,9 @@
},
"comment": {
"flagged": "flagged",
"anon": "Anonymous"
"anon": "Anonymous",
"ban_user": "Ban User",
"banned_user": "Banned User"
},
"embedlink": {
"copy": "Copy to Clipboard"
@@ -38,7 +47,7 @@
"comment-settings": "Comment Settings",
"embed-comment-stream": "Embed Comment Stream",
"banned-word-header": "Write the bannned words list",
"banned-word-text": "Comments which contain these words or phrases, not seperated by commas and not case sensitive, will be automatically removed from the comment stream.",
"banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.",
"wordlist": "Banned words list",
"save-changes": "Save Changes",
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
@@ -47,6 +56,13 @@
"community": "Community",
"closed-comments-desc": "Write a message for closed threads",
"closed-comments-label": "Write a message..."
},
"bandialog": {
"ban_user": "Ban User?",
"are_you_sure": "Are you sure you would like to ban {0}?",
"note": "Note: Banning this user will also place this comment in the Rejected queue.",
"cancel": "Cancel",
"yes_ban_user": "Yes, Ban User"
}
},
"es": {
@@ -56,7 +72,14 @@
"newsroom_role": "Rol en la redacción",
"admin": "Administrador",
"moderator": "Moderador",
"role": "Select role..."
"role": "Seleccionar rol...",
"no-results": "No se encontraron usuarios con ese nombre de usuario o correo electronico.",
"status": "Estado",
"select-status": "Seleccionar estado...",
"active": "Activa",
"banned": "Suspendido",
"banned-user": "Usuario Suspendido",
"loading": "Cargando resultados"
},
"modqueue": {
"pending": "pendiente",
@@ -67,7 +90,9 @@
},
"comment": {
"flagged": "marcado",
"anon": "Anónimo"
"anon": "Anónimo",
"ban_user": "Suspender Usuario",
"banned_user": "Usuario Suspendido"
},
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
@@ -76,9 +101,9 @@
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"wordlist": "¡traduceme!",
"banned-word-header": "¡traduceme!",
"banned-word-text": "¡traduceme!",
"wordlist": "Lista de palabras no permitidas",
"banned-word-header": "Escribir las palabras no permitidas",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"save-changes": "Guardar Cambios",
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
"moderate": "Moderar",
@@ -86,6 +111,13 @@
"community": "Comunidad",
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
"closed-comments-label": "Escribe un mensaje..."
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
"are_you_sure": "Estas segura que quieres suspender a {props.author.displayName}?",
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
}
}
}
@@ -0,0 +1,37 @@
.container {
position: relative;
}
.apply {
position: absolute;
top: 38%;
transform: translateX(-50%);
right: 0;
}
ul {
list-style: none;
padding: 0;
}
ul ul {
padding-left: 20px
}
.checkbox {
vertical-align: top;
margin: 12px 12px 12px 0;
}
h4 {
font-size: 14px;
margin-bottom: 5px;
}
p {
max-width: 380px;
}
.wrapper {
margin-bottom: 20px;
}
@@ -0,0 +1,53 @@
import React from 'react';
import {Button, Checkbox} from 'coral-ui';
import styles from './ConfigureCommentStream.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
const lang = new I18n(translations);
export default ({handleChange, handleApply, changed, ...props}) => (
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{lang.t('configureCommentStream.title')}</h3>
<p>{lang.t('configureCommentStream.description')}</p>
<Button
className={styles.apply}
cStyle={changed ? 'green' : 'darkGrey'}
onClick={handleApply}
>
{lang.t('configureCommentStream.apply')}
</Button>
</div>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premod"
onChange={handleChange}
checked={props.premod}
info={{
title: lang.t('configureCommentStream.enablePremod'),
description: lang.t('configureCommentStream.enablePremodDescription')
}}
/>
<ul>
<li>
<Checkbox
className={styles.checkbox}
cStyle={changed ? 'green' : 'darkGrey'}
name="premodLinks"
onChange={handleChange}
checked={props.premodLinks}
info={{
title: lang.t('configureCommentStream.enablePremodLinks'),
description: lang.t('configureCommentStream.enablePremodDescription')
}}
/>
</li>
</ul>
</li>
</ul>
</div>
);
@@ -0,0 +1,83 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
import CloseCommentsInfo from '../components/CloseCommentsInfo';
import ConfigureCommentStream from '../components/ConfigureCommentStream';
class ConfigureStreamContainer extends Component {
constructor (props) {
super(props);
this.state = {
premod: props.config.moderation === 'pre',
premodLinks: false
};
this.toggleStatus = this.toggleStatus.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleApply = this.handleApply.bind(this);
}
handleApply () {
const {premod, changed} = this.state;
const newConfig = {
moderation: premod ? 'pre' : 'post'
};
if (changed) {
this.props.updateConfiguration(newConfig);
setTimeout(() => {
this.setState({
changed: false
});
}, 300);
}
}
handleChange (e) {
const {name, checked} = e.target;
this.setState({
[name]: checked,
changed: true
});
}
toggleStatus () {
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
}
render () {
const {status} = this.props;
return (
<div>
<ConfigureCommentStream
handleChange={this.handleChange}
handleApply={this.handleApply}
changed={this.state.changed}
{...this.state}
/>
<hr />
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
<CloseCommentsInfo
onClick={this.toggleStatus}
status={status}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
config: state.config.toJS()
});
const mapDispatchToProps = dispatch => ({
updateStatus: status => dispatch(updateOpenStatus(status)),
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ConfigureStreamContainer);
+24
View File
@@ -0,0 +1,24 @@
{
"en": {
"configureCommentStream": {
"apply": "Apply",
"title": "Configure Comment Stream",
"description": "As an admin you may customize the settings for the comment stream for this article",
"enablePremod": "Enable Premoderation",
"enablePremodDescription": "Moderators must approve any comment before its published.",
"enablePremodLinks": "Pre-Moderate Comments Containing Links",
"enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published."
}
},
"es": {
"configureCommentStream": {
"apply": "Aplicar",
"title": "Configurar los comentarios",
"description": "Como Administrador puedes modificar las opciones de los comentarios en este artículo",
"enablePremod": "Activar Pre Moderación",
"enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación",
"enablePremodLinks": "Pre-Moderar Commentarios que contienen Links",
"enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación."
}
}
}
+51 -30
View File
@@ -6,8 +6,7 @@ import {
itemActions,
Notification,
notificationActions,
authActions,
configActions
authActions
} from '../../coral-framework';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
@@ -17,7 +16,7 @@ import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import FlagButton from '../../coral-plugin-flags/FlagButton';
import FlagComment from '../../coral-plugin-flags/FlagComment';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
@@ -27,12 +26,12 @@ import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import CloseCommentsInfo from '../../coral-framework/components/CloseCommentsInfo';
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout, showSignInDialog} = authActions;
const {updateOpenStatus} = configActions;
class CommentStream extends Component {
@@ -44,7 +43,6 @@ class CommentStream extends Component {
};
this.changeTab = this.changeTab.bind(this);
this.toggleStatus = this.toggleStatus.bind(this);
}
changeTab (tab) {
@@ -53,10 +51,6 @@ class CommentStream extends Component {
});
}
toggleStatus () {
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
}
static propTypes = {
items: PropTypes.object.isRequired,
addItem: PropTypes.func.isRequired,
@@ -96,9 +90,11 @@ class CommentStream extends Component {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {actions, users, comments} = this.props.items;
const {status, moderation, closedMessage} = this.props.config;
const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth;
const {status, closedMessage} = this.props.config;
const {activeTab} = this.state;
const banned = (this.props.userData.status === 'banned');
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 150
} : {};
@@ -112,8 +108,6 @@ class CommentStream extends Component {
<Tab>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
{/* Add to the restricted param a boolean if the user is suspended*/}
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
<TabContent show={activeTab === 0}>
{
status === 'open'
@@ -122,16 +116,20 @@ class CommentStream extends Component {
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={<SuspendedAccount />}>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
premod={moderation}
reply={false}
currentUser={this.props.auth.user}
banned={banned}
author={user}
/>
</RestrictedContent>
</div>
: <p>{closedMessage}</p>
}
@@ -141,14 +139,26 @@ class CommentStream extends Component {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<AuthorName
author={users[comment.author_id]}
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
currentUser={this.props.auth.user}
showReply={comment.showReply}
banned={banned}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
@@ -158,18 +168,21 @@ class CommentStream extends Component {
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="commentActionsRight">
<FlagButton
<FlagComment
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
@@ -183,7 +196,8 @@ class CommentStream extends Component {
id={rootItemId}
author={user}
parent_id={commentId}
premod={this.props.config.moderation}
premod={moderation}
currentUser={user}
showReply={comment.showReply}/>
{
comment.children &&
@@ -198,6 +212,8 @@ class CommentStream extends Component {
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
banned={banned}
currentUser={this.props.auth.user}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
@@ -208,18 +224,21 @@ class CommentStream extends Component {
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="replyActionsRight">
<FlagButton
<FlagComment
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
author_id={comment.author_id}
flag={actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
@@ -235,7 +254,9 @@ class CommentStream extends Component {
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
premod={moderation}
banned={banned}
currentUser={user}
showReply={reply.showReply}/>
</div>;
})
@@ -257,12 +278,13 @@ class CommentStream extends Component {
/>
</TabContent>
<TabContent show={activeTab === 2}>
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
<RestrictedContent restricted={!loggedIn}>
<CloseCommentsInfo onClick={this.toggleStatus} status={status} />
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</RestrictedContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
@@ -285,19 +307,18 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = (dispatch) => ({
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
addItem: (item, item_id) => dispatch(addItem(item, item_id)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout()),
updateStatus: status => dispatch(updateOpenStatus(status))
logout: () => dispatch(logout())
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
@@ -127,6 +127,10 @@ hr {
font-weight: bolder;
}
.coral-plugin-author-name-bio-flag {
float: right;
}
/* Reply styles */
@@ -197,6 +201,54 @@ hr {
margin: 8px;
}
/* Flag Styles */
.coral-plugin-flags-container {
position: relative;
}
.coral-plugin-flags-popup span {
min-width: 280px;
bottom: 36px;
left: -190px;
position: absolute;
}
.coral-plugin-flags-popup-form {
margin-bottom: 10px;
}
.coral-plugin-flags-popup-header {
font-weight: bolder;
font-size: 16px;
margin-bottom: 10px;
}
.coral-plugin-flags-popup-radio {
margin:5px;
}
.coral-plugin-flags-popup-radio-label {
margin:5px;
font-size: 14px;
}
.coral-plugin-flags-popup-counter {
float: left;
margin-top: 21px;
color: #999;
}
.coral-plugin-flags-popup-button {
float: right;
margin-top: 10px;
}
.coral-plugin-flags-other-text {
margin-left: 20px;
width: 75%;
}
/* Close comments */
.close-comments-intro-wrapper {
+1 -1
View File
@@ -91,7 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
export const fetchForgotPassword = email => dispatch => {
dispatch(forgotPassowordRequest(email));
coralApi('/user/request-password-reset', {method: 'POST', body: {email}})
coralApi('/users/request-password-reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
+24 -10
View File
@@ -1,19 +1,33 @@
import coralApi from '../helpers/response';
/* Config Actions */
import * as actions from '../constants/config';
import {addNotification} from '../actions/notification';
/**
* Action name constants
*/
export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
export const updateOpenStatus = status => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'})
.then(() => dispatch({type: status === 'open' ? OPEN_COMMENTS : CLOSE_COMMENTS}));
.then(() => dispatch({type: status === 'open' ? actions.OPEN_COMMENTS : actions.CLOSE_COMMENTS}));
};
const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST});
const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config});
const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE});
export const updateConfiguration = newConfig => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
dispatch(updateConfigRequest());
coralApi(`/asset/${assetId}/settings`, {method: 'PUT', body: newConfig})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
dispatch(updateConfigSuccess(newConfig));
})
.catch(error => dispatch(updateConfigFailure(error)));
};
+3 -13
View File
@@ -1,14 +1,9 @@
import coralApi from '../helpers/response';
import {fromJS} from 'immutable';
/* Item Actions */
/**
* Action name constants
*/
import {UPDATE_CONFIG} from '../constants/config';
export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
/**
@@ -106,7 +101,7 @@ export function getStream (assetUrl) {
dispatch(addItem(action, 'actions'));
});
} else if (type === 'settings') {
dispatch({type: UPDATE_SETTINGS, config: fromJS(json[type])});
dispatch({type: UPDATE_CONFIG, config: fromJS(json[type])});
} else {
json[type].forEach(item => {
dispatch(addItem(item, type));
@@ -217,13 +212,8 @@ export function postItem (item, type, id) {
*
*/
export function postAction (item_id, action_type, user_id, item_type) {
export function postAction (item_id, item_type, action) {
return () => {
const action = {
action_type,
user_id
};
return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action});
};
}
+1 -1
View File
@@ -12,7 +12,7 @@ const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
export const saveBio = (user_id, formData) => dispatch => {
dispatch(saveBioRequest());
coralApi(`/user/${user_id}/bio`, {method: 'PUT', body: formData})
coralApi(`/users/${user_id}/bio`, {method: 'PUT', body: formData})
.then(({settings}) => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(settings));
@@ -2,7 +2,10 @@ import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
import styles from './RestrictedContent.css';
export default () => (
<span>{lang.t('suspendedAccountMsg')}</span>
<div className={styles.message}>
<span>{lang.t('suspendedAccountMsg')}</span>
</div>
);
@@ -0,0 +1,9 @@
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
+13 -16
View File
@@ -1,31 +1,28 @@
/* @flow */
import {Map} from 'immutable';
import * as actions from '../actions/config';
import * as actions from '../constants/config';
const initialState = Map({
features: Map({}),
status: 'open',
closedMessage: ''
moderation: null
});
export default (state = initialState, action) => {
switch(action.type) {
// Override config if worked
case actions.UPDATE_SETTINGS:
return action.config;
case actions.UPDATE_CONFIG:
return state
.merge(Map(action.config));
case actions.UPDATE_CONFIG_SUCCESS:
return state
.merge(Map(action.config));
case actions.OPEN_COMMENTS:
return state.set('status', 'open');
return state
.set('status', 'open');
case actions.CLOSE_COMMENTS:
return state.set('status', 'closed');
return state
.set('status', 'closed');
case actions.ADD_ITEM:
return action.item_type === 'assets' ?
state.set('status', action.item.status)
: state;
return action.item_type === 'assets' ? state.set('status', action.item.status) : state;
default:
return state;
}
+2
View File
@@ -1,5 +1,6 @@
{
"en": {
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
"successBioUpdate": "Your Bio has been updated",
"contentNotAvailable": "This content is not available",
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
@@ -13,6 +14,7 @@
}
},
"es": {
"successUpdateSettings": "La configuración de este articulo fue actualizada",
"successBioUpdate": "Tu bio fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
+10 -1
View File
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import {Tooltip} from 'coral-ui';
import FlagBio from '../coral-plugin-flags/FlagBio';
const packagename = 'coral-plugin-author-name';
export default class AuthorName extends Component {
@@ -36,7 +37,15 @@ export default class AuthorName extends Component {
onMouseLeave={this.handleMouseLeave}
>
{author && author.displayName}
{ showTooltip && <Tooltip>{author.settings.bio}</Tooltip>}
{ showTooltip && <Tooltip>
<div className={`${packagename}-bio`}>
{author.settings.bio}
</div>
<div className={`${packagename}-bio-flag`}>
<FlagBio {...this.props}/>
</div>
</Tooltip>
}
</div>
);
}
@@ -6,15 +6,15 @@
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
"comment-post-banned-word": "Your comment contains one or more words which we do not allow. It has been sent to the moderation team for review."
"comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
},
"es": {
"post": "Publicar",
"reply": "Respuesta",
"comment": "Comentario",
"name": "Nombre",
"comment-post-notif": "¡traduceme!",
"comment-post-notif-premod": "¡traduceme!",
"comment-post-banned-word": "¡traduceme!"
"comment-post-notif": "Tu comentario ha sido publicado.",
"comment-post-notif-premod": "Gracias por comentar. Nuestro equipo de moderación va a revisarlo muy pronto.",
"comment-post-banned-word": "Tu comentario contiene una o más palabras que no estan permitidasen nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
}
}
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import FlagButton from './FlagButton';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const FlagBio = (props) => <FlagButton {...props} getPopupMenu={getPopupMenu} />;
const getPopupMenu = [
() => {
return {
header: lang.t('step-2-header'),
itemType: 'user',
field: 'bio',
options: [
{val: 'This bio is offensive', text: lang.t('bio-offensive')},
{val: 'I don\'t like this bio', text: lang.t('no-like-bio')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
{val: 'other', text: lang.t('other')}
],
button: lang.t('continue'),
sets: 'detail'
};
},
() => {
return {
header: lang.t('step-3-header'),
text: lang.t('thank-you'),
button: lang.t('done'),
};
}
];
export default FlagBio;
const lang = new I18n(translations);
+159 -30
View File
@@ -1,49 +1,178 @@
import React from 'react';
import React, {Component} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import {PopupMenu, Button} from 'coral-ui';
import onClickOutside from 'react-onclickoutside';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
class FlagButton extends Component {
state = {
showMenu: false,
showOther: false,
itemType: '',
detail: '',
otherText: '',
step: 0,
posted: false
}
// When the "report" button is clicked expand the menu
onReportClick = () => {
if (!this.props.currentUser) {
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
return;
}
if (!flagged) {
postAction(id, 'flag', currentUser.id, 'comments')
this.setState({showMenu: !this.state.showMenu});
}
onPopupContinue = () => {
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
const {itemType, field, detail, step, otherText, posted} = 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});
} else {
this.setState({step: step + 1});
}
// If itemType and detail are both set, post the action
if (itemType && detail && !posted) {
// Set the text from the "other" field if it exists.
const updatedDetail = otherText || detail;
let item_id;
switch(itemType) {
case 'comments':
item_id = id;
break;
case 'user':
item_id = author_id;
break;
}
const action = {
action_type: 'flag',
field,
detail: updatedDetail
};
postAction(item_id, itemType, action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, id, 'comments');
updateItem(action.item_id, action.action_type, id, action.item_type);
this.setState({posted: true});
});
addNotification('success', lang.t('flag-notif'));
} else {
deleteAction(flagged.id)
.then(() => {
updateItem(id, 'flag', '', 'comments');
});
addNotification('success', lang.t('flag-notif-remove'));
}
};
}
return <div className={`${name}-container`}>
<button onClick={onFlagClick} className={`${name}-button`}>
onPopupOptionClick = (sets) => (e) => {
// If the "other" option is clicked, show the other textbox
if(sets === 'detail' && e.target.value === 'other') {
this.setState({showOther: true});
}
// If flagging a user, indicate that this is referencing the username rather than the bio
if(sets === 'itemType' && e.target.value === 'user') {
this.setState({field: 'username'});
}
// Set itemType and field if they are defined in the popupMenu
const currentMenu = this.props.getPopupMenu[this.state.step]();
if (currentMenu.itemType) {
this.setState({itemType: currentMenu.itemType});
}
if (currentMenu.field) {
this.setState({field: currentMenu.field});
}
this.setState({[sets]: e.target.value});
}
onOtherTextChange = (e) => {
this.setState({otherText: e.target.value});
}
handleClickOutside () {
this.setState({showMenu: false});
}
render () {
const {flag, getPopupMenu} = this.props;
const flagged = flag && flag.current_user;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
<button onClick={this.onReportClick} className={`${name}-button`}>
{
flagged
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
{
flagged
? <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
this.state.showMenu &&
<div className={`${name}-popup`}>
<PopupMenu>
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
{
popupMenu.text &&
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
}
{
popupMenu.options && <form className={`${name}-popup-form`}>
{
popupMenu.options.map((option) =>
<div key={option.val}>
<input
className={`${name}-popup-radio`}
type="radio"
id={option.val}
checked={this.state[popupMenu.sets] === option.val}
onClick={this.onPopupOptionClick(popupMenu.sets)}
value={option.val}/>
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
</div>
)
}
{
this.state.showOther && <div>
<input
className={`${name}-other-text`}
type="text"
id="otherText"
onChange={this.onOtherTextChange}
value={this.state.otherText}/>
<label htmlFor={'otherText'} className={`${name}-popup-radio-label screen-reader-text`}>
lang.t('flag-reason')
</label><br/>
</div>
}
</form>
}
<div className={`${name}-popup-counter`}>
{this.state.step + 1} of {getPopupMenu.length}
</div>
{
popupMenu.button && <Button
className={`${name}-popup-button`}
onClick={this.onPopupContinue}>
{popupMenu.button}
</Button>
}
</PopupMenu>
</div>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
</div>;
};
</div>;
}
}
export default FlagButton;
export default onClickOutside(FlagButton);
const styles = {
flaggedIcon: {
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
import FlagButton from './FlagButton';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const FlagComment = (props) => <FlagButton {...props} getPopupMenu={getPopupMenu} />;
const getPopupMenu = [
() => {
return {
header: lang.t('step-1-header'),
options: [
{val: 'user', text: lang.t('flag-username')},
{val: 'comments', text: lang.t('flag-comment')}
],
button: lang.t('continue'),
sets: 'itemType'
};
},
(itemType) => {
const options = itemType === 'comments' ?
[
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
{val: 'This comment reveals personally identifiable infomration', text: lang.t('personal-info')},
{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 looks like an ad/marketing', text: lang.t('marketing')},
{val: 'other', text: lang.t('other')}
];
return {
header: lang.t('step-2-header'),
options,
button: lang.t('continue'),
sets: 'detail'
};
},
() => {
return {
header: lang.t('step-3-header'),
text: lang.t('thank-you'),
button: lang.t('done'),
};
}
];
export default FlagComment;
const lang = new I18n(translations);
+44 -8
View File
@@ -1,14 +1,50 @@
{
"en": {
"flag": "Flag",
"flagged": "Flagged",
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
"flag-notif-remove": "Your flag has been removed."
"report": "Report",
"reported": "Reported",
"report-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
"report-notif-remove": "Your report has been removed.",
"step-1-header": "Report an issue",
"step-2-header": "Help us understand",
"step-3-header": "Thank you for your input",
"flag-username": "Flag username",
"flag-comment": "Flag comment",
"continue": "Continue",
"done": "Done",
"no-agree-comment": "I don't agree with this comment",
"comment-offensive": "This comment is offensive",
"personal-info": "This comment reveals personally identifiable information",
"username-offensive": "This username is offensive",
"no-like-username": "I don't like this username",
"bio-offensive": "This bio is offensive",
"no-like-bio": "I don't like this bio",
"marketing": "This looks like an ad/marketing",
"thank-you": "We value your safety and feedback. A moderator will review your flag.",
"flag-reason": "Reason for flag",
"other": "Other"
},
"es": {
"flag": "Marcar",
"flagged": "Marcado",
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
"flag-notif-remove": "¡traduceme!"
"report": "Informe",
"reported": "Informado",
"report-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
"report-notif-remove": "Tu marca ha sido eliminada.",
"step-1-header": "Reportar un problema",
"step-2-header": "Ayudanos a entender",
"step-3-header": "Gracias por tu participación",
"flag-username": "Marcar el nombre de usuario",
"flag-comment": "Marcar el comentario",
"continue": "Continuar",
"done": "hecho",
"no-agree-comment": "No estoy de acuerdo con este comentario",
"comment-offensive": "Este comentario es ofensivo",
"personal-info": "Este comentario muestra información personal",
"username-offensive": "Este nombre de usuario es ofensivo",
"no-like-username": "No me gusta ese nombre de usuario",
"bio-offensive": "Esta bio es ofensiva",
"no-like-bio": "No me gusta esta bio",
"marketing": "Esto parece una publicidad/marketing",
"thank-you": "Nos interesa tu protección y comentarios. Un moderador va a mirar tu marca.",
"flag-reason": "Razón por la que marcar",
"other": "Otro"
}
}
+8 -2
View File
@@ -4,7 +4,7 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
@@ -12,8 +12,14 @@ const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDial
showSignInDialog(offset);
return;
}
if (banned) {
return;
}
if (!liked) {
postAction(id, 'like', currentUser.id, 'comments')
const action = {
action_type: 'like'
};
postAction(id, 'comments', action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
+6 -1
View File
@@ -6,7 +6,12 @@ const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => props.updateItem(props.id, 'showReply', !props.showReply, 'comments')}>
onClick={() => {
if (props.banned) {
return;
}
props.updateItem(props.id, 'showReply', !props.showReply, 'comments');
}}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
View File
+11 -1
View File
@@ -21,7 +21,7 @@
cursor: pointer;
text-decoration: none;
text-align: center;
line-height: 36px;
line-height: 28px;
vertical-align: middle;
margin: 2px;
}
@@ -77,6 +77,16 @@
background: #696969;
}
.type--green {
color: white;
background: #00897B;
}
.type--green:hover {
color: white;
background: #00a291;
}
.full {
width: 100%;
margin: 0;
+70
View File
@@ -0,0 +1,70 @@
.label {
position: relative;
display: inline-block;
}
.label input {
visibility: hidden;
position: absolute;
left: 7px;
bottom: 7px;
margin: 0;
padding: 0;
outline: none;
cursor: pointer;
opacity: 0;
}
.checkbox {
cursor: pointer;
}
.label input[type="checkbox"]:checked + .checkbox:before {
content: "\e834";
}
.label input[type="checkbox"] + .checkbox:before {
content: "\e835";
color: #717171;
}
.label.type--green input[type="checkbox"] + .checkbox:before {
color: #00a291;
}
.label input[type="checkbox"] + .checkbox:before {
position: absolute;
left: 4px;
top: 0px;
width: 18px;
height: 18px;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
vertical-align: -6px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-feature-settings: 'liga';
font-feature-settings: 'liga';
-webkit-transition: all .2s ease;
transition: all .2s ease;
z-index: 1;
}
.checkboxInfo {
display: inline-block;
max-width: 360px;
margin-left: 50px;
}
.checkboxInfo h4 {
margin: 0 0 5px;
}
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import styles from './Checkbox.css';
export default ({name, cStyle = 'base', onChange, label, className, info, checked = 'false'}) => (
<label className={`${styles.label} ${styles[`type--${cStyle}`]} ${className}`} htmlFor={name}>
<input type="checkbox" id={name} name={name} onChange={onChange} checked={checked} />
<span className={styles.checkbox}></span>
{label && <span>{label}</span>}
{info && (
<div className={styles.checkboxInfo}>
<h4>{info.title}</h4>
<span>{info.description}</span>
</div>
)}
</label>
);
+31
View File
@@ -0,0 +1,31 @@
.popupMenu {
display: inline-block;
width: inherit;
border: solid 1px #999;
box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3);
box-sizing: border-box;
background: white;
border-radius: 3px;
padding: 20px 10px;
z-index: 3;
}
.popupMenu:before{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 3em;
bottom: -20px;
z-index: 2;
}
.popupMenu:after{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 3em;
bottom: -21px;
z-index: 1;
}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import styles from './PopupMenu.css';
export default ({children}) => (
<span className={styles.popupMenu}>{children}</span>
);
+2
View File
@@ -7,3 +7,5 @@ export {default as TabContent} from './components/TabContent';
export {default as Button} from './components/Button';
export {default as Spinner} from './components/Spinner';
export {default as Tooltip} from './components/Tooltip';
export {default as PopupMenu} from './components/PopupMenu';
export {default as Checkbox} from './components/Checkbox';
+4 -8
View File
@@ -12,7 +12,9 @@ const ActionSchema = new Schema({
action_type: String,
item_type: String,
item_id: String,
user_id: String
user_id: String,
field: String, // Used when an action references a particular field of an object. (e.g. a flag on a username or bio)
detail: String, // Describes the reason for an action (e.g. 'Username is offensive')
}, {
timestamps: {
createdAt: 'created_at',
@@ -35,13 +37,7 @@ ActionSchema.statics.findById = function(id) {
* @param {String} action the new action to the comment
* @return {Promise}
*/
ActionSchema.statics.insertUserAction = ({item_id, item_type, user_id, action_type}) => {
const action = {
item_id,
item_type,
user_id,
action_type
};
ActionSchema.statics.insertUserAction = (action) => {
// Create/Update the action.
return Action.findOneAndUpdate(action, action, {
+3 -1
View File
@@ -132,10 +132,12 @@ AssetSchema.statics.findOrCreateByUrl = (url) => Asset.findOneAndUpdate({url}, {
* @param {[type]} settings [description]
* @return {[type]} [description]
*/
AssetSchema.statics.overrideSettings = (id, settings) => Asset.update({id}, {
AssetSchema.statics.overrideSettings = (id, settings) => Asset.findOneAndUpdate({id}, {
$set: {
settings
}
}, {
new: true
});
/**
+5 -3
View File
@@ -287,11 +287,13 @@ CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.u
* @param {String} action the new action to the comment
* @return {Promise}
*/
CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.insertUserAction({
CommentSchema.statics.addAction = (item_id, user_id, action_type, field, detail) => Action.insertUserAction({
item_id,
item_type: 'comment',
item_type: 'comments',
user_id,
action_type
action_type,
field,
detail
});
/**
+69
View File
@@ -3,6 +3,9 @@ const uuid = require('uuid');
const _ = require('lodash');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Action = require('./action');
const Comment = require('./comment');
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
// through during the salting process.
@@ -14,6 +17,12 @@ const USER_ROLES = [
'moderator'
];
// USER_STATUSES is the list of statuses that are permitted for the user status.
const USER_STATUS = [
'active',
'banned'
];
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
// set the process.env.TALK_SESSION_SECRET.
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
@@ -76,6 +85,9 @@ const UserSchema = new mongoose.Schema({
// user.
roles: [String],
// Status provides a string that says in which state the account is.
// When the account is banned, the user login is disabled.
status: {type: String, enum: USER_STATUS, default: 'active'},
// User's settings
settings: {
bio: {
@@ -399,6 +411,47 @@ UserService.removeRoleFromUser = (id, role) => {
});
};
/**
* Set status of a user.
* @param {String} id id of a user
* @param {String} status status to set
* @param {String} comment_id id of the comment that the user was ban for.
* @param {Function} done callback after the operation is complete
*/
UserService.setStatus = (id, status, comment_id) => {
// Check to see if the user status is in the allowable set of roles.
if (USER_STATUS.indexOf(status) === -1) {
// User status is not supported! Error out here.
return Promise.reject(new Error(`status ${status} is not supported`));
}
// If ban then reject the comment and update status
if (status === 'banned') {
return UserModel.update({
id: id
}, {
$set: {
status: status
}
})
.then(() => {
return Comment.pushStatus(comment_id, 'rejected', id);
});
}
if (status === 'active') {
return UserModel.update({
id: id
}, {
$set: {
status: status
}
});
}
};
/**
* Finds a user with the id.
* @param {String} id user id (uuid)
@@ -544,3 +597,19 @@ UserService.addBio = (id, bio) => (
new: true
})
);
/**
* Add an action to the user.
* @param {String} item_id identifier of the user (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the user
* @return {Promise}
*/
UserService.addAction = (item_id, user_id, action_type, field, detail) => Action.insertUserAction({
item_id,
item_type: 'users',
user_id,
action_type,
field,
detail
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+1
View File
@@ -0,0 +1 @@
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36" version="6.0.1.3" editor="www.draw.io" type="device"><diagram name="Pre-Moderation">7VlNc9sgEP01nmkP9ejD+vAxdpP20M6kk0PbI5GIRIqEilFs99d3EWAJSc4krp24nebgEQ9Y4O1bdqVM/GWx+cBRlX9mKaYTz0k3E//9xPOiuQO/EtgqIPAiBWScpApyW+CG/MIa1POymqR4ZQ0UjFFBKhtMWFniRFgY4pyt7WF3jNqrVijDA+AmQXSIfiWpyBUaB06Lf8Qky83KrqN7blHyI+OsLvV6E8+/a/5Ud4GMLT1+laOUrTuQfznxl5wxoZ6KzRJTSa2hTc272tO72zfHpXjShEDNeEC0xmbLzcbE1pCxzonANxVKZHsN/p74i1wUFFouPGoDmAu82bsLd3c2kAxmBRZ8C0PMBKMXLRfXmFi35LuG/LxDvB/MtdO1w7Od7fbQ8KDPPc6BN6AgYUUht+6FFBZapOQBHjP5+KbE67cGB8OdrgFvsAroFRqL0zPo2QR6Ro9dAr0RAnfgnxAYP4dAVFWcPeD0HFmcOTaNvjt7QRrnz6GR43u4+P4OGt3wJdXouiNXmuLojkkyO7SEP2tmOt6tmjR0AQPcqNq0nS2nysptH6gNcM3xuwJyIUeCsLLjmbo/BbCBGcDUBve4EBwibF+tBGc/8JJRxgEpWSn9e0co7UGIkqyEZgIOxIAvpHsJpLsL3VGQNKX7xHEERfi2IOIRPcxH9HAUOQwTGk4h2esm4yJnGSsRvWzRRZPBsbTg2IzjDRHfJDyNAt38rkcBFXzb7ZNt07kSiIsLWZlIN1C0WpHEwFeE7syXqRmkXQeI7pdm7rEQW10uoVowgNoDfGKsGhUGFCFO87frMUVNG/CSk8edCxSymidmlCnLEM+w6GJDEXBMISAebPt/5NNwEOKfO2HnfKkx4P9Q/HhOPA2sEJqHwxByghOFUDRyo/ZjqlVuK++9VD+uSBNkbjfCpoEVY1aAqYA7TMjxiI7jcWd0uB4rRg32ZLnrFa4ZadLSeCkXxD0Pqs3rSd3Svmcn6mXhmd8zpI48MNSoYXfqpwlkWAEOBHL6siOcvepbhDPgYGnKN+dGcIyKf+lGCkOb7PlISndOlNK9YYV3RVGWkTJrvgk0rK/gMWXwUzLpgVuuekWOC/2eLltMQfLVvJtAfkICmQ7cBS/rFW58IF6mkA56IRyOCHq0jnaOIWj//G79qeNE9s3vx6EBrjEncEoZDk1+OCwfeCP5IDijfBAeng9sO6dLB97sHJQzi2Ld7gtjXxVxeBFhPqN1VTP/L5pnieYJXyJft8g8rjzOqcY8WB5+78PtLDqWPqDZfgVXw9v/NPiXvwE=</diagram><diagram name="Post-Moderation">3VlNc6M4EP01rpo9xIXEh/Ex9iYzh52qbOWwu0cNKJgZQB4hYnt+/TRGAiRBQhLbmZ0cUqgltcTr160nPHPX+f4jJ9vNZxbTbIadeD9z/5xhvFg68L82HBqDjxeNIeFp3JhQZ7hPf1BplPOSKo1pqQ0UjGUi3erGiBUFjYRmI5yznT7sgWX6qluSUMtwH5HMtv6TxmLTWEPf6eyfaJps1MrIkT1fSPQt4awq5Hoz7D4c/5runChfcny5ITHb9Uzuzcxdc8ZE85Tv1zSroVWwNfNuR3rbfXNaiCkTcDPhkWQVVTs+7kscFBa7TSro/ZZEdXsH4Z65q43IM2gheJQOKBd0P7oJ1L4aMIaynAp+gCFyAlboSbbgULZ3HfYokLZND3dXBYTIeCet7+6d4UG+9jAErgVBxPK83joOMlhoFaeP8JjUjx8KuvtD2cFxr8vCDVYBukJjdX4EXR3AFqw+gHgAwNb4FgC9lwBItlvOHmn8K6LoYV+D0cOXhDEYSMUGogdWY9lDJfheMdVxVR6r5zUMQIvtvuvsIG28fDENlTLcsVJc5VDDOREpK3qRqcw5YLP8gK3Z4UgIISBCj1UpOPtG1yxjHCwFK+r4PqRZZphIliYFNCMIIAX7qg5vCmX6WnbkaRxnY+Q4ASMCPa+G6tJygA+noMPCToUYzijZZFxsWMIKkt101tXx4KG1A0cHnO5T8W9tni982fxPjgIk+KHfV7dVZykIF9f1gVpHISNlmUbKfJtmrfsiVoNk5MAi+2s3X6kQB3nKk0owMHUv8Bdj20FewNnpHP/aHnUWd/leY6LFtmQVj6iWUbDZhArN9JKzyR2kwFtCG1qZ/rmXfM7fFQX7b5RFAOJcr6zYD+xMcvzzZNLyeY3TI3DH8lGonyamyjU0dzzcT7a5H7qyfUd5Cu9RR0NPwitn7mA9DedhM602mPMmJIFnJwFyXhpBd6kXQk+B2oufP1AIn9JncrE7lh5Pt70ecHVxMPOteTU5qS9sDT+BoSl9z3DUAGI5OpKpBWASvxSc7yuiA9/QgMrFc+LFQyfIMYQsDNZKBDr3glOS/04VLTCEwXJAGDhnEgbIvrBcpp7ptUyTDnrBOhawCaXJPUVpQp4eChxeqDQFry1NC8OPe77K5NnMOIWI7GtI9L+UiRa/dGZim5l4mIWcZqDbHnXvb0pv+x54m5EkgYhgp62p73pLNhIuGNByZ7slo8X7VD/Hqn6vY5YqcFrRC4YBn1jBJlNwuBwZ346C5SvLWmj4OaPgshX9E1+cOP1KI/GLfnFy9XuRf9EPd3iCcL1MLrVKAulKAqHwhVef59Jv4JvASGW/TPaZFytTDEzNPlMGuaY6OV36YVvrX4Q2cKGGzO9TJ3SUEhm9UBvS9PWFe0ASNHVoXJkaV1B34JZwGmVqruSZtXcyi/yRLZ+BRfidWDT5GnNSrnhPUwUZAXTORhVzJdPFZKoEI1t+M1Wg2f0A2gzvfmR2b34C</diagram></mxfile>
+2 -9
View File
@@ -82,18 +82,11 @@ router.post('/:asset_id/scrape', (req, res, next) => {
});
router.put('/:asset_id/settings', (req, res, next) => {
// Override the settings for the asset.
Asset
.overrideSettings(req.params.asset_id, req.body)
.then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
.then(() => res.status(204).end())
.catch((err) => next(err));
});
router.put('/:asset_id/status', (req, res, next) => {
+4 -3
View File
@@ -144,7 +144,6 @@ router.delete('/:comment_id', authorization.needed('admin'), (req, res, next) =>
});
router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next) => {
const {
status
} = req.body;
@@ -162,11 +161,13 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
router.post('/:comment_id/actions', (req, res, next) => {
const {
action_type
action_type,
field,
detail
} = req.body;
Comment
.addAction(req.params.comment_id, req.user.id, action_type)
.addAction(req.params.comment_id, req.user.id, action_type, field, detail)
.then((action) => {
res.status(201).json(action);
})
+1 -1
View File
@@ -16,7 +16,7 @@ router.use('/actions', authorization.needed(), require('./actions'));
router.use('/auth', require('./auth'));
router.use('/stream', require('./stream'));
router.use('/user', require('./user'));
router.use('/users', require('./users'));
// Bind the kue handler to the /kue path.
router.use('/kue', authorization.needed('admin'), require('../../kue').kue.app);
@@ -27,7 +27,6 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
User.count()
])
.then(([result, count]) => {
res.json({
result,
limit: Number(limit),
@@ -48,6 +47,15 @@ router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) =>
.catch(next);
});
router.post('/:user_id/status', (req, res, next) => {
User
.setStatus(req.params.user_id, req.body.status, req.body.comment_id)
.then(status => {
res.json(status);
})
.catch(next);
});
router.post('/', (req, res, next) => {
const {email, password, displayName} = req.body;
@@ -150,4 +158,21 @@ router.put('/:user_id/bio', (req, res, next) => {
});
});
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
const {
action_type,
field,
detail
} = req.body;
User
.addAction(req.params.user_id, req.user.id, action_type, field, detail)
.then((action) => {
res.status(201).json(action);
})
.catch((err) => {
next(err);
});
});
module.exports = router;
@@ -155,7 +155,11 @@ describe('itemActions', () => {
describe('postAction', () => {
it ('should post an action', () => {
fetchMock.post('*', {id: '456'});
return actions.postAction('abc', 'flag', '123', 'comments')(store.dispatch)
const action = {
action_type: 'flag',
detail: 'Comment smells funny'
};
return actions.postAction('abc', 'comments', action)(store.dispatch)
.then(response => {
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/comments/abc/actions');
expect(response).to.deep.equal({id:'456'});
+88
View File
@@ -1,4 +1,6 @@
const User = require('../../models/user');
const Comment = require('../../models/comment');
const expect = require('chai').expect;
describe('models.User', () => {
@@ -77,4 +79,90 @@ describe('models.User', () => {
});
});
describe('#setStatus', () => {
it('should set the status to active', () => {
return User
.setStatus(mockUsers[0].id, 'active')
.then(() => {
User.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('active');
});
});
});
});
describe('#ban', () => {
let mockComment;
beforeEach(() => {
return Promise.all([
Comment.create([{body: 'testing the comment for that user if it is rejected.', id: mockUsers[0].id}])
])
.then((comment) => {
mockComment = comment;
});
});
it('should set the status to banned', () => {
return User
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
.then(() => {
User.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('banned');
});
});
});
it('should set the comment to rejected', () => {
return User
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
.then(() => {
Comment.findById(mockComment.id)
.then((comment) => {
expect(comment).to.have.property('status')
.and.to.equal('rejected');
});
});
});
it('should still disable and ban the user if there is no comment', () => {
return User
.setStatus(mockUsers[0].id, 'banned', '')
.then(() => {
User.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('banned');
});
});
});
});
describe('#unban', () => {
let mockComment;
beforeEach(() => {
return Promise.all([
Comment.create([{body: 'testing the comment for that user if it is rejected.', id: mockUsers[0].id}])
])
.then((comment) => {
mockComment = comment;
});
});
it('should set the status to active', () => {
return User
.setStatus(mockUsers[0].id, 'active', mockComment.id)
.then(() => {
User.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('active');
});
});
});
});
});
+2 -1
View File
@@ -395,11 +395,12 @@ describe('/api/v1/comments/:comment_id/actions', () => {
return chai.request(app)
.post('/api/v1/comments/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'user_id': '456', 'action_type': 'flag'})
.send({'action_type': 'flag', 'detail': 'Comment is too awesome.'})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('detail', 'Comment is too awesome.');
expect(res.body).to.have.property('item_id', 'abc');
});
});
+44
View File
@@ -0,0 +1,44 @@
const passport = require('../../../passport');
const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
// Setup chai.
chai.should();
chai.use(require('chai-http'));
const User = require('../../../../models/user');
describe('/api/v1/users/:user_id/actions', () => {
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
beforeEach(() => {
return User.createLocalUsers(users);
});
describe('#post', () => {
it('it should update actions', () => {
return chai.request(app)
.post('/api/v1/users/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'action_type': 'flag', 'detail': 'Bio is too awesome.'})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('detail', 'Bio is too awesome.');
expect(res.body).to.have.property('item_id', 'abc');
});
});
});
});
+1 -1
View File
@@ -117,7 +117,7 @@
}
$.ajax({
url: '/api/v1/user/update-password',
url: '/api/v1/users/update-password',
contentType: 'application/json',
method: 'POST',
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})