Merge branch 'master' into suspect-wordlist

This commit is contained in:
Wyatt Johnson
2016-12-15 13:51:16 -07:00
75 changed files with 858 additions and 252 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"sourceMaps": true,
"presets": [
"stage-0",
"es2015-minimal"
"es2015"
],
"plugins": [
["transform-decorators-legacy"],
+1
View File
@@ -1,2 +1,3 @@
dist
client/lib
**/*.html
+2 -2
View File
@@ -3,10 +3,10 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const passport = require('./passport');
const passport = require('./services/passport');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./redis');
const redis = require('./services/redis');
const app = express();
+1 -1
View File
@@ -15,7 +15,7 @@ const pkg = require('../package.json');
const parseDuration = require('parse-duration');
const Table = require('cli-table');
const Asset = require('../models/asset');
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
+2 -2
View File
@@ -13,8 +13,8 @@ process.env.DEBUG = process.env.TALK_DEBUG;
const program = require('commander');
const scraper = require('../services/scraper');
const util = require('../util');
const mongoose = require('../mongoose');
const kue = require('../kue');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
util.onshutdown([
() => mongoose.disconnect()
+1 -1
View File
@@ -11,7 +11,7 @@ const debug = require('debug')('talk:server');
const http = require('http');
const init = require('../init');
const scraper = require('../services/scraper');
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const util = require('../util');
/**
+1 -1
View File
@@ -11,7 +11,7 @@ process.env.DEBUG = process.env.TALK_DEBUG;
*/
const program = require('commander');
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const Setting = require('../models/setting');
const util = require('../util');
+1 -1
View File
@@ -14,7 +14,7 @@ const program = require('commander');
const pkg = require('../package.json');
const prompt = require('prompt');
const User = require('../models/user');
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const util = require('../util');
const Table = require('cli-table');
+4 -1
View File
@@ -26,7 +26,10 @@ export const updateSettings = settings => {
};
export const saveSettingsToServer = () => (dispatch, getState) => {
const settings = getState().settings.toJS().settings;
let settings = getState().settings.toJS().settings;
if (settings.charCount) {
settings.charCount = parseInt(settings.charCount);
}
dispatch({type: SAVE_SETTINGS_LOADING});
coralApi('/settings', {method: 'PUT', body: settings})
.then(() => {
+15 -12
View File
@@ -14,18 +14,18 @@ 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'));
let authorStatus = author.status;
const links = linkify.getMatches(comment.body);
return (
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<i className={`material-icons ${styles.avatar}`}>person</i>
<span>{author.get('displayName') || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
<span>{author.displayName || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
</div>
<div>
{links ?
@@ -42,7 +42,7 @@ export default props => {
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
{comment.get('body')}
{comment.body}
</Linkify>
</span>
</div>
@@ -52,9 +52,10 @@ export default props => {
// 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');
const {comment, author} = props;
const status = comment.status;
const flagged = comment.flagged;
const banned = (author.status === 'banned');
if (action === 'flag' && (status || flagged === true)) {
return null;
@@ -64,17 +65,19 @@ const getActionButton = (action, i, props) => {
<Button
disabled={banned ? 'disabled' : ''}
cStyle='black'
onClick={() => props.onClickShowBanDialog(props.author.get('id'), props.author.get('displayName'), props.comment.get('id'))}
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
key={i} >
{lang.t('comment.ban_user')}
</Button>
);
}
return (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
<FabButton
className={styles.actionButton}
icon={props.actionsMap[action].icon}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.id)}
/>
);
};
@@ -37,7 +37,7 @@ export default class CommentList extends React.Component {
// If entering to singleview and no active, active is the first eleement
componentWillReceiveProps (nextProps) {
if (nextProps.singleView && !this.state.active) {
this.setState({active: nextProps.commentIds.get(0)});
this.setState({active: nextProps.commentIds[0]});
}
}
@@ -81,12 +81,12 @@ export default class CommentList extends React.Component {
const {commentIds} = this.props;
const {active} = this.state;
// check boundaries
if (active === null || !commentIds.size) {
this.setState({active: commentIds.get(0)});
} else if (direction === 'up' && active !== commentIds.first()) {
this.setState({active: commentIds.get(commentIds.indexOf(active) - 1)});
} else if (direction === 'down' && active !== commentIds.last()) {
this.setState({active: commentIds.get(commentIds.indexOf(active) + 1)});
if (active === null || !commentIds.length) {
this.setState({active: commentIds[0]});
} else if (direction === 'up' && active !== commentIds[0]) {
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
}
// scroll to the position
@@ -105,10 +105,10 @@ export default class CommentList extends React.Component {
// activate the next comment
if (id === this.state.active) {
const {commentIds} = this.props;
if (commentIds.last() === this.state.active) {
this.setState({active: commentIds.get(commentIds.size - 2)});
if (commentIds[commentIds.length - 1] === this.state.active) {
this.setState({active: commentIds[commentIds.length - 2]});
} else {
this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))});
this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]});
}
}
this.props.onClickAction(action, id, author_id);
@@ -125,10 +125,10 @@ export default class CommentList extends React.Component {
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
{commentIds.map((commentId, index) => {
const comment = comments.get(commentId);
const comment = comments[commentId];
const author = users[comment.author_id];
return <Comment comment={comment}
author={users.get(comment.get('author_id'))}
ref={el => { if (el && commentId === active) { this._active = el; } }}
author={author}
key={index}
index={index}
onClickAction={this.onClickAction}
@@ -137,7 +137,7 @@ export default class CommentList extends React.Component {
actionsMap={actions}
isActive={commentId === active}
hideActive={hideActive} />;
}).toArray()}
})}
</ul>
);
}
@@ -46,9 +46,9 @@ class CommentStream extends React.Component {
<CommentBox onSubmit={this.onSubmit} />
<CommentList isActive hideActive
singleView={false}
commentIds={comments.get('ids')}
comments={comments.get('byId')}
users={users.get('byId')}
commentIds={comments.ids}
comments={comments.byId}
users={users.byId}
onClickAction={this.onClickAction}
actions={['flag']}
loading={comments.loading} />
@@ -58,4 +58,9 @@ class CommentStream extends React.Component {
}
}
export default connect(({comments, users}) => ({comments, users}))(CommentStream);
const mapStateToProps = state => ({
comments: state.comments.toJS(),
users: state.users.toJS()
});
export default connect(mapStateToProps)(CommentStream);
@@ -8,9 +8,25 @@ import {
ListItemContent,
ListItemAction,
Textfield,
Checkbox
Checkbox,
Icon
} from 'react-mdl';
const updateCharCountEnable = (updateSettings, charCountChecked) => () => {
const charCountEnable = !charCountChecked;
updateSettings({charCountEnable});
};
const updateCharCount = (updateSettings, settingsError) => (event) => {
const charCount = event.target.value;
if (charCount.match(/[^0-9]/) || charCount.length === 0) {
settingsError('charCount', true);
} else {
settingsError('charCount', false);
}
updateSettings({charCount: charCount});
};
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'pre' ? 'post' : 'pre';
updateSettings({moderation});
@@ -31,48 +47,80 @@ const updateClosedMessage = (updateSettings) => (event) => {
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')}
const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <List>
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</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>;
</ListItem>
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</ListItemContent>
</ListItem>
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={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} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={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(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>;
export default CommentSettings;
@@ -18,9 +18,27 @@
.configSetting {
border: 1px solid #ccc;
border-radius: 4px;
height: 90px;
height: 95px;
margin-bottom: 10px;
cursor: pointer;
align-items: flex-start;
}
.settingsError {
color: #d50000;
}
.settingsError i {
font-size: 14px;
margin-right: 3px;
}
.settingsHeader {
margin-top: 3px;
margin-bottom: 10px;
}
.disabledSettingText {
color: #ccc;
}
.configSettingInfoBox {
@@ -45,6 +63,22 @@
display: block;
}
.charCountTexfield {
width: 4em;
padding: 0px;
border-color: #ccc;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
.charCountTexfieldEnabled {
border-color: #4caf50;
}
.charCountTexfield:focus {
outline: none;
}
.changedSave {
background-color:#4caf50;
}
@@ -84,7 +118,15 @@
margin-bottom:3px;
}
.enabledSetting {
border-left-color: #4caf50;
border-left-style: solid;
border-left-width: 7px;
}
.disabledSetting {
padding-left: 22px;
}
.hidden {
display: none;
@@ -22,7 +22,8 @@ class Configure extends React.Component {
this.state = {
activeSection: 'comments',
wordlist: [],
changed: false
changed: false,
errors: {}
};
}
@@ -64,12 +65,23 @@ class Configure extends React.Component {
this.props.dispatch(updateSettings(setting));
}
// Sets an arbitrary error string and a boolean state.
// This allows the system to track multiple errors.
onSettingError = (error, state) => {
this.setState((prevState) => {
prevState.errors[error] = state;
return prevState;
});
}
getSection = (section) => {
switch(section){
case 'comments':
return <CommentSettings
settings={this.props.settings}
updateSettings={this.onSettingUpdate}/>;
updateSettings={this.onSettingUpdate}
errors={this.state.errors}
settingsError={this.onSettingError}/>;
case 'embed':
return <EmbedLink/>;
case 'wordlist':
@@ -94,6 +106,9 @@ class Configure extends React.Component {
let pageTitle = this.getPageTitle(this.state.activeSection);
const section = this.getSection(this.state.activeSection);
const showSave = Object.keys(this.state.errors).reduce(
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
if (this.props.fetchingSettings) {
pageTitle += ' - Loading...';
}
@@ -119,7 +134,7 @@ class Configure extends React.Component {
</ListItem>
</List>
{
this.state.changed ?
showSave ?
<Button
raised
onClick={this.saveSettings}
@@ -79,6 +79,10 @@ class ModerationQueue extends React.Component {
const {comments, users} = this.props;
const {activeTab, singleView, modalOpen} = this.state;
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected');
const flaggedIds = comments.ids.filter(id => comments.byId[id].flagged === true);
return (
<div>
<div className='mdl-tabs mdl-js-tabs mdl-js-ripple-effect'>
@@ -94,41 +98,26 @@ class ModerationQueue extends React.Component {
<CommentList
isActive={activeTab === 'pending'}
singleView={singleView}
commentIds={
comments.get('ids')
.filter(id =>
comments
.get('byId')
.get(id)
.get('status') === 'premod')
}
comments={comments.get('byId')}
users={users.get('byId')}
commentIds={premodIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, commentId) => this.onCommentAction(action, commentId)}
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
actions={['reject', 'approve', 'ban']}
loading={comments.loading} />
<BanUserDialog
open={comments.get('showBanUserDialog')}
open={comments.showBanUserDialog}
handleClose={() => this.hideBanUserDialog()}
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
user={comments.get('banUser')}/>
user={comments.banUser}/>
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<CommentList
isActive={activeTab === 'rejected'}
singleView={singleView}
commentIds={
comments
.get('ids')
.filter(id =>
comments
.get('byId')
.get(id)
.get('status') === 'rejected')
}
comments={comments.get('byId')}
users={users.get('byId')}
commentIds={rejectedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['approve']}
loading={comments.loading} />
@@ -137,12 +126,9 @@ class ModerationQueue extends React.Component {
<CommentList
isActive={activeTab === 'rejected'}
singleView={singleView}
commentIds={comments.get('ids').filter(id => {
const data = comments.get('byId').get(id);
return !data.get('status') && data.get('flagged') === true;
})}
comments={comments.get('byId')}
users={users.get('byId')}
commentIds={flaggedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, id) => this.onCommentAction(action, id)}
actions={['reject', 'approve']}
loading={comments.loading} />
@@ -155,6 +141,11 @@ class ModerationQueue extends React.Component {
}
}
export default connect(({comments, users}) => ({comments, users}))(ModerationQueue);
const mapStateToProps = state => ({
comments: state.comments.toJS(),
users: state.users.toJS()
});
export default connect(mapStateToProps)(ModerationQueue);
const lang = new I18n(translations);
+12 -2
View File
@@ -41,6 +41,7 @@
},
"configure": {
"enable-pre-moderation": "Enable pre-moderation",
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
@@ -55,7 +56,11 @@
"configure": "Configure",
"community": "Community",
"closed-comments-desc": "Write a message for closed threads",
"closed-comments-label": "Write a message..."
"closed-comments-label": "Write a message...",
"comment-count-header": "Limit Comment Length",
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number."
},
"bandialog": {
"ban_user": "Ban User?",
@@ -96,6 +101,7 @@
},
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
"include-text": "Incluir tu texto aqui.",
@@ -110,7 +116,11 @@
"configure": "Configurar",
"community": "Comunidad",
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
"closed-comments-label": "Escribe un mensaje..."
"closed-comments-label": "Escribe un mensaje...",
"comment-count-header": "Limitar el largo del comentario",
"comment-count-text-pre": "El largo de comentarios será ",
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido."
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
+1 -1
View File
@@ -3,7 +3,7 @@
"configureCommentStream": {
"apply": "Apply",
"title": "Configure Comment Stream",
"description": "As an admin you may customize the settings for the comment stream for this article",
"description": "As an admin you may customize the settings for the comment stream for this asset",
"enablePremod": "Enable Premoderation",
"enablePremodDescription": "Moderators must approve any comment before its published.",
"enablePremodLinks": "Pre-Moderate Comments Containing Links",
@@ -90,7 +90,7 @@ class CommentStream extends Component {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {actions, users, comments} = this.props.items;
const {status, moderation, closedMessage} = this.props.config;
const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const {activeTab} = this.state;
const banned = (this.props.userData.status === 'banned');
@@ -128,7 +128,7 @@ class CommentStream extends Component {
currentUser={this.props.auth.user}
banned={banned}
author={user}
/>
charCount={charCountEnable && charCount}/>
</RestrictedContent>
</div>
: <p>{closedMessage}</p>
@@ -198,6 +198,7 @@ class CommentStream extends Component {
parent_id={commentId}
premod={moderation}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={comment.showReply}/>
{
comment.children &&
@@ -257,6 +258,7 @@ class CommentStream extends Component {
premod={moderation}
banned={banned}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={reply.showReply}/>
</div>;
})
+11 -1
View File
@@ -95,7 +95,7 @@ hr {
margin-top: 10px;
}
#coralStream .coral-plugin-commentbox-button {
.coral-plugin-commentbox-button {
float: right;
margin-top: 10px;
padding: 5px 10px;
@@ -111,6 +111,16 @@ hr {
margin-bottom: 5px;
}
.coral-plugin-commentbox-char-count {
color: #ccc;
text-align: right;
font-size: 12px;
}
.coral-plugin-commentbox-char-max {
color: #d50000;
}
/* Comment styles */
.comment {
margin-bottom: 10px;
+29 -9
View File
@@ -6,14 +6,6 @@ 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' ? 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});
@@ -24,10 +16,38 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
.toArray()[0];
dispatch(updateConfigRequest());
coralApi(`/asset/${assetId}/settings`, {method: 'PUT', body: newConfig})
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
dispatch(updateConfigSuccess(newConfig));
})
.catch(error => dispatch(updateConfigFailure(error)));
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
dispatch(updateConfigRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
dispatch(updateConfigSuccess(closedBody));
})
.catch(error => dispatch(updateConfigFailure(error)));
};
const openStream = () => ({type: actions.OPEN_COMMENTS});
const closeStream = () => ({type: actions.CLOSE_COMMENTS});
export const updateOpenStatus = status => dispatch => {
if (status === 'open') {
dispatch(openStream());
dispatch(updateOpenStream({closedAt: null}));
} else {
dispatch(closeStream());
dispatch(updateOpenStream({closedAt: new Date().getTime()}));
}
};
+4
View File
@@ -2,6 +2,10 @@ import coralApi from '../helpers/response';
import {fromJS} from 'immutable';
import {UPDATE_CONFIG} from '../constants/config';
/**
* Action name constants
*/
export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
+29
View File
@@ -1,5 +1,7 @@
import * as actions from '../constants/user';
import * as assetActions from '../constants/assets';
import {addNotification} from '../actions/notification';
import {addItem} from '../actions/items';
import coralApi from '../helpers/response';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -19,3 +21,30 @@ export const saveBio = (user_id, formData) => dispatch => {
})
.catch(error => dispatch(saveBioFailure(error)));
};
/**
*
* Get a list of comments by a single user
*
* @param {string} user_id
* @returns Promise
*/
export const fetchCommentsByUserId = userId => {
return (dispatch) => {
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
return coralApi(`/comments?user_id${userId}`)
.then(({comments, assets}) => {
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
assets.forEach(asset => dispatch(addItem(asset, 'assets')));
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
})
.catch(error => {
console.error(error.stack);
console.error('FAILURE_COMMENTS_BY_USER', error);
dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error});
});
};
};
@@ -0,0 +1,3 @@
export const MULTIPLE_ASSETS_REQUEST = 'MULTIPLE_ASSETS_REQUEST';
export const MULTIPLE_ASSETS_SUCCESS = 'MULTIPLE_ASSETS_SUCCESS';
export const MULTIPLE_ASSSETS_FAILURE = 'MULTIPLE_ASSSETS_FAILURE';
+3
View File
@@ -1,3 +1,6 @@
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST';
export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS';
export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE';
+1 -1
View File
@@ -22,7 +22,7 @@ export default (state = initialState, action) => {
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 && action.item.closedAt && new Date(action.item.closedAt).getTime() <= new Date().getTime()) ? 'closed' : 'open') : state;
default:
return state;
}
+1
View File
@@ -6,6 +6,7 @@ import * as actions from '../actions/items';
const initialState = fromJS({
comments: {},
users: {},
assets: {},
actions: {}
});
+9 -2
View File
@@ -1,11 +1,14 @@
import {Map} from 'immutable';
import {Map, fromJS} from 'immutable';
import * as authActions from '../constants/auth';
import * as actions from '../constants/user';
import * as assetActions from '../constants/assets';
const initialState = Map({
displayName: '',
profiles: [],
settings: {}
settings: {},
myComments: [],
myAssets: [] // the assets from which myComments (above) originated
});
const purge = user => {
@@ -30,6 +33,10 @@ export default function user (state = initialState, action) {
case actions.SAVE_BIO_SUCCESS:
return state
.set('settings', action.settings);
case actions.COMMENTS_BY_USER_SUCCESS:
return state.set('myComments', fromJS(action.comments));
case assetActions.MULTIPLE_ASSETS_SUCCESS:
return state.set('myAssets', fromJS(action.assets));
default :
return state;
}
@@ -1,20 +1,23 @@
import React from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import has from 'lodash/has';
import reduce from 'lodash/reduce';
const name = 'coral-plugin-comment-count';
const CommentCount = ({items, id}) => {
let count = 0;
if (items.assets[id] && items.assets[id].comments) {
if (has(items, `assets.${id}.comments`)) {
count += items.assets[id].comments.length;
}
const itemKeys = Object.keys(items.comments);
for (let i = 0; i < itemKeys.length; i++) {
const item = items.comments[itemKeys[i]];
if (item.children) {
count += item.children.length;
// lodash reduce works on {}
count += reduce(items.comments, (accum, comment) => {
if (comment.children) {
accum += comment.children.length;
}
}
return accum;
}, 0);
return <div className={`${name}-text`}>
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
+26 -6
View File
@@ -23,7 +23,18 @@ class CommentBox extends Component {
}
postComment = () => {
const {postItem, updateItem, id, parent_id, child_id, addNotification, appendItemArray, premod, author} = this.props;
const {
postItem,
updateItem,
id,
parent_id,
child_id,
addNotification,
appendItemArray,
premod,
author
} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
@@ -42,11 +53,14 @@ class CommentBox extends Component {
if (child_id || parent_id) {
updateItem(child_id || parent_id, 'showReply', false, 'comments');
}
if (this.props.charCount && this.state.body.length > this.props.charCount) {
return;
}
postItem(comment, 'comments')
.then((postedComment) => {
const commentId = postedComment.id;
const status = postedComment.status;
if (status[0] && status[0].type === 'rejected') {
if (postedComment.status === 'rejected') {
addNotification('error', lang.t('comment-post-banned-word'));
} else if (premod === 'pre') {
addNotification('success', lang.t('comment-post-notif-premod'));
@@ -60,8 +74,8 @@ class CommentBox extends Component {
}
render () {
const {styles, reply, author} = this.props;
// How to handle language in plugins? Should we have a dependency on our central translation file?
const {styles, reply, author, charCount} = this.props;
const length = this.state.body.length;
return <div>
<div
className={`${name}-container`}>
@@ -80,10 +94,16 @@ class CommentBox extends Component {
onChange={(e) => this.setState({body: e.target.value})}
rows={3}/>
</div>
<div className={`${name}-char-count ${length > charCount ? `${name}-char-max` : ''}`}>
{
charCount &&
`${charCount - length} ${lang.t('characters-remaining')}`
}
</div>
<div className={`${name}-button-container`}>
{ author && (
<Button
cStyle='darkGrey'
cStyle={length > charCount ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
onClick={this.postComment}>
{lang.t('post')}
@@ -6,7 +6,8 @@
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
"comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
"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.",
"characters-remaining": " characters remaining"
},
"es": {
"post": "Publicar",
@@ -15,6 +16,7 @@
"name": "Nombre",
"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."
"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.",
"characters-remaining": "carácteres restantes"
}
}
+8
View File
@@ -0,0 +1,8 @@
.assetURL {
font-size: 16px;
color: black;
}
.commentBody {
}
+25
View File
@@ -0,0 +1,25 @@
import React, {PropTypes} from 'react';
import styles from './Comment.css';
const Comment = props => {
return (
<div>
<p className="myCommentAsset">
<a className={`${styles.assetURL} myCommentAnchor`} href={props.asset.url}>{props.asset.url}</a>
</p>
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
</div>
);
};
Comment.propTypes = {
comment: PropTypes.shape({
body: PropTypes.string
}).isRequired,
asset: PropTypes.shape({
url: PropTypes.string
}).isRequired
};
export default Comment;
@@ -0,0 +1,27 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import styles from './CommentHistory.css';
const CommentHistory = props => {
return (
<div className={`${styles.header} commentHistory`}>
<h2>All Comments</h2>
<div className="commentHistory__list">
{props.comments.map((comment, i) => {
const asset = props.assets.find(asset => asset.id === comment.asset_id);
return <Comment
key={i}
comment={comment}
asset={asset} />;
})}
</div>
</div>
);
};
CommentHistory.propTypes = {
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
assets: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default CommentHistory;
@@ -1,15 +0,0 @@
import React from 'react';
import styles from './CommentHistory.css';
export default ({comments = []}) => (
<div className={styles.header}>
<h1>Comments</h1>
<ul>
{comments.map(() => (
<li>
{/* Comment Data*/}
</li>
))}
</ul>
</div>
);
@@ -1,12 +1,12 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {saveBio} from 'coral-framework/actions/user';
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
import BioContainer from './BioContainer';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent} from '../../coral-ui';
import CommentHistory from '../components/CommentHistory';
import CommentHistory from 'coral-plugin-history/CommentHistory';
import SettingsHeader from '../components/SettingsHeader';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
@@ -22,6 +22,7 @@ class SignInContainer extends Component {
componentWillMount () {
// Fetch commentHistory
this.props.fetchCommentsByUserId(this.props.userData.id);
}
handleTabChange(tab) {
@@ -31,7 +32,7 @@ class SignInContainer extends Component {
}
render() {
const {loggedIn, userData, showSignInDialog} = this.props;
const {loggedIn, userData, showSignInDialog, items, user} = this.props;
const {activeTab} = this.state;
return (
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
@@ -41,7 +42,13 @@ class SignInContainer extends Component {
<Tab>Profile Settings</Tab>
</TabBar>
<TabContent show={activeTab === 0}>
<CommentHistory {...this.props}/>
{
user.myComments.length && user.myAssets.length
? <CommentHistory
comments={user.myComments.map(id => items.comments[id])}
assets={user.myAssets.map(id => items.assets[id])} />
: <p>Loading comment history...</p>
}
</TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
@@ -51,12 +58,14 @@ class SignInContainer extends Component {
}
}
const mapStateToProps = () => ({
const mapStateToProps = state => ({
items: state.items.toJS(),
user: state.user.toJS()
});
const mapDispatchToProps = dispatch => ({
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
getHistory: () => dispatch(),
fetchCommentsByUserId: userId => dispatch(fetchCommentsByUserId(userId))
});
export default connect(
+11
View File
@@ -77,6 +77,17 @@
background: #696969;
}
.type--lightGrey {
color: white;
background: #ccc;
cursor: default;
}
.type--lightGrey:hover {
color: white;
background: #ccc;
}
.type--green {
color: white;
background: #00897B;

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+4 -4
View File
@@ -256,7 +256,7 @@ paths:
description: An error occured.
schema:
$ref: '#/definitions/Error'
/asset:
/assets:
get:
parameters:
- name: limit
@@ -294,7 +294,7 @@ paths:
items:
$ref: '#/definitions/Asset'
/asset/{asset_id}:
/assets/{asset_id}:
get:
parameters:
- name: asset_id
@@ -315,7 +315,7 @@ paths:
$ref: '#/definitions/Error'
/asset/{asset_id}/scrape:
/assets/{asset_id}/scrape:
post:
parameters:
- name: asset_id
@@ -335,7 +335,7 @@ paths:
schema:
$ref: '#/definitions/Error'
/asset/{asset_id}/settings:
/assets/{asset_id}/settings:
put:
parameters:
- name: asset_id
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const Schema = mongoose.Schema;
+19 -9
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const Setting = require('./setting');
@@ -19,7 +19,7 @@ const AssetSchema = new Schema({
},
type: {
type: String,
default: 'article'
default: 'assets'
},
scraped: {
type: Date,
@@ -68,13 +68,6 @@ 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).
@@ -87,6 +80,13 @@ AssetSchema.statics.findById = (id) => Asset.findOne({id});
*/
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
/**
* Returns true if the asset is closed, false else.
*/
AssetSchema.virtual('isClosed').get(function() {
return this.closedAt && this.closedAt.getTime() <= new Date().getTime();
});
/**
* Retrieves the settings given an asset query and rectifies it against the
* global settings.
@@ -156,6 +156,16 @@ AssetSchema.statics.search = (value) => value.length === 0 ? Asset.find({}) : As
}
});
/**
* Finds multiple assets with matching ids
* @param {Array} ids an array of Strings of asset_id
* @return {Promise} resolves to list of Assets
*/
AssetSchema.statics.findMultipleById = function (ids) {
const query = ids.map(id => ({id}));
return Asset.find(query);
};
const Asset = mongoose.model('Asset', AssetSchema);
module.exports = Asset;
+10 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const uuid = require('uuid');
@@ -324,6 +324,15 @@ CommentSchema.statics.removeAction = (item_id, user_id, action_type) => Action.r
*/
CommentSchema.statics.all = () => Comment.find();
/**
* Returns all the comments by user
* probably to be paginated at some point in the future
* @return {Promise} array resolves to an array of comments by that user
*/
CommentSchema.statics.findByUserId = function (author_id) {
return Comment.find({author_id});
};
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
+19 -3
View File
@@ -1,7 +1,7 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const cache = require('../cache');
const cache = require('../services/cache');
const WordlistSchema = new Schema({
banned: [String],
@@ -46,6 +46,14 @@ const SettingSchema = new Schema({
default: ''
},
wordlist: WordlistSchema,
charCount: {
type: Number,
default: 5000
},
charCountEnable: {
type: Boolean,
default: false
}
}, {
timestamps: {
createdAt: 'created_at',
@@ -83,7 +91,15 @@ SettingSchema.method('merge', function(src) {
*/
SettingSchema.method('filterForUser', function(user = false) {
if (!user || !user.roles.includes('admin')) {
return _.pick(this.toJSON(), ['moderation', 'infoBoxEnable', 'infoBoxContent']);
return _.pick(this.toJSON(), [
'moderation',
'infoBoxEnable',
'infoBoxContent',
'closeTimeout',
'closedMessage',
'charCountEnable',
'charCount'
]);
}
return this.toJSON();
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const bcrypt = require('bcrypt');
+28 -14
View File
@@ -5,21 +5,26 @@
"main": "app.js",
"scripts": {
"start": "./bin/cli serve --jobs",
"build": "NODE_ENV=production webpack --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint . --fix",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive tests",
"test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
"pree2e": "NODE_ENV=test ./pree2e.sh",
"e2e": "NODE_ENV=test node_modules/.bin/nightwatch",
"build": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.config.dev.js --watch",
"lint": "./node_modules/.bin/eslint bin/* .",
"lint-fix": "./node_modules/.bin/eslint bin/* . --fix",
"test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
"test-watch": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive -w tests",
"pree2e": "NODE_ENV=test ./scripts/pree2e.sh",
"e2e": "NODE_ENV=test ./node_modules/.bin/nightwatch",
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs"
},
"config": {
"pre-git": {
"commit-msg": [],
"pre-commit": ["npm run lint", "npm test"],
"pre-push": ["npm test"],
"pre-commit": [
"npm run lint",
"npm test"
],
"pre-push": [
"npm test"
],
"post-commit": [],
"post-merge": []
}
@@ -28,7 +33,12 @@
"type": "git",
"url": "git+https://github.com/coralproject/talk.git"
},
"keywords": ["talk", "coral", "coralproject", "ask"],
"keywords": [
"talk",
"coral",
"coralproject",
"ask"
],
"author": "",
"license": "Apache-2.0",
"bugs": {
@@ -59,7 +69,6 @@
"passport-facebook": "^2.1.1",
"passport-local": "^1.0.0",
"prompt": "^1.0.0",
"react-linkify": "^0.1.3",
"redis": "^2.6.3",
"uuid": "^2.0.3"
},
@@ -77,16 +86,17 @@
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-polyfill": "^6.16.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-es2015-minimal": "^2.1.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-http": "^3.0.0",
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.25.0",
"dialog-polyfill": "^0.4.4",
"eslint": "^3.9.1",
"enzyme": "^2.6.0",
"eslint": "^3.12.1",
"eslint-config-postcss": "^2.0.2",
"eslint-config-standard": "^6.2.1",
"eslint-module-utils": "^2.0.0",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-mocha": "^4.7.0",
@@ -96,8 +106,10 @@
"exports-loader": "^0.6.3",
"fetch-mock": "^5.5.0",
"hammerjs": "^2.0.8",
"ignore-styles": "^5.0.1",
"immutable": "^3.8.1",
"imports-loader": "^0.6.5",
"jsdom": "^9.8.3",
"json-loader": "^0.5.4",
"keymaster": "^1.6.2",
"material-design-lite": "^1.2.1",
@@ -112,7 +124,9 @@
"precss": "^1.4.0",
"pym.js": "^1.1.1",
"react": "15.3.2",
"react-addons-test-utils": "15.3.2",
"react-dom": "15.3.2",
"react-linkify": "^0.1.3",
"react-mdl": "^1.7.2",
"react-mdl-selectfield": "^0.2.0",
"react-onclickoutside": "^5.7.1",
-2
View File
@@ -1,2 +0,0 @@
node_modules/selenium-standalone/bin/selenium-standalone install
npm start &
@@ -90,11 +90,28 @@ router.put('/:asset_id/settings', (req, res, next) => {
});
router.put('/:asset_id/status', (req, res, next) => {
// Update the asset status
const id = req.params.asset_id;
const {
closedAt,
closedMessage
} = req.body;
Asset
.update({id: req.params.asset_id}, {status: req.query.status})
.then(() => res.status(204).end())
.catch((err) => next(err));
.update({id}, {
$set: {
closedAt,
closedMessage
}
})
.then(() => {
res.status(204).json();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
+1 -1
View File
@@ -1,5 +1,5 @@
const express = require('express');
const passport = require('../../../passport');
const passport = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const router = express.Router();
+14 -4
View File
@@ -14,7 +14,8 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
const {
status = null,
action_type = null,
asset_id = null
asset_id = null,
user_id = null
} = req.query;
/**
@@ -32,6 +33,8 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
if (status) {
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
} else if (user_id) {
query = Comment.findByUserId(user_id);
} else if (action_type) {
query = Comment
.findIdsByActionType(action_type)
@@ -47,13 +50,15 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
query.then((comments) => {
return Promise.all([
comments,
Asset.findMultipleById(comments.map(comment => comment.asset_id)),
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
]);
})
.then(([comments, users, actions])=>
.then(([comments, assets, users, actions]) =>
res.status(200).json({
comments,
assets,
users,
actions
}))
@@ -87,7 +92,6 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
// 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}`));
}
@@ -97,7 +101,13 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
// Return `premod` if pre-moderation is enabled and an empty "new" status
// in the event that it is not in pre-moderation mode.
.then(({moderation}) => moderation === 'pre' ? 'premod' : false);
.then(({moderation, charCountEnable, charCount}) => {
// Reject if the comment is too long
if (charCountEnable && body.length > charCount) {
return 'rejected';
}
return moderation === 'pre' ? 'premod' : '';
});
}
status.then((status) => Comment.publicCreate({
+2 -2
View File
@@ -7,7 +7,7 @@ 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('/assets', authorization.needed('admin'), require('./assets'));
router.use('/settings', authorization.needed('admin'), require('./settings'));
router.use('/queue', authorization.needed('admin'), require('./queue'));
@@ -19,6 +19,6 @@ router.use('/stream', require('./stream'));
router.use('/users', require('./users'));
// Bind the kue handler to the /kue path.
router.use('/kue', authorization.needed('admin'), require('../../kue').kue.app);
router.use('/kue', authorization.needed('admin'), require('../../services/kue').kue.app);
module.exports = router;
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# install selenium
../node_modules/selenium-standalone/bin/selenium-standalone install
# start the app server
npm start &
View File
View File
+1 -1
View File
@@ -1,5 +1,5 @@
const passport = require('passport');
const User = require('./models/user');
const User = require('../models/user');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
View File
+1 -1
View File
@@ -1,4 +1,4 @@
const kue = require('../kue');
const kue = require('./kue');
const debug = require('debug')('talk:services:scraper');
const Asset = require('../models/asset');
const JOB_NAME = 'scraper';
@@ -0,0 +1,30 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import {expect} from 'chai';
import Comment from '../../../client/coral-plugin-history/Comment';
describe('coral-plugin-history/Comment', () => {
let render;
const comment = {body: 'this is a comment'};
const asset = {url: 'https://google.com'};
beforeEach(() => {
render = shallow(<Comment asset={asset} comment={comment} />);
});
it('should render the provided comment body', () => {
const wrapper = mount(<Comment asset={asset} comment={comment} />);
expect(wrapper.find('.myCommentBody')).to.have.length(1);
expect(wrapper.find('.myCommentBody').text()).to.equal('this is a comment');
});
it('should render the asset url as a link', () => {
const wrapper = mount(<Comment asset={asset} comment={comment} />);
expect(wrapper.find('.myCommentAnchor')).to.have.length(1);
expect(wrapper.find('.myCommentAnchor').text()).to.equal('https://google.com');
});
it('should render the comment with styles', () => {
expect(render.props().style).to.be.defined;
});
});
@@ -0,0 +1,24 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import {expect} from 'chai';
import CommentHistory from '../../../client/coral-plugin-history/CommentHistory';
describe('coral-plugin-history/CommentHistory', () => {
let render;
const comments = [{body: 'a comment or something', 'status_history':[{'type':'premod', 'created_at':'2016-12-09T01:40:53.327Z', 'assigned_by':null}, {'created_at':'2016-12-09T22:52:44.888Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-09T01:40:53.360Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T22:52:44.893Z', 'id':'3962c2ea-4ec4-42e4-b9bd-c571ff30f56b'}, {'body':'another comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-09T22:53:43.148Z', 'assigned_by':null}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-09T22:53:43.158Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'premod', '__v':0, 'updated_at':'2016-12-09T22:53:43.158Z', 'id':'b51e27af-bcfd-4932-91be-e3f01a4802e6'}, {'body':'can I comment?', 'status_history':[{'type':'premod', 'created_at':'2016-12-13T23:23:47.123Z', 'assigned_by':null}, {'created_at':'2016-12-13T23:23:58.487Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'cef81015-1b53-4d70-b9af-6eca680f22fc', 'created_at':'2016-12-13T23:23:47.131Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-13T23:23:58.493Z', 'id':'dc9d7be1-b911-4dc3-8e1e-400e8b8d110e'}, {'body':'pre-mod comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T21:34:56.994Z', 'assigned_by':null}, {'created_at':'2016-12-08T21:38:04.961Z', 'type':'rejected', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T21:34:56.997Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'rejected', '__v':0, 'updated_at':'2016-12-08T21:38:04.965Z', 'id':'6f02af16-a8f8-4ead-80ea-0d48824eb74d'}, {'body':'a flagged commetn', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T21:38:26.342Z', 'assigned_by':null}, {'created_at':'2016-12-09T23:47:27.009Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T21:38:26.344Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T23:47:27.018Z', 'id':'784c5f91-36b9-4bda-b4ca-a114cef2c9f0'}, {'body':'a post mod comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T22:19:05.870Z', 'assigned_by':null}, {'created_at':'2016-12-09T23:26:41.427Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T22:19:05.874Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T23:26:41.450Z', 'id':'e8b86039-f850-4e53-bd9d-f8c9186a9637'}, {'body':'an actual post-mod comment here', 'status_history':[], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T22:20:11.147Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':null, '__v':0, 'updated_at':'2016-12-08T22:20:11.147Z', 'id':'cff1a318-50c6-431e-9a63-de7a7b7136bf'}];
const assets = [{'settings': null, 'created_at':'2016-12-06T21:36:09.302Z', 'url':'localhost:3000/', 'scraped':null, 'status':'open', 'updated_at':'2016-12-08T02:11:15.943Z', '_id':'58472f499e775a38f23d5da0', 'type':'article', 'closedMessage':null, 'id':'7302e637-f884-47c0-9723-02cc10a18617', 'closedAt':null}, {'settings':null, 'created_at':'2016-12-07T02:25:31.983Z', 'url':'http://localhost:3000/', 'scraped':null, 'status':'open', 'updated_at':'2016-12-13T22:58:36.061Z', '_id':'5847731b9e775a38f23d5da1', 'type':'article', 'closedMessage':null, 'id':'96fddf96-7c83-4008-80ad-50091997d006', 'closedAt':null}, {'settings':null, 'created_at':'2016-12-12T19:04:05.770Z', 'url':'http://localhost:3000/embed/stream', 'scraped':null, 'updated_at':'2016-12-14T20:13:21.934Z', '_id':'584ef4a59e775a38f23d5e86', 'type':'article', 'closedMessage':null, 'id':'cef81015-1b53-4d70-b9af-6eca680f22fc', 'closedAt':null}];
beforeEach(() => {
render = shallow(<CommentHistory comments={comments} assets={assets} />);
});
it('should render Comments as children when given comments and assets', () => {
const wrapper = mount(<CommentHistory comments={comments} assets={assets} />);
expect(wrapper.find('.commentHistory__list').children()).to.have.length(7);
});
it('should render with styles', () => {
expect(render.props().style).to.be.defined;
});
});
+49
View File
@@ -0,0 +1,49 @@
const jsdom = require('jsdom').jsdom;
const fs = require('fs');
const path = require('path');
// Storage Mock
function storageMock() {
const storage = {};
return {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return storage[key] || null;
},
removeItem: function(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
const keys = Object.keys(storage);
return keys[i] || null;
}
};
}
global.document = jsdom(fs.readFileSync(path.resolve(__dirname, 'index.test.html')));
global.window = document.defaultView;
// these lines are required for react-mdl
global.window.CustomEvent = undefined;
require('react-mdl/extra/material');
global.Element = global.window.Element;
global.navigator = {
userAgent: 'node.js'
};
global.documentRef = document;
global.localStorage = {};
global.sessionStorage = storageMock();
global.XMLHttpRequest = storageMock();
global.Headers = function(headers) {
return headers;
};
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -24,11 +24,12 @@ describe('models.Asset', () => {
});
describe('#findByUrl', ()=> {
beforeEach(() => Asset.findOrCreateByUrl('http://test.com'));
it('should find an asset by a url', () => {
return Asset.findByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('url')
.and.to.equal('http://test.com');
expect(asset).to.have.property('url', 'http://test.com');
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ const expect = require('chai').expect;
describe('models.Setting', () => {
beforeEach(() => Setting.init({moderation: 'pre'}));
beforeEach(() => Setting.init({moderation: 'pre', wordlist: ['donut']}));
describe('#retrieve()', () => {
it('should have a moderation field defined', () => {
+24 -25
View File
@@ -95,12 +95,17 @@ describe('models.User', () => {
});
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}])
return Comment.create([
{
body: 'testing the comment for that user if it is rejected.',
id: mockUsers[0].id
}
])
.then((comment) => {
.then(([comment]) => {
mockComment = comment;
});
});
@@ -109,11 +114,11 @@ describe('models.User', () => {
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');
});
return User.findById(mockUsers[0].id);
})
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('banned');
});
});
@@ -121,23 +126,20 @@ describe('models.User', () => {
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');
});
return 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');
});
.then(() => User.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'banned');
});
});
});
@@ -156,12 +158,9 @@ describe('models.User', () => {
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');
});
.then(() => User.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'active');
});
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
beforeEach(function (done) {
function clearDB() {
+33 -4
View File
@@ -17,7 +17,8 @@ describe('/api/v1/assets', () => {
{
url: 'https://coralproject.net/news/asset1',
title: 'Asset 1',
description: 'term1'
description: 'term1',
id: '1'
},
{
url: 'https://coralproject.net/news/asset2',
@@ -31,7 +32,7 @@ describe('/api/v1/assets', () => {
it('should return all assets without a search query', () => {
return chai.request(app)
.get('/api/v1/asset')
.get('/api/v1/assets')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
const body = res.body;
@@ -47,7 +48,7 @@ describe('/api/v1/assets', () => {
it('should return assets that we search for', () => {
return chai.request(app)
.get('/api/v1/asset?search=term2')
.get('/api/v1/assets?search=term2')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
const body = res.body;
@@ -68,7 +69,7 @@ describe('/api/v1/assets', () => {
it('should not return assets that we do not search for', () => {
return chai.request(app)
.get('/api/v1/asset?search=term3')
.get('/api/v1/assets?search=term3')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
const body = res.body;
@@ -82,4 +83,32 @@ describe('/api/v1/assets', () => {
});
describe('#put', () => {
it('should close the asset', function() {
const today = Date.now();
return Asset.findOrCreateByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('isClosed', null);
expect(asset).to.have.property('closedAt', null);
return chai.request(app)
.put(`/api/v1/assets/${asset.id}/status`)
.set(passport.inject({roles: ['admin']}))
.send({closedAt: today});
})
.then((res) => {
expect(res).to.have.status(204);
return Asset.findByUrl('http://test.com');
})
.then((asset) => {
expect(asset).to.have.property('isClosed', true);
expect(asset).to.have.property('closedAt').and.to.not.equal(null);
});
});
});
});
+22
View File
@@ -233,6 +233,28 @@ describe('/api/v1/comments', () => {
});
});
it('should create a rejected comment if the body is above the character count', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {charCountEnable: true, charCount: 10})
.then(() => asset);
})
.then((asset) => {
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'This is way way way way way too long.', '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('asset_id');
expect(res.body).to.have.property('status', 'rejected');
});
});
it('shouldn\'t create a comment when the asset has expired commenting', () => {
return Asset.create({
closedAt: new Date().setDate(0),
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../../mongoose');
const mongoose = require('../../services/mongoose');
// Ensure the NODE_ENV is set to 'test',
// this is helpful when you would like to change behavior when testing.