Merge branch 'master' into embed-link

This commit is contained in:
David Erwin
2016-11-08 14:44:18 -05:00
committed by GitHub
16 changed files with 279 additions and 76 deletions
+14 -2
View File
@@ -1,6 +1,12 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
A commenting platform from The Coral Project. [https://coralproject.net](https://coralproject.net)
## Contributing to Talk
### Local Dependencies
Node
Mongo
### Getting Started
`npm install`
Run it once to install the dependencies.
@@ -8,16 +14,22 @@ Run it once to install the dependencies.
`npm start`
Runs Talk.
### Running with Docker
Make sure you have Docker running first and then run `docker-compose up -d`
### Testing
`npm test`
### Lint
`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/
### Docs
`swagger.yaml`
### Mantainers
### License
**Apache-2.0**
+6
View File
@@ -8,6 +8,8 @@ const app = express();
// Middleware declarations.
app.use(morgan('dev'));
app.use(bodyParser.json());
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// API Routes.
app.use('/api/v1', require('./routes/api'));
@@ -15,4 +17,8 @@ app.use('/api/v1', require('./routes/api'));
// Static Routes.
app.use('/client/', express.static(path.join(__dirname, 'dist')));
app.get('/admin*', (req, res) => {
res.render('admin', {basePath: '/client/coral-admin'});
});
module.exports = app;
-1
View File
@@ -3,5 +3,4 @@ public/bundle.js
public/embed/comment-stream
.DS_Store
npm-debug.log
config.json
yarn.lock
+5
View File
@@ -0,0 +1,5 @@
{
"basePath": "admin",
"talkHost": "http://localhost:16180",
"xeniaHost": "http://localhost:16180"
}
+4 -5
View File
@@ -8,17 +8,16 @@ import store from 'services/store'
import CommentStream from 'containers/CommentStream'
import EmbedLink from 'components/EmbedLink'
import Configure from 'containers/Configure'
import config from 'services/config'
export default class App extends React.Component {
render (props) {
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path={config.basePath} component={ModerationQueue} />
<Route path={`${config.basePath}/embed`} component={CommentStream} />
<Route path={`${config.basePath}/embedlink`} component={EmbedLink} />
<Route path={`${config.basePath}/configure`} component={Configure} />
<Route path='admin' component={ModerationQueue} />
<Route path='admin/embed' component={CommentStream} />
<Route path='admin/embedlink' component={EmbedLink} />
<Route path='admin/configure' component={Configure} />
</Router>
</Provider>
)
+4 -4
View File
@@ -9,14 +9,14 @@ export default (props) => (
<Layout fixedDrawer>
<Header title='Talk'>
<Navigation>
<Link className={styles.navLink} to={`/${config.basePath}/`}>Moderate</Link>
<Link className={styles.navLink} to={`/${config.basePath}/configure`}>Configure</Link>
<Link className={styles.navLink} to={`/admin/`}>Moderate</Link>
<Link className={styles.navLink} to={`/admin/configure`}>Configure</Link>
</Navigation>
</Header>
<Drawer>
<Navigation>
<Link className={styles.navLink} to={`/${config.basePath}/`}>Moderate</Link>
<Link className={styles.navLink} to={`/${config.basePath}/configure`}>Configure</Link>
<Link className={styles.navLink} to={`/admin/`}>Moderate</Link>
<Link className={styles.navLink} to={`/admin/configure`}>Configure</Link>
</Navigation>
</Drawer>
{props.children}
+1 -11
View File
@@ -1,16 +1,6 @@
const path = require('path')
const fs = require('fs')
const autoprefixer = require('autoprefixer')
const precss = require('precss')
const config = require('./config.json')
// doing a string replace here because I spent a day trying to do it the "webpack" way
// ond nothing works. just trying to replace a string in an index.html file is
// apparently something no one has ever done in the js community.
let templateString = fs.readFileSync(path.join(__dirname, 'index.ejs')).toString()
templateString = templateString.replace('<%= basePath %>', config.basePath)
fs.writeFileSync(path.join(__dirname, 'public/index.html'), templateString)
module.exports = {
entry: {
@@ -24,7 +14,7 @@ module.exports = {
loaders: [
{ test: /.js$/, loader: 'babel', include: path.join(__dirname, 'src'), exclude: /node_modules/ },
{ test: /\.json$/, loaders: 'json', include: __dirname, exclude: /node_modules/ },
{ test: /.css$/, loaders: ['style-loader', 'css-loader?importLoaders=1', 'postcss-loader'] }
{ test: /.css$/, loaders: ['style-loader', 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader'] }
]
},
plugins: [ autoprefixer, precss ],
+1 -10
View File
@@ -1,17 +1,8 @@
const path = require('path')
const fs = require('fs')
const devConfig = require('./webpack.config.dev')
const autoprefixer = require('autoprefixer')
const precss = require('precss')
const Copy = require('copy-webpack-plugin')
const config = require('./config.json')
// doing a string replace here because I spent a day trying to do it the "webpack" way
// ond nothing works. just trying to replace a string in an index.html file is
// apparently something no one has ever done in the js community.
let templateString = fs.readFileSync(path.join(__dirname, 'index.ejs')).toString()
templateString = templateString.replace('<%= basePath %>', config.basePath)
fs.writeFileSync(path.join(__dirname, 'public/index.html'), templateString)
module.exports = Object.assign({}, devConfig, {
module: {
@@ -19,7 +10,7 @@ module.exports = Object.assign({}, devConfig, {
loaders: [
{ test: /.js$/, loader: 'babel', include: [path.join(__dirname, 'src'), path.join(__dirname, '../', 'coral-framework')], exclude: /node_modules/ },
{ test: /.json$/, loader: 'json', include: __dirname, exclude: /node_modules/ },
{ test: /.css$/, loaders: ['style-loader', 'css-loader?importLoaders=1', 'postcss-loader'] }
{ test: /.css$/, loaders: ['style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader'] }
]
},
plugins: [
+27 -1
View File
@@ -28,7 +28,7 @@ ActionSchema.statics.findById = function(id) {
};
/**
* Finds users in an array of ids.
* Finds actions in an array of ids.
* @param {String} ids array of user identifiers (uuid)
*/
ActionSchema.statics.findByItemIdArray = function(item_ids) {
@@ -37,6 +37,32 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
});
};
/**
* Finds all comments for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
ActionSchema.statics.findByType = function(action_type, item_type) {
return Action.find({
'action_type': action_type,
'item_type': item_type
});
};
/**
* Finds all comments ids for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_type) {
return Action.find({
'action_type': action_type,
'item_type': item_type
},
'item_id'
);
};
const Action = mongoose.model('Action', ActionSchema);
module.exports = Action;
+52
View File
@@ -31,6 +31,23 @@ const CommentSchema = new Schema({
}
});
//==============================================================================
// New Statics
//==============================================================================
/**
* Create a comment.
* @param {String} body content of comment
*/
CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, status, username) {
let comment = new Comment({body, author_id, asset_id, parent_id, status, username});
return comment.save();
};
//==============================================================================
// Find Statics
//==============================================================================
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
@@ -47,6 +64,28 @@ CommentSchema.statics.findByAssetId = function(asset_id) {
return Comment.find({asset_id});
};
/**
* Find comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
*/
CommentSchema.statics.findByActionType = function(action_type) {
return Action.findCommentsIdByActionType(action_type, 'comment').then((actions) => {
return Comment.find({'id': {'$in': actions.map(function(a){return a.item_id;})}});
});
};
/**
* Find comments by their status.
* @param {String} status the status to search for
*/
CommentSchema.statics.findByStatus = function(status) {
return Comment.find({'status': status});
};
//==============================================================================
// Update Statics
//==============================================================================
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
@@ -72,6 +111,19 @@ CommentSchema.statics.addAction = function(id, user_id, action_type) {
return action.save();
};
//==============================================================================
// Remove Statics
//==============================================================================
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
*/
CommentSchema.statics.removeById = function(id) {
return Comment.remove({'id': id});
};
const Comment = mongoose.model('Comment', CommentSchema);
module.exports = Comment;
+11 -3
View File
@@ -5,7 +5,7 @@
"main": "app.js",
"scripts": {
"start": "./bin/www",
"build": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.js && cd client/coral-admin && npm run build",
"build": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.js && cd client/coral-admin && npm install && npm run build",
"lint": "eslint .",
"pretest": "npm install",
"test": "mocha tests --recursive",
@@ -42,14 +42,17 @@
},
"homepage": "https://github.com/coralproject/talk#readme",
"dependencies": {
"autoprefixer": "^6.5.2",
"body-parser": "^1.15.2",
"debug": "^2.2.0",
"ejs": "^2.5.2",
"express": "^4.14.0",
"mongoose": "^4.6.5",
"uuid": "^2.0.3",
"morgan": "^1.7.0"
"morgan": "^1.7.0",
"uuid": "^2.0.3"
},
"devDependencies": {
"autoprefixer": "6.5.0",
"babel-core": "6.14.0",
"babel-jest": "^15.0.0",
"babel-loader": "6.2.5",
@@ -64,6 +67,7 @@
"chai": "^3.5.0",
"chai-http": "^3.0.0",
"copy-webpack-plugin": "^3.0.1",
"css-loader": "0.25.0",
"eslint": "^3.9.1",
"exports-loader": "^0.6.3",
"immutable": "^3.8.1",
@@ -71,6 +75,9 @@
"json-loader": "^0.5.4",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
"postcss-loader": "0.13.0",
"postcss-modules": "0.5.2",
"precss": "1.4.0",
"pre-git": "^3.10.0",
"pym.js": "^1.1.1",
"react": "15.3.2",
@@ -79,6 +86,7 @@
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"style-loader": "0.13.1",
"supertest": "^2.0.1",
"timeago.js": "^2.0.3",
"webpack": "^1.13.2",
+56 -14
View File
@@ -4,7 +4,7 @@ const Comment = require('../../../models/comment');
const router = express.Router();
//==============================================================================
// Routes
// Get Routes
//==============================================================================
router.get('/', (req, res, next) => {
@@ -23,16 +23,54 @@ router.get('/:comment_id', (req, res, next) => {
});
});
router.post('/', (req, res, next) => {
const {body, author_id, asset_id, parent_id, status, username} = req.body;
let comment = new Comment({body, author_id, asset_id, parent_id, status, username});
comment.save().then(({id}) => {
res.status(200).send({'id': id});
//==============================================================================
// Moderation Queues Routes
//==============================================================================
router.get('/action/:action_type', (req, res, next) => {
Comment.findByActionType(req.params.action_type).then((comments) => {
res.status(200).json(comments);
}).catch(error => {
next(error);
});
});
router.get('/status/rejected', (req, res, next) => {
Comment.findByStatus('rejected').then((comments) => {
res.status(200).json(comments);
}).catch(error => {
next(error);
});
});
router.get('/status/pending', (req, res, next) => {
Comment.findByStatus('').then((comments) => {
res.status(200).json(comments);
}).catch(error => {
next(error);
});
});
//==============================================================================
// Post Routes
//==============================================================================
router.post('/', (req, res, next) => {
const {body, author_id, asset_id, parent_id, status, username} = req.body;
Comment.new(body, author_id, asset_id, parent_id, status, username).then((comment) => {
res.status(200).send({'id': comment.id});
}).catch(error => {
next(error);
});
// let comment = new Comment({body, author_id, asset_id, parent_id, status, username});
// comment.save().then(({id}) => {
// res.status(200).send({'id': id});
// }).catch(error => {
// next(error);
// });
});
router.post('/:comment_id', (req, res, next) => {
Comment.findById(req.params.comment_id).then((comment) => {
comment.body = req.body.body;
@@ -48,14 +86,6 @@ router.post('/:comment_id', (req, res, next) => {
});
});
router.delete('/:comment_id', (req, res, next) => {
Comment.remove(req.params.comment_id).then(() => {
res.status(201).send('OK. Deleted');
}).catch(error => {
next(error);
});
});
router.post('/:comment_id/status', (req, res, next) => {
Comment.changeStatus(req.params.comment_id, req.body.status).then((comment) => {
res.status(200).send(comment);
@@ -72,4 +102,16 @@ router.post('/:comment_id/actions', (req, res, next) => {
});
});
//==============================================================================
// Delete Routes
//==============================================================================
router.delete('/:comment_id', (req, res, next) => {
Comment.removeById(req.params.comment_id).then(() => {
res.status(201).send('OK. Removed');
}).catch(error => {
next(error);
});
});
module.exports = router;
+97 -21
View File
@@ -66,6 +66,86 @@ describe('Get /comments', () => {
});
});
describe('Get moderation queues rejected, pending, flags', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: 'rejected'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
}];
const users = [{
id: '123',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Maria',
}];
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('should return all the rejected comments', function(done){
chai.request(app)
.get('/api/v1/comments/status/rejected')
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'abc');
done();
});
});
it('should return all the pending comments', function(done){
chai.request(app)
.get('/api/v1/comments/status/pending')
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'def');
done();
});
});
it('should return all the flagged comments', function(done){
chai.request(app)
.get('/api/v1/comments/action/flag')
.end(function(err, res){
expect(res).to.have.status(200);
expect(err).to.be.null;
expect(res.body.length).to.equal(1);
expect(res.body[0]).to.have.property('id', 'abc');
done();
});
});
});
describe('Post /comments', () => {
const users = [{
id: '123',
@@ -128,10 +208,12 @@ describe('Get /:comment_id', () => {
const actions = [{
action_type: 'flag',
item_id: 'abc'
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij'
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
@@ -144,13 +226,12 @@ describe('Get /:comment_id', () => {
it('should return the right comment for the comment_id', function(done){
chai.request(app)
.get('/api/v1/comments')
.query({'comment_id': 'abc'})
.get('/api/v1/comments/abc')
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('body');
expect(res.body[0].body).to.equal('comment 10');
expect(res).to.have.property('body');
expect(res.body).to.have.property('body', 'comment 10');
done();
});
});
@@ -205,14 +286,13 @@ describe('Put /:comment_id', () => {
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body).to.have.property('body');
expect(res.body.body).to.equal('Something body.');
expect(res.body).to.have.property('body', 'Something body.');
done();
});
});
});
describe('Delete /:comment_id', () => {
describe('Remove /:comment_id', () => {
const comments = [{
id: 'abc',
@@ -258,9 +338,10 @@ describe('Delete /:comment_id', () => {
chai.request(app)
.delete('/api/v1/comments/abc')
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(201);
Comment.findById({'id': 'abc'}).then((comment) => {
expect(comment).to.be.null;
Comment.findById('abc').then((comment) => {
expect(comment).to.be.empty;
});
done();
});
@@ -320,8 +401,7 @@ describe('Post /:comment_id/status', () => {
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res).to.have.body;
expect(res.body).to.have.property('status');
expect(res.body.status).to.equal('accepted');
expect(res.body).to.have.property('status', 'accepted');
done();
});
});
@@ -380,14 +460,10 @@ describe('Post /:comment_id/actions', () => {
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res).to.have.body;
expect(res.body).to.have.property('item_type');
expect(res.body.item_type).to.equal('comment');
expect(res.body).to.have.property('action_type');
expect(res.body.action_type).to.equal('flag');
expect(res.body).to.have.property('item_id');
expect(res.body.item_id).to.equal('abc');
expect(res.body).to.have.property('user_id');
expect(res.body.user_id).to.equal('456');
expect(res.body).to.have.property('item_type', 'comment');
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('item_id', 'abc');
expect(res.body).to.have.property('user_id', '456');
done();
});
});
+1 -3
View File
@@ -24,8 +24,7 @@ describe('GET /settings', () => {
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res).to.be.json;
expect(res.body).to.have.property('moderation');
expect(res.body.moderation).to.equal('pre');
expect(res.body).to.have.property('moderation', 'pre');
done(err);
});
});
@@ -33,7 +32,6 @@ describe('GET /settings', () => {
// update the settings.
describe('update settings', () => {
it('should respond ok to a PUT', () => {
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
.then(() => {
-1
View File
@@ -60,7 +60,6 @@ describe('api/stream: routes', () => {
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
if (err) {return done(err);}
done();
});
});