Merge branch 'master' into hide-wordlist-from-fe

This commit is contained in:
Gabriela Rodríguez Berón
2016-12-13 11:40:18 -08:00
committed by GitHub
42 changed files with 199 additions and 119 deletions
+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');
+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);
@@ -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);
+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",
+28 -8
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});
@@ -31,3 +23,31 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
})
.catch(error => dispatch(updateConfigFailure(error)));
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
dispatch(updateConfigRequest());
coralApi(`/asset/${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()}));
}
};
+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;
}

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

View File
+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;
+9 -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,
@@ -64,13 +64,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).
@@ -83,6 +76,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.
+1 -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');
+2 -2
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');
/**
* SettingSchema manages application settings that get used on front and backend.
+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');
+24 -13
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 --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"
},
@@ -84,9 +93,10 @@
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.25.0",
"dialog-polyfill": "^0.4.4",
"eslint": "^3.9.1",
"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",
@@ -113,6 +123,7 @@
"pym.js": "^1.1.1",
"react": "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 &
+21 -4
View File
@@ -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();
-1
View File
@@ -87,7 +87,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}`));
}
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('../mongoose');
const mongoose = require('../services/mongoose');
beforeEach(function (done) {
function clearDB() {
+30 -1
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',
@@ -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/asset/${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);
});
});
});
});
+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.