mirror of
https://github.com/wassname/talk.git
synced 2026-08-02 13:10:23 +08:00
Merge branch 'flag-username' into flag-bio
This commit is contained in:
@@ -3,8 +3,14 @@ A commenting platform from The Coral Project. [https://coralproject.net](https:/
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
### Product Roadmap
|
||||
You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk
|
||||
|
||||
### Local Dependencies
|
||||
Node
|
||||
|
||||
Mongo
|
||||
|
||||
### Getting Started
|
||||
@@ -19,13 +25,19 @@ Runs Talk.
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies.
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name.
|
||||
- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required*) - SMTP port.
|
||||
|
||||
### Running with Docker
|
||||
Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
@@ -37,9 +49,11 @@ Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
`npm run lint`
|
||||
|
||||
### Helpful URLs
|
||||
Bare comment stream: http://localhost:5000/client/coral-embed-stream/
|
||||
Comment stream embedded on sample article: http://localhost:5000/client/coral-embed-stream/samplearticle.html
|
||||
Moderator view: http://localhost:5000/admin/
|
||||
Comment stream: http://localhost:3000/
|
||||
|
||||
Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html
|
||||
|
||||
Moderator view: http://localhost:3000/admin
|
||||
|
||||
### Docs
|
||||
`swagger.yaml`
|
||||
|
||||
@@ -94,7 +94,9 @@ app.use((req, res, next) => {
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
if (app.get('env') !== 'test') {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const mongoose = require('../mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const util = require('../util');
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
+68
-9
@@ -34,6 +34,7 @@ function createUser(options) {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
displayName: options.name,
|
||||
role: options.role
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +63,11 @@ function createUser(options) {
|
||||
name: 'displayName',
|
||||
description: 'Display Name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
description: 'User Role',
|
||||
required: false
|
||||
}
|
||||
], (err, result) => {
|
||||
if (err) {
|
||||
@@ -76,15 +82,21 @@ function createUser(options) {
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim());
|
||||
})
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
return User
|
||||
.addRoleToUser(user.id, result.role.trim())
|
||||
.then(() => {
|
||||
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
|
||||
util.shutdown();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,6 +219,7 @@ function listUsers() {
|
||||
'Display Name',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
@@ -217,6 +230,7 @@ function listUsers() {
|
||||
user.displayName,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
]);
|
||||
});
|
||||
@@ -284,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
|
||||
@@ -330,6 +378,7 @@ program
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
@@ -371,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')
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ 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';
|
||||
@@ -46,3 +47,10 @@ export const setRole = (id, role) => dispatch => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => dispatch => {
|
||||
return coralApi(`/user/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
Textfield,
|
||||
Checkbox
|
||||
} from 'react-mdl';
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'pre' ? 'post' : 'pre';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
const infoBoxEnable = !infoBox;
|
||||
updateSettings({infoBoxEnable});
|
||||
};
|
||||
|
||||
const updateInfoBoxContent = (updateSettings) => (event) => {
|
||||
const infoBoxContent = event.target.value;
|
||||
updateSettings({infoBoxContent});
|
||||
};
|
||||
|
||||
const updateClosedMessage = (updateSettings) => (event) => {
|
||||
const closedMessage = event.target.value;
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const CommentSettings = (props) => <List>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateModeration(props.updateSettings, props.settings.moderation)}
|
||||
checked={props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateInfoBoxEnable(props.updateSettings, props.settings.infoBoxEnable)}
|
||||
checked={props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(props.updateSettings)}
|
||||
value={props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<Textfield
|
||||
onChange={updateClosedMessage(props.updateSettings)}
|
||||
value={props.settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>;
|
||||
|
||||
export default CommentSettings;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -45,6 +45,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color:#4caf50;
|
||||
}
|
||||
|
||||
.copiedText {
|
||||
color: #008000;
|
||||
float: right;
|
||||
@@ -69,6 +73,19 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
#bannedWordlist {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bannedWordHeader {
|
||||
font-weight: bold;
|
||||
font-size:18px;
|
||||
margin-bottom:3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -5,126 +5,94 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
Textfield,
|
||||
Checkbox,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {activeSection: 'comments', copied: false};
|
||||
|
||||
this.copyToClipBoard = this.copyToClipBoard.bind(this);
|
||||
|
||||
// Update settings
|
||||
this.updateModeration = this.updateModeration.bind(this);
|
||||
// InfoBox has two settings. Enable or not and the content of it if it is enable.
|
||||
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
|
||||
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
|
||||
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.state = {
|
||||
activeSection: 'comments',
|
||||
wordlist: [],
|
||||
changed: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount = () => {
|
||||
this.props.dispatch(fetchSettings());
|
||||
}
|
||||
|
||||
updateModeration () {
|
||||
const moderation = this.props.settings.moderation === 'pre' ? 'post' : 'pre';
|
||||
this.props.dispatch(updateSettings({moderation}));
|
||||
}
|
||||
|
||||
updateInfoBoxEnable () {
|
||||
const infoBoxEnable = !this.props.settings.infoBoxEnable;
|
||||
this.props.dispatch(updateSettings({infoBoxEnable}));
|
||||
}
|
||||
|
||||
updateInfoBoxContent (event) {
|
||||
const infoBoxContent = event.target.value;
|
||||
this.props.dispatch(updateSettings({infoBoxContent}));
|
||||
}
|
||||
|
||||
saveSettings () {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
}
|
||||
|
||||
getCommentSettings () {
|
||||
return <List>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateModeration}
|
||||
checked={this.props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateInfoBoxEnable}
|
||||
checked={this.props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={this.updateInfoBoxContent}
|
||||
value={this.props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>;
|
||||
}
|
||||
|
||||
copyToClipBoard () {
|
||||
const copyTextarea = document.querySelector(`.${ styles.embedInput}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.setState({copied: true});
|
||||
} catch (err) {
|
||||
console.error('Unable to copy', err);
|
||||
componentWillUpdate = (newProps) => {
|
||||
if ((!this.props.settings
|
||||
|| !this.props.settings.wordlist)
|
||||
&& newProps.settings.wordlist
|
||||
&& newProps.settings.wordlist.length !== 0 ) {
|
||||
this.setState({wordlist: newProps.settings.wordlist.join(', ')});
|
||||
}
|
||||
}
|
||||
|
||||
getEmbed () {
|
||||
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>;
|
||||
saveSettings = () => {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection (activeSection) {
|
||||
changeSection = (activeSection) => () => {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
onChangeWordlist = (event) => {
|
||||
event.preventDefault();
|
||||
const newlist = event.target.value;
|
||||
this.setState({wordlist: newlist.toLowerCase(), changed: true});
|
||||
this.props.dispatch(updateSettings({
|
||||
wordlist: newlist.toLowerCase()
|
||||
.split(',')
|
||||
.map((word) => word.trim())
|
||||
}));
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
}
|
||||
|
||||
getSection = (section) => {
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}/>;
|
||||
case 'embed':
|
||||
return <EmbedLink/>;
|
||||
case 'wordlist':
|
||||
return <Wordlist
|
||||
wordlist={this.state.wordlist}
|
||||
onChangeWordlist={this.onChangeWordlist}/>;
|
||||
}
|
||||
}
|
||||
|
||||
getPageTitle = (section) => {
|
||||
switch(section) {
|
||||
case 'comments':
|
||||
return lang.t('configure.comment-settings');
|
||||
case 'embed':
|
||||
return lang.t('configure.embed-comment-stream');
|
||||
case 'wordlist':
|
||||
return lang.t('configure.wordlist');
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let pageTitle = this.state.activeSection === 'comments'
|
||||
? lang.t('configure.comment-settings')
|
||||
: lang.t('configure.embed-comment-stream');
|
||||
let pageTitle = this.getPageTitle(this.state.activeSection);
|
||||
const section = this.getSection(this.state.activeSection);
|
||||
|
||||
if (this.props.fetchingSettings) {
|
||||
pageTitle += ' - Loading...';
|
||||
@@ -136,28 +104,41 @@ class Configure extends React.Component {
|
||||
<List>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'comments')}
|
||||
onClick={this.changeSection('comments')}
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'embed')}
|
||||
onClick={this.changeSection('embed')}
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('wordlist')}
|
||||
icon='settings'>{lang.t('configure.wordlist')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button raised colored onClick={this.saveSettings}>
|
||||
<Icon name='save' /> {lang.t('configure.save-changes')}
|
||||
{
|
||||
this.state.changed ?
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
className={styles.changedSave}>
|
||||
<Icon name='check' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
: <Button
|
||||
raised
|
||||
disabled>
|
||||
{lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<h1>{pageTitle}</h1>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{
|
||||
this.state.activeSection === 'comments'
|
||||
? this.getCommentSettings()
|
||||
: this.getEmbed()
|
||||
}
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, {Component} from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Button
|
||||
} from 'react-mdl';
|
||||
|
||||
class EmbedLink extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {copied: false};
|
||||
}
|
||||
|
||||
copyToClipBoard = () => {
|
||||
const copyTextarea = document.querySelector(`.${styles.embedInput}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.setState({copied: true});
|
||||
} catch (err) {
|
||||
console.error('Unable to copy', err);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbedLink;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
Card
|
||||
} from 'react-mdl';
|
||||
|
||||
const Wordlist = ({wordlist, onChangeWordlist}) => <Card id={styles.bannedWordlist} shadow={2}>
|
||||
<p className={styles.bannedWordHeader}>{lang.t('configure.banned-word-header')}</p>
|
||||
<p className={styles.bannedWordText}>{lang.t('configure.banned-word-text')}</p>
|
||||
<textarea
|
||||
rows={5}
|
||||
type='text'
|
||||
className={styles.bannedWordInput}
|
||||
onChange={onChangeWordlist}
|
||||
value={wordlist}/>
|
||||
</Card>;
|
||||
|
||||
export default Wordlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -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) {
|
||||
@@ -81,16 +96,24 @@ class ModerationQueue extends React.Component {
|
||||
singleView={singleView}
|
||||
commentIds={
|
||||
comments.get('ids')
|
||||
.filter(id => !comments.get('byId')
|
||||
.filter(id =>
|
||||
comments
|
||||
.get('byId')
|
||||
.get(id)
|
||||
.get('status'))
|
||||
.get('status') === 'premod')
|
||||
}
|
||||
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'}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,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(`/user/${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}));
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
@@ -37,11 +46,23 @@
|
||||
"include-text": "Include your text here.",
|
||||
"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.",
|
||||
"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",
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community"
|
||||
"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": {
|
||||
@@ -51,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",
|
||||
@@ -62,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",
|
||||
@@ -71,11 +101,23 @@
|
||||
"include-text": "Incluir tu texto aqui.",
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"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",
|
||||
"configure": "Configurar",
|
||||
"community": "Comunidad"
|
||||
"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);
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
itemActions,
|
||||
Notification,
|
||||
notificationActions,
|
||||
authActions,
|
||||
configActions
|
||||
authActions
|
||||
} from '../../coral-framework';
|
||||
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
@@ -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} = this.props.config;
|
||||
const {status, moderation, closedMessage} = this.props.config;
|
||||
const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
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,6 +116,7 @@ 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}
|
||||
@@ -130,10 +125,13 @@ class CommentStream extends Component {
|
||||
id={rootItemId}
|
||||
premod={moderation}
|
||||
reply={false}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}
|
||||
author={user}
|
||||
/>
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>Comments are closed for this thread.</p>
|
||||
: <p>{closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset} />}
|
||||
{
|
||||
@@ -148,7 +146,9 @@ class CommentStream extends Component {
|
||||
<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,7 +158,8 @@ 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">
|
||||
<FlagComment
|
||||
@@ -171,6 +172,7 @@ class CommentStream extends Component {
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
commentId={commentId}
|
||||
@@ -185,6 +187,7 @@ class CommentStream extends Component {
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
premod={moderation}
|
||||
currentUser={user}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.children &&
|
||||
@@ -199,6 +202,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}
|
||||
@@ -209,19 +214,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">
|
||||
<FlagComment
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
author_id={comment.author_id}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
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}
|
||||
@@ -238,6 +245,8 @@ class CommentStream extends Component {
|
||||
parent_id={commentId}
|
||||
child_id={replyId}
|
||||
premod={moderation}
|
||||
banned={banned}
|
||||
currentUser={user}
|
||||
showReply={reply.showReply}/>
|
||||
</div>;
|
||||
})
|
||||
@@ -259,12 +268,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}
|
||||
@@ -298,8 +308,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
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);
|
||||
|
||||
@@ -1,18 +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)));
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
@@ -197,7 +192,7 @@ export function postItem (item, type, id) {
|
||||
return coralApi(`/${type}`, {method: 'POST', body: item})
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}, type));
|
||||
return json.id;
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -1,30 +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'
|
||||
status: 'open',
|
||||
moderation: null
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch(action.type) {
|
||||
// Override config if worked
|
||||
case actions.UPDATE_SETTINGS:
|
||||
return state.merge(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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,16 +39,22 @@ class CommentBox extends Component {
|
||||
related = 'comments';
|
||||
parent_type = 'assets';
|
||||
}
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
if (child_id || parent_id) {
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then((comment_id) => {
|
||||
if (premod === 'pre') {
|
||||
addNotification('success', lang.t('comment-post-notif-premod'));
|
||||
} else {
|
||||
appendItemArray(parent_id || id, related, comment_id, !parent_id, parent_type);
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
})
|
||||
.then((postedComment) => {
|
||||
const commentId = postedComment.id;
|
||||
const status = postedComment.status;
|
||||
if (status[0] && status[0].type === 'rejected') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
} else if (premod === 'pre') {
|
||||
addNotification('success', lang.t('comment-post-notif-premod'));
|
||||
} else {
|
||||
appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
this.setState({body: ''});
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
"comment": "Comment",
|
||||
"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-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 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-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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class FlagButton extends Component {
|
||||
itemType: '',
|
||||
detail: '',
|
||||
otherText: '',
|
||||
step: 1
|
||||
step: 0,
|
||||
posted: false
|
||||
}
|
||||
|
||||
// When the "report" button is clicked expand the menu
|
||||
@@ -29,14 +30,17 @@ class FlagButton extends Component {
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
|
||||
const {itemType, field, detail, step, otherText} = this.state;
|
||||
const {itemType, field, detail, step, otherText, posted} = this.state;
|
||||
|
||||
//Proceed to the next step
|
||||
this.setState({step: step + 1});
|
||||
//Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.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) {
|
||||
|
||||
if (itemType && detail && !posted) {
|
||||
// Set the text from the "other" field if it exists.
|
||||
const updatedDetail = otherText || detail;
|
||||
let item_id;
|
||||
@@ -58,11 +62,11 @@ class FlagButton extends Component {
|
||||
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, action.item_type);
|
||||
this.setState({posted: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When a popup option is clicked, update the state
|
||||
onPopupOptionClick = (sets) => (e) => {
|
||||
|
||||
// If the "other" option is clicked, show the other textbox
|
||||
@@ -89,7 +93,7 @@ class FlagButton extends Component {
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const flagged = flag && flag.current_user;
|
||||
const popupMenu = getPopupMenu(this.state.step, this.state.itemType);
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={this.onReportClick} className={`${name}-button`}>
|
||||
@@ -143,7 +147,7 @@ class FlagButton extends Component {
|
||||
</form>
|
||||
}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step} of 3
|
||||
{this.state.step + 1} of {this.getPopupMenu.length}
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
|
||||
@@ -5,9 +5,8 @@ import translations from './translations.json';
|
||||
|
||||
const FlagComment = (props) => <FlagButton {...props} getPopupMenu={getPopupMenu} />;
|
||||
|
||||
const getPopupMenu = (step, itemType) => {
|
||||
switch(step) {
|
||||
case 1: {
|
||||
const getPopupMenu = [
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-1-header'),
|
||||
options: [
|
||||
@@ -17,8 +16,8 @@ const getPopupMenu = (step, itemType) => {
|
||||
button: lang.t('continue'),
|
||||
sets: 'itemType'
|
||||
};
|
||||
}
|
||||
case 2: {
|
||||
},
|
||||
(itemType) => {
|
||||
const options = itemType === 'comments' ?
|
||||
[
|
||||
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
|
||||
@@ -38,14 +37,15 @@ const getPopupMenu = (step, itemType) => {
|
||||
button: lang.t('continue'),
|
||||
sets: 'detail'
|
||||
};
|
||||
}
|
||||
case 3: {
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-3-header'),
|
||||
text: lang.t('thank-you')
|
||||
text: lang.t('thank-you'),
|
||||
button: lang.t('done'),
|
||||
};
|
||||
}}
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
export default FlagComment;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"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",
|
||||
@@ -24,13 +25,14 @@
|
||||
"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": "¡traduceme!",
|
||||
"report-notif-remove": "Tu marca ha sido eliminada.",
|
||||
"step-1-header": "¡traduceme!",
|
||||
"step-2-header": "¡traduceme!",
|
||||
"step-3-header": "¡traduceme!",
|
||||
"flag-username": "¡traduceme!",
|
||||
"flag-comment": "¡traduceme!",
|
||||
"continue": "¡traduceme!",
|
||||
"done": "¡traduceme!",
|
||||
"no-agree-comment": "¡traduceme!",
|
||||
"comment-offensive": "¡traduceme!",
|
||||
"personal-info": "¡traduceme!",
|
||||
@@ -38,7 +40,7 @@
|
||||
"no-like-username": "¡traduceme!",
|
||||
"marketing": "¡traduceme!",
|
||||
"thank-you": "¡traduceme!",
|
||||
"flag-reason": "Reason for flag",
|
||||
"flag-reason": "¡traduceme!",
|
||||
"other": "¡traduceme!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +12,9 @@ const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDial
|
||||
showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
if (banned) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
const action = {
|
||||
action_type: 'like'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -77,6 +77,16 @@
|
||||
background: #696969;
|
||||
}
|
||||
|
||||
.type--green {
|
||||
color: white;
|
||||
background: #00897B;
|
||||
}
|
||||
|
||||
.type--green:hover {
|
||||
color: white;
|
||||
background: #00a291;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -8,3 +8,4 @@ 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';
|
||||
|
||||
@@ -7,6 +7,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "2525:2525"
|
||||
environment:
|
||||
- "TALK_PORT=5000"
|
||||
- "TALK_MONGO_URL=mongodb://mongo"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// The maximum depth to recurse into nested objects checking for mongoose
|
||||
// objects.
|
||||
const maxRecursion = 3;
|
||||
|
||||
/**
|
||||
* Middleware to wrap the `res.json` function to ensure that we can filter the
|
||||
* payload response first based on user and role.
|
||||
*/
|
||||
module.exports = (req, res, next) => {
|
||||
/**
|
||||
* Updates the original document based on filtering out for roles.
|
||||
* @param {Mixed} o original object to be modified
|
||||
* @param {Integer} l current level of depth in the first object
|
||||
* @return {Mixed} (possibly) modified object
|
||||
*/
|
||||
const wrap = (o, d = 0) => {
|
||||
if (d > maxRecursion) {
|
||||
return o;
|
||||
}
|
||||
|
||||
// If this is an array, we need to walk over all the object's elements.
|
||||
if (Array.isArray(o)) {
|
||||
|
||||
// Map each of the array elements.
|
||||
return o.map((ob) => wrap(ob, d + 1));
|
||||
|
||||
} else if (o && o.constructor && o.constructor.name === 'model') {
|
||||
|
||||
// The object here is definitly a mongoose model.
|
||||
|
||||
// Check to see if it has a `filterForUser` method.
|
||||
if (typeof o.filterForUser === 'function') {
|
||||
|
||||
// The object here actually has the `filterForUser` function, so filter
|
||||
// the object!
|
||||
o = o.filterForUser(req.user);
|
||||
}
|
||||
|
||||
} else if (typeof o === 'object') {
|
||||
|
||||
// Iterate over the props, find only properties owned by the object.
|
||||
for (let prop in o) {
|
||||
|
||||
// If and only if the object owns the property do we actually pull the
|
||||
// property out.
|
||||
if (typeof o.hasOwnProperty === 'function' && o.hasOwnProperty(prop)) {
|
||||
|
||||
// Wrap the property with one more layer down.
|
||||
o[prop] = wrap(o[prop], d + 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
// Save a reference to the original json function.
|
||||
const json = res.json;
|
||||
|
||||
// Override the original json function.
|
||||
res.json = (payload) => {
|
||||
|
||||
// Restore the old pointer.
|
||||
res.json = json;
|
||||
|
||||
// Send it down the pipe after we've filtered it.
|
||||
res.json(wrap(payload));
|
||||
};
|
||||
|
||||
// Now that we've overridden the `res.json`, let's hand it down.
|
||||
next();
|
||||
};
|
||||
+1
-9
@@ -37,15 +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, field, detail}) => {
|
||||
const action = {
|
||||
item_id,
|
||||
item_type,
|
||||
user_id,
|
||||
action_type,
|
||||
field,
|
||||
detail
|
||||
};
|
||||
ActionSchema.statics.insertUserAction = (action) => {
|
||||
|
||||
// Create/Update the action.
|
||||
return Action.findOneAndUpdate(action, action, {
|
||||
|
||||
+20
-7
@@ -29,6 +29,14 @@ const AssetSchema = new Schema({
|
||||
type: Schema.Types.Mixed,
|
||||
default: null
|
||||
},
|
||||
closedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
closedMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
title: String,
|
||||
description: String,
|
||||
image: String,
|
||||
@@ -36,11 +44,7 @@ const AssetSchema = new Schema({
|
||||
subsection: String,
|
||||
author: String,
|
||||
publication_date: Date,
|
||||
modified_date: Date,
|
||||
status: {
|
||||
type: String,
|
||||
default: 'open'
|
||||
}
|
||||
modified_date: Date
|
||||
}, {
|
||||
versionKey: false,
|
||||
timestamps: {
|
||||
@@ -60,6 +64,13 @@ AssetSchema.index({
|
||||
background: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns true if the asset is closed, false else.
|
||||
*/
|
||||
AssetSchema.virtual('isClosed').get(function() {
|
||||
return this.closedAt && this.closedAt.getTime() <= new Date().getTime();
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an asset by its id.
|
||||
* @param {String} id identifier of the asset (uuid).
|
||||
@@ -85,7 +96,7 @@ AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
return Object.assign({}, settings, asset.settings);
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
@@ -121,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
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+33
-30
@@ -1,5 +1,6 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const Action = require('./action');
|
||||
|
||||
@@ -45,7 +46,7 @@ const CommentSchema = new Schema({
|
||||
},
|
||||
asset_id: String,
|
||||
author_id: String,
|
||||
status: [StatusSchema],
|
||||
status_history: [StatusSchema],
|
||||
parent_id: String
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -59,22 +60,7 @@ const CommentSchema = new Schema({
|
||||
* output.
|
||||
*/
|
||||
CommentSchema.options.toJSON = {};
|
||||
CommentSchema.options.toJSON.hide = '_id status';
|
||||
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* toJSON overrides to remove fields from the json
|
||||
* output.
|
||||
*/
|
||||
CommentSchema.options.toJSON = {};
|
||||
CommentSchema.options.toJSON.virtuals = true;
|
||||
CommentSchema.options.toJSON.hide = '_id';
|
||||
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
@@ -86,14 +72,31 @@ CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
CommentSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'body', 'asset_id', 'author_id', 'parent_id', 'status']);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up a virtual getter function on a comment such that when you try and
|
||||
* access the `comment.last_status` it returns the last status in the array
|
||||
* of status's on the comment, or `null` if there was no status.
|
||||
* of status's on the comment, or `null` if there was no status_history.
|
||||
*/
|
||||
CommentSchema.virtual('last_status').get(function() {
|
||||
if (this.status && this.status.length > 0) {
|
||||
return this.status[this.status.length - 1].type;
|
||||
CommentSchema.virtual('status').get(function() {
|
||||
|
||||
// Here we are taking advantage of the fact that when documents are inserted
|
||||
// for the new status on a comment that they are always appended to the end
|
||||
// of the list in the order that they are inserted, hence, the last status
|
||||
// is always the most recent.
|
||||
if (this.status_history && this.status_history.length > 0) {
|
||||
return this.status_history[this.status_history.length - 1].type;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,7 +126,7 @@ CommentSchema.statics.publicCreate = (comment) => {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status: status ? [{
|
||||
status_history: status ? [{
|
||||
type: status,
|
||||
created_at: new Date()
|
||||
}] : [],
|
||||
@@ -157,7 +160,7 @@ CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
|
||||
*/
|
||||
CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
|
||||
asset_id,
|
||||
'status.type': 'accepted'
|
||||
'status_history.type': 'accepted'
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -169,10 +172,10 @@ CommentSchema.statics.findAcceptedAndNewByAssetId = (asset_id) => Comment.find({
|
||||
asset_id,
|
||||
$or: [
|
||||
{
|
||||
'status.type': 'accepted'
|
||||
'status_history.type': 'accepted'
|
||||
},
|
||||
{
|
||||
status: {
|
||||
status_history: {
|
||||
$size: 0
|
||||
}
|
||||
}
|
||||
@@ -202,7 +205,7 @@ CommentSchema.statics.findIdsByActionType = (action_type) => Action
|
||||
.then((actions) => actions.map(a => a.item_id));
|
||||
|
||||
/**
|
||||
* Find comments by their status.
|
||||
* Find comments by their status_history.
|
||||
* @param {String} status the status of the comment to search for
|
||||
* @return {Promise}
|
||||
*/
|
||||
@@ -210,9 +213,9 @@ CommentSchema.statics.findByStatus = (status = false) => {
|
||||
let q = {};
|
||||
|
||||
if (status) {
|
||||
q['status.type'] = status;
|
||||
q['status_history.type'] = status;
|
||||
} else {
|
||||
q.status = {$size: 0};
|
||||
q.status_history = {$size: 0};
|
||||
}
|
||||
|
||||
return Comment.find(q);
|
||||
@@ -269,7 +272,7 @@ CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
|
||||
*/
|
||||
CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
|
||||
$push: {
|
||||
status: {
|
||||
status_history: {
|
||||
type: status,
|
||||
created_at: new Date(),
|
||||
assigned_by
|
||||
@@ -286,7 +289,7 @@ CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.u
|
||||
*/
|
||||
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,
|
||||
field,
|
||||
|
||||
+49
-9
@@ -28,6 +28,16 @@ const SettingSchema = new Schema({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
closedTimeout: {
|
||||
type: Number,
|
||||
|
||||
// Two weeks default expiry.
|
||||
default: 60 * 60 * 24 * 7 * 2
|
||||
},
|
||||
closedMessage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
wordlist: [String]
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -36,6 +46,42 @@ const SettingSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* toJSON provides settings overrides to this object's serialization methods.
|
||||
*/
|
||||
SettingSchema.options.toJSON = {};
|
||||
SettingSchema.options.toJSON.virtuals = true;
|
||||
|
||||
/**
|
||||
* Merges two settings objects.
|
||||
*/
|
||||
SettingSchema.method('merge', function(src) {
|
||||
SettingSchema.eachPath((path) => {
|
||||
|
||||
// Exclude internal fields...
|
||||
if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the source object contains the path, shallow copy it.
|
||||
if (path in src) {
|
||||
this[path] = src[path];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
SettingSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['moderation', 'infoBoxEnable', 'infoBoxContent']);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo Mongoose object.
|
||||
*/
|
||||
@@ -62,7 +108,9 @@ const EXPIRY_TIME = 60 * 2;
|
||||
* Gets the entire settings record and sends it back
|
||||
* @return {Promise} settings the whole settings record
|
||||
*/
|
||||
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => Setting.findOne(selector));
|
||||
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => {
|
||||
return Setting.findOne(selector);
|
||||
}).then((setting) => new Setting(setting));
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
@@ -83,14 +131,6 @@ SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
|
||||
.then(() => settings);
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the document to ensure that the resulting document is indeed ready
|
||||
* for non authenticated users.
|
||||
* @param {Object} settings the source settings object
|
||||
* @return {Object} the filtered settings object
|
||||
*/
|
||||
SettingService.public = (settings) => _.pick(settings, ['moderation', 'infoBoxEnable', 'infoBoxContent']);
|
||||
|
||||
/**
|
||||
* This is run once when the app starts to ensure settings are populated.
|
||||
* @return {Promise} null initialize the global settings object
|
||||
|
||||
+62
-12
@@ -1,9 +1,12 @@
|
||||
const mongoose = require('../mongoose');
|
||||
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.
|
||||
const SALT_ROUNDS = 10;
|
||||
@@ -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: {
|
||||
@@ -106,7 +118,8 @@ UserSchema.index({
|
||||
* output.
|
||||
*/
|
||||
UserSchema.options.toJSON = {};
|
||||
UserSchema.options.toJSON.hide = '_id password profiles roles disabled';
|
||||
UserSchema.options.toJSON.hide = '_id password';
|
||||
UserSchema.options.toJSON.virtuals = true;
|
||||
UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
@@ -118,20 +131,16 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* toObject overrides to remove the password field from the toObject
|
||||
* output.
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
UserSchema.options.toObject = {};
|
||||
UserSchema.options.toObject.hide = 'password';
|
||||
UserSchema.options.toObject.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
UserSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'displayName', 'settings', 'created_at', 'updated_at']);
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
@@ -402,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)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -80,7 +80,20 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id))
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(new Error('asset referenced is not found'));
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new Error(`asset has commenting closed because: ${asset.closedMessage}`));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
@@ -95,9 +108,8 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
author_id: req.user.id
|
||||
}))
|
||||
.then((comment) => {
|
||||
|
||||
// The comment was created! Send back the created comment.
|
||||
res.status(201).send(comment);
|
||||
res.status(201).json(comment);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
@@ -132,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;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
const express = require('express');
|
||||
const authorization = require('../../middleware/authorization');
|
||||
const payloadFilter = require('../../middleware/payload-filter');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Filter all content going down the pipe based on user roles.
|
||||
router.use(payloadFilter);
|
||||
|
||||
router.use('/asset', authorization.needed('admin'), require('./asset'));
|
||||
router.use('/settings', authorization.needed('admin'), require('./settings'));
|
||||
router.use('/queue', authorization.needed('admin'), require('./queue'));
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
const scraper = require('../../../services/scraper');
|
||||
const url = require('url');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
const Setting = require('../../../models/setting');
|
||||
const ErrInvalidAssetURL = new Error('asset_url is invalid');
|
||||
ErrInvalidAssetURL.status = 400;
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
|
||||
let asset_url = decodeURIComponent(req.query.asset_url);
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
return next(ErrInvalidAssetURL);
|
||||
}
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
// Find or create the asset by url.
|
||||
Asset.findOrCreateByUrl(decodeURIComponent(req.query.asset_url))
|
||||
Asset.findOrCreateByUrl(asset_url)
|
||||
|
||||
// Add the found asset to the scraper if it's not already scraped.
|
||||
.then((asset) => {
|
||||
@@ -34,7 +45,7 @@ router.get('/', (req, res, next) => {
|
||||
// Merge the asset specific settings with the returned settings object in
|
||||
// the event that the asset that was returned also had settings.
|
||||
if (asset && asset.settings) {
|
||||
settings = Object.assign({}, settings, asset.settings);
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
// Fetch the appropriate comments stream.
|
||||
@@ -98,7 +109,7 @@ router.get('/', (req, res, next) => {
|
||||
comments,
|
||||
users,
|
||||
actions,
|
||||
settings: Setting.public(settings)
|
||||
settings
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
+30
-30
@@ -26,26 +26,14 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
.limit(limit),
|
||||
User.count()
|
||||
])
|
||||
.then(([data, count]) => {
|
||||
const users = data.map((user) => {
|
||||
const {id, displayName, created_at} = user;
|
||||
return {
|
||||
id,
|
||||
displayName,
|
||||
created_at,
|
||||
profiles: user.toObject().profiles,
|
||||
roles: user.toObject().roles
|
||||
};
|
||||
});
|
||||
|
||||
.then(([result, count]) => {
|
||||
res.json({
|
||||
result: users,
|
||||
result,
|
||||
limit: Number(limit),
|
||||
count,
|
||||
page: Number(page),
|
||||
totalPages: Math.ceil(count / limit)
|
||||
totalPages: Math.ceil(count / (limit === 0 ? 1 : limit))
|
||||
});
|
||||
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
@@ -53,8 +41,17 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
|
||||
User
|
||||
.addRoleToUser(req.params.user_id, req.body.role)
|
||||
.then(role => {
|
||||
res.send(role);
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.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);
|
||||
});
|
||||
@@ -65,13 +62,17 @@ router.post('/', (req, res, next) => {
|
||||
User
|
||||
.createLocalUser(email, password, displayName)
|
||||
.then(user => {
|
||||
res.status(201).send(user);
|
||||
|
||||
res.status(201).json(user);
|
||||
})
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
|
||||
ErrPasswordTooShort.status = 400;
|
||||
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
@@ -81,7 +82,7 @@ router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return res.status(400).send('Password must be at least 8 characters');
|
||||
return next(ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
@@ -93,7 +94,8 @@ router.post('/update-password', (req, res, next) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(401).send('Not Authorized');
|
||||
|
||||
next(authorization.ErrNotAuthorized);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,10 +135,8 @@ router.post('/request-password-reset', (req, res, next) => {
|
||||
// if we fail on missing emails, it would reveal if people are registered or not.
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
|
||||
res.status(500).json({error: errorMsg});
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,15 +150,15 @@ router.put('/:user_id/bio', (req, res, next) => {
|
||||
|
||||
User
|
||||
.addBio(user_id, bio)
|
||||
.then(user => res.status(200).send(user))
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
res.status(500).json({error: errorMsg});
|
||||
.then(user => {
|
||||
res.json(user);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
|
||||
console.log('Hit action endpoint');
|
||||
const {
|
||||
action_type,
|
||||
field,
|
||||
@@ -166,7 +166,7 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
|
||||
} = req.body;
|
||||
|
||||
User
|
||||
.addAction(req.params.comment_id, req.user.id, action_type, field, detail)
|
||||
.addAction(req.params.user_id, req.user.id, action_type, field, detail)
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
})
|
||||
|
||||
+4
-8
@@ -3,7 +3,7 @@ const nodemailer = require('nodemailer');
|
||||
const smtpRequiredProps = [
|
||||
'TALK_SMTP_USERNAME',
|
||||
'TALK_SMTP_PASSWORD',
|
||||
'TALK_SMTP_PROVIDER'
|
||||
'TALK_SMTP_HOST'
|
||||
];
|
||||
|
||||
smtpRequiredProps.forEach(prop => {
|
||||
@@ -13,9 +13,7 @@ smtpRequiredProps.forEach(prop => {
|
||||
});
|
||||
|
||||
const options = {
|
||||
// list of providers here:
|
||||
// https://github.com/nodemailer/nodemailer-wellknown#supported-services
|
||||
service: process.env.TALK_SMTP_PROVIDER,
|
||||
host: process.env.TALK_SMTP_HOST,
|
||||
auth: {
|
||||
user: process.env.TALK_SMTP_USERNAME,
|
||||
pass: process.env.TALK_SMTP_PASSWORD
|
||||
@@ -24,10 +22,8 @@ const options = {
|
||||
|
||||
if (process.env.TALK_SMTP_PORT) {
|
||||
options.port = process.env.TALK_SMTP_PORT;
|
||||
}
|
||||
|
||||
if (process.env.TALK_SMTP_HOST) {
|
||||
options.host = process.env.TALK_SMTP_HOST;
|
||||
} else {
|
||||
options.port = 25;
|
||||
}
|
||||
|
||||
const defaultTransporter = nodemailer.createTransport(options);
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ const scraper = {
|
||||
title: `Scrape for asset ${asset.id}`,
|
||||
asset_id: asset.id
|
||||
})
|
||||
.attempts(10)
|
||||
.attempts(3)
|
||||
.delay(1000)
|
||||
.backoff({type: 'exponential'})
|
||||
.save((err) => {
|
||||
@@ -106,7 +106,7 @@ const scraper = {
|
||||
|
||||
// Handle errors that occur.
|
||||
.catch((err) => {
|
||||
console.error(`Failed to scrape on Job[${job.id}] for Asset[${job.data.asset_id}]:`, err);
|
||||
debug(`Failed to scrape on Job[${job.id}] for Asset[${job.data.asset_id}]:`, err);
|
||||
|
||||
done(err);
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('itemActions', () => {
|
||||
body: JSON.stringify(item.data)
|
||||
}
|
||||
);
|
||||
expect(id).to.equal('123');
|
||||
expect(id).to.deep.equal({id: '123'});
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: {
|
||||
|
||||
+20
-24
@@ -11,14 +11,14 @@ describe('models.Comment', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
asset_id: '123',
|
||||
status: [],
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '1'
|
||||
}, {
|
||||
body: 'comment 20',
|
||||
asset_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -27,14 +27,14 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [],
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '3'
|
||||
}, {
|
||||
body: 'comment 40',
|
||||
asset_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -43,7 +43,7 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
asset_id: '1234',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -52,7 +52,7 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
asset_id: '1234',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -99,8 +99,7 @@ describe('models.Comment', () => {
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.have.length(1);
|
||||
expect(c.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status).to.be.equal('accepted');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,17 +115,15 @@ describe('models.Comment', () => {
|
||||
}]).then(([c1, c2, c3]) => {
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.id).to.be.uuid;
|
||||
expect(c1.status).to.have.length(1);
|
||||
expect(c1.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c1.status).to.be.equal('accepted');
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.have.length(0);
|
||||
expect(c2.status).to.be.null;
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
expect(c3.status).to.have.length(1);
|
||||
expect(c3.status[0]).to.have.property('type', 'rejected');
|
||||
expect(c3.status).to.be.equal('rejected');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,17 +217,16 @@ describe('models.Comment', () => {
|
||||
|
||||
return Comment.findById(comment_id)
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(0);
|
||||
expect(c.status).to.be.null;
|
||||
|
||||
return Comment.pushStatus(comment_id, 'rejected', '123');
|
||||
})
|
||||
.then(() => Comment.findById(comment_id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(1);
|
||||
expect(c.status[0]).to.have.property('type', 'rejected');
|
||||
expect(c.status[0]).to.have.property('assigned_by', '123');
|
||||
expect(c.status_history).to.have.length(1);
|
||||
expect(c.status_history[0]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,13 +234,13 @@ describe('models.Comment', () => {
|
||||
return Comment.pushStatus(comments[1].id, 'rejected', '123')
|
||||
.then(() => Comment.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(2);
|
||||
expect(c.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status[0]).to.have.property('assigned_by', null);
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
expect(c.status[1]).to.have.property('type', 'rejected');
|
||||
expect(c.status[1]).to.have.property('assigned_by', '123');
|
||||
expect(c.status_history[1]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,4 +39,18 @@ describe('models.Setting', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#merge', () => {
|
||||
it('should merge a settings object and its overrides', () => {
|
||||
return Setting
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
let ovrSett = {moderation: 'post'};
|
||||
|
||||
settings.merge(ovrSett);
|
||||
|
||||
expect(settings).to.have.property('moderation', 'post');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ const settings = {id: '1', moderation: 'pre'};
|
||||
|
||||
describe('/api/v1/comments', () => {
|
||||
|
||||
// Ensure that the settings are always available.
|
||||
beforeEach(() => Promise.all([
|
||||
wordlist.insert(['bad words']),
|
||||
Setting.init(settings)
|
||||
]));
|
||||
|
||||
describe('#get', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
@@ -32,13 +38,13 @@ describe('/api/v1/comments', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
@@ -75,11 +81,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
return Action.create(actions);
|
||||
}),
|
||||
User.createLocalUsers(users),
|
||||
wordlist.insert([
|
||||
'bad words'
|
||||
]),
|
||||
Setting.init(settings)
|
||||
User.createLocalUsers(users)
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -142,11 +144,19 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
describe('#post', () => {
|
||||
|
||||
let asset_id;
|
||||
|
||||
beforeEach(() => Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
|
||||
|
||||
// Update the asset id.
|
||||
asset_id = asset.id;
|
||||
}));
|
||||
|
||||
it('should create a comment', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
@@ -157,12 +167,11 @@ describe('/api/v1/comments', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status').and.to.have.length(1);
|
||||
expect(res.body.status[0]).to.have.property('type', 'rejected');
|
||||
expect(res.body).to.have.property('status', 'rejected');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,10 +193,46 @@ describe('/api/v1/comments', () => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('asset_id');
|
||||
expect(res.body).to.have.property('status').and.to.have.length(1);
|
||||
expect(res.body.status[0]).to.have.property('type', 'premod');
|
||||
expect(res.body).to.have.property('status', 'premod');
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t create a comment when the asset has expired commenting', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(0),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(500);
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.response.body).to.not.be.null;
|
||||
expect(err.response.body).to.have.property('message');
|
||||
expect(err.response.body.message).to.contain('tests said expired!');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment when the asset has not expired yet', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(32),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,20 +346,20 @@ describe('/api/v1/comments/:comment_id/actions', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: []
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
@@ -350,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
@@ -29,14 +29,14 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('/api/v1/stream', () => {
|
||||
body: 'comment 10',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
@@ -33,21 +33,21 @@ describe('/api/v1/stream', () => {
|
||||
body: 'comment 20',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: []
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'uio',
|
||||
body: 'comment 30',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
parent_id: '',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 40',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}];
|
||||
@@ -116,6 +116,16 @@ describe('/api/v1/stream', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject requests without a scheme in the asset_url', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: 'test.com'})
|
||||
.catch((err) => {
|
||||
expect(err).to.have.status(400);
|
||||
expect(err.response.body.message).to.contain('asset_url is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the settings when the asset contains settings to override it with', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
@@ -126,6 +136,7 @@ describe('/api/v1/stream', () => {
|
||||
expect(res.body.comments.length).to.equal(1);
|
||||
expect(res.body.users.length).to.equal(1);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
expect(res.body.settings).to.not.have.property('wordlist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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/user/: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/user/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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user