merge master

This commit is contained in:
Riley Davis
2016-11-07 15:28:20 -07:00
19 changed files with 550 additions and 74 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ RUN npm install
COPY . /usr/src/app
# Compile static assets
# RUN npm run build
RUN npm run build
# Prune development dependancies
RUN npm prune --production
+1 -1
View File
@@ -17,6 +17,6 @@
</head>
<body>
<div id="root"></div>
<script src="<%= basePath %>/bundle.js" charset="utf-8"></script>
<script src="<%= basePath %>/bundlezzzzzz.js" charset="utf-8"></script>
</body>
</html>
+1 -1
View File
@@ -17,6 +17,6 @@
</head>
<body>
<div id="root"></div>
<script src="client/coral-admin/bundle.js" charset="utf-8"></script>
<script src="client/coral-admin/bundlezzzzzz.js" charset="utf-8"></script>
</body>
</html>
-1
View File
@@ -4,7 +4,6 @@ const devConfig = require('./webpack.config.dev')
const autoprefixer = require('autoprefixer')
const precss = require('precss')
const Copy = require('copy-webpack-plugin')
const webpack = require('webpack')
const config = require('./config.json')
// doing a string replace here because I spent a day trying to do it the "webpack" way
@@ -9,7 +9,7 @@
<div id='coralStreamEmbed'></div>
<script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script>
<script>
var pymParent = new pym.Parent('coralStreamEmbed', 'index.html', {});
var pymParent = new pym.Parent('coralStreamEmbed', 'index.html', {title: 'comments'});
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
</div>
</body>
@@ -11,6 +11,7 @@ import Content from '../../coral-plugin-commentcontent/CommentContent'
import PubDate from '../../coral-plugin-pubdate/PubDate'
import Count from '../../coral-plugin-comment-count/CommentCount'
import Flag from '../../coral-plugin-flags/FlagButton'
import AuthorName from '../../coral-plugin-author-name/AuthorName'
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'
import Pym from 'pym.js'
@@ -110,13 +111,15 @@ class CommentStream extends Component {
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}/>
id={rootItemId}
reply={false}/>
</div>
{
rootItem.comments.map((commentId) => {
const comment = this.props.items[commentId]
return <div className="comment">
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName name={comment.username}/>
<PubDate created_at={comment.created_at}/>
<Content content={comment.body}/>
<div className="commentActions">
@@ -135,14 +138,16 @@ class CommentStream extends Component {
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={commentId}
id={rootItemId}
parent_id={commentId}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items[replyId]
return <div className="reply">
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName name={reply.username}/>
<PubDate created_at={reply.created_at}/>
<Content content={reply.body}/>
<div className="replyActions">
+13 -1
View File
@@ -56,7 +56,7 @@ hr {
.coral-plugin-commentbox-textarea {
flex: 1;
padding: 10px;
padding: 5px;
}
.coral-plugin-commentbox-button-container {
@@ -75,6 +75,12 @@ hr {
border-radius: 2px;
}
.coral-plugin-commentbox-username {
width: 50%;
padding-left: 5px;
margin-bottom: 5px;
}
/* Comment styles */
.comment {
margin-bottom: 10px;
@@ -84,6 +90,12 @@ hr {
margin-bottom: 10px;
}
.coral-plugin-author-name-text {
display: inline-block;
margin-right: 10px;
font-weight: bolder;
}
/* Reply styles */
@@ -9,13 +9,14 @@ module.exports = {
path.join(__dirname, 'src', 'app')
],
output: {
path: path.join(__dirname, 'dist'),
path: path.join(__dirname, '..', '..','dist', 'coral-embed-stream'),
filename: 'bundle.js',
publicPath: '/'
},
resolve: {
root: [
path.resolve(__dirname, 'src')
path.resolve(__dirname, 'src'),
path.resolve(__dirname, '..')
],
extensions: ['', '.js', '.jsx']
},
+11 -9
View File
@@ -97,12 +97,10 @@ export function getStream (assetId) {
/* Check for root and child comments. */
if (
item.type === 'comment' &&
item.asset_id === assetId &&
!item.parent_id) {
rootComments.push(item.id)
} else if (
item.type === 'comment' &&
item.asset_id === assetId
) {
let children = childComments[item.parent_id] || []
@@ -178,18 +176,22 @@ export function postItem (item, type, id) {
}
let options = {
method: 'POST',
body: JSON.stringify(item)
body: JSON.stringify(item),
headers: {
'Content-Type':'application/json'
}
}
return fetch('api/v1/' + type, options)
console.log('postItem', options);
return fetch('/api/v1/' + type, options)
.then(
response => {
return response.ok ? response.json()
return response.ok ? response.text()
: Promise.reject(response.status + ' ' + response.statusText)
}
)
.then((json) => {
dispatch(addItem(json))
return json.id
.then((id) => {
dispatch(addItem({...item, id}))
return id
})
}
}
@@ -223,7 +225,7 @@ export function postAction (id, type, user_id) {
}
dispatch(appendItemArray(id, type, user_id))
return fetch('api/v1/comments/' + id + '/actions', options)
return fetch('/api/v1/comments/' + id + '/actions', options)
.then(
response => {
return response.ok ? response.text()
@@ -0,0 +1,9 @@
import React from 'react'
const packagename = 'coral-plugin-author-name'
const AuthorName = ({name}) =>
<div className={packagename + '-text'}>
{name}
</div>
export default AuthorName
+33 -18
View File
@@ -8,56 +8,67 @@ class CommentBox extends Component {
static propTypes = {
postItem: PropTypes.func,
updateItem: PropTypes.func,
item_id: PropTypes.string,
id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool
}
state = {
content: ''
body: '',
username: ''
}
postComment = () => {
const {postItem, updateItem, item_id, reply, addNotification, appendItemArray} = this.props
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray} = this.props
let comment = {
content: this.state.content
body: this.state.body,
asset_id: id,
username: this.state.username
}
let related
if (reply) {
comment.parent_id = item_id
if (parent_id) {
comment.parent_id = parent_id
related = 'children'
} else {
comment.asset_id = item_id
related = 'comments'
}
updateItem(item_id, 'showReply', false)
updateItem(parent_id, 'showReply', false)
postItem(comment, 'comments')
.then((id) => {
appendItemArray(item_id, related, id)
.then((comment_id) => {
appendItemArray(parent_id || id, related, comment_id)
addNotification('success', 'Your comment has been posted.')
}).catch((err) => console.error(err))
this.setState({content: ''})
this.setState({body: ''})
}
render () {
const {styles, reply} = this.props
// How to handle language in plugins? Should we have a dependency on our central translation file?
return <div>
<div className={name + '-container'}>
<input type='text'
className={name + '-username'}
style={styles && styles.textarea}
value={this.state.username}
id={reply ? 'replyUser' : 'commentUser'}
placeholder='Name'
onChange={(e) => this.setState({username: e.target.value})}/>
</div>
<div
className={name + '-container'}
style={styles && styles.container}>
className={name + '-container'}>
<label
htmlFor={ reply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{reply ? 'Reply': 'Comment'}
{reply ? lang.t('reply'): lang.t('comment')}
</label>
<textarea
className={name + '-textarea'}
style={styles && styles.textarea}
value={this.state.content}
value={this.state.body}
placeholder='Comment'
id={reply ? 'replyText' : 'commentText'}
onChange={(e) => this.setState({content: e.target.value})}
onChange={(e) => this.setState({body: e.target.value})}
rows={3}/>
</div>
<div className={name + '-button-container'}>
@@ -76,9 +87,13 @@ export default CommentBox
const lang = new I18n({
en: {
post: 'Post'
post: 'Post',
reply: 'Reply',
comment: 'Comment',
},
es: {
post: 'Publicar'
post: 'Publicar',
reply: 'Respuesta',
comment: 'Comentario'
}
})
+2 -1
View File
@@ -8,7 +8,8 @@ const ReplyBox = (props) => <div
style={props.styles && props.styles.container}>
{
props.showReply && <CommentBox
item_id = {props.item_id}
id = {props.id}
parent_id = {props.parent_id}
postItem = {props.postItem}
addNotification = {props.addNotification}
appendItemArray = {props.appendItemArray}
+1 -1
View File
@@ -5,7 +5,7 @@ const name = 'coral-plugin-replies'
const ReplyButton = (props) => <button
className={name + '-reply-button'}
onClick={(e) => props.updateItem(props.item_id || props.parent_id, 'showReply', true)}>
onClick={(e) => props.updateItem(props.id || props.parent_id, 'showReply', true)}>
<i className={name + '-icon material-icons'}
aria-hidden={true}>reply</i>
{lang.t('reply')}
+30 -2
View File
@@ -1,5 +1,7 @@
const mongoose = require('../mongoose');
const uuid = require('uuid');
const Action = require('./action');
const Schema = mongoose.Schema;
const CommentSchema = new Schema({
@@ -15,6 +17,7 @@ const CommentSchema = new Schema({
},
asset_id: String,
author_id: String,
username: String,
status: {
type: String,
enum: ['accepted', 'rejected', ''],
@@ -30,10 +33,10 @@ const CommentSchema = new Schema({
/**
* Finds a comment by the id.
* @param {String} asset_id identifier of comment (uuid)
* @param {String} id identifier of comment (uuid)
*/
CommentSchema.statics.findById = function(id) {
return Comment.findOne({id});
return Comment.findOne({'id': id});
};
/**
@@ -44,6 +47,31 @@ CommentSchema.statics.findByAssetId = function(asset_id) {
return Comment.find({asset_id});
};
/**
* 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.changeStatus = function(id, status) {
return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}}, {upsert: false, new: true});
};
/**
* Add an action to the comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} action the new action to the comment
*/
CommentSchema.statics.addAction = function(id, user_id, action_type) {
// check that the comment exist
let action = new Action({
action_type: action_type,
item_type: 'comment',
item_id: id,
user_id: user_id
});
return action.save();
};
const Comment = mongoose.model('Comment', CommentSchema);
module.exports = Comment;
+3 -4
View File
@@ -5,12 +5,12 @@
"main": "app.js",
"scripts": {
"start": "./bin/www",
"build": "webpack --config ./client/coral-embed-stream/webpack.config.js && webpack --config ./client/coral-admin/webpack.config.js",
"build": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.js && ./node_modules/webpack/bin/webpack.js --config ./client/coral-admin/webpack.config.js",
"lint": "eslint .",
"pretest": "npm install",
"test": "mocha tests --recursive",
"test-watch": "mocha tests --recursive -w",
"embed-start": "node client/coral-embed-stream/dev-server.js"
"embed-start": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.dev.js && ./bin/www"
},
"config": {
"pre-git": {
@@ -43,7 +43,6 @@
"homepage": "https://github.com/coralproject/talk#readme",
"dependencies": {
"body-parser": "^1.15.2",
"chai-http": "^3.0.0",
"debug": "^2.2.0",
"express": "^4.14.0",
"mongoose": "^4.6.5",
@@ -63,7 +62,7 @@
"babel-preset-es2015-minimal": "^2.1.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-http": "^1.0.0",
"chai-http": "^3.0.0",
"copy-webpack-plugin": "^3.0.1",
"eslint": "^3.9.1",
"exports-loader": "^0.6.3",
+40 -19
View File
@@ -7,8 +7,12 @@ const router = express.Router();
// Routes
//==============================================================================
router.get('/', (req, res) => {
res.send('Read all of the comments ever');
router.get('/', (req, res, next) => {
Comment.find({}).then((comments) => {
res.status(200).json(comments);
}).catch(error => {
next(error);
});
});
router.get('/:comment_id', (req, res, next) => {
@@ -20,35 +24,52 @@ router.get('/:comment_id', (req, res, next) => {
});
router.post('/', (req, res, next) => {
let comment = new Comment({
body: req.query.body,
author_id: req.query.author_id,
asset_id: req.query.asset_id,
parent_id: req.query.parent_id,
status: req.query.status
});
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(201).send(id);
res.status(200).send({'id': id});
}).catch(error => {
next(error);
});
});
router.put('/:comment_id', (req, res) => {
res.send('Update a comment');
router.post('/:comment_id', (req, res, next) => {
Comment.findById(req.params.comment_id).then((comment) => {
comment.body = req.body.body;
comment.author_id = req.body.author_id;
comment.asset_id = req.body.asset_id;
comment.parent_id = req.body.parent_id;
comment.status = req.body.status;
return comment.save();
}).then((comment) => {
res.status(200).send(comment);
}).catch(error => {
next(error);
});
});
router.delete('/:comment_id', (req, res) => {
res.send('Delete a comment');
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) => {
res.send('Update a comment status');
router.post('/:comment_id/status', (req, res, next) => {
Comment.changeStatus(req.params.comment_id, req.body.status).then((comment) => {
res.status(200).send(comment);
}).catch(error => {
next(error);
});
});
router.post('/:comment_id/actions', (req, res) => {
res.send('Add a comment action');
router.post('/:comment_id/actions', (req, res, next) => {
Comment.addAction(req.params.comment_id, req.body.user_id, req.body.action_type).then((action) => {
res.status(200).send(action);
}).catch(error => {
next(error);
});
});
module.exports = router;
+32
View File
@@ -0,0 +1,32 @@
/* eslint-env node, mocha */
require('../utils/mongoose');
const Setting = require('../../models/setting');
const expect = require('chai').expect;
describe('Setting: model', () => {
beforeEach(() => {
const defaults = {id: 1, moderation: 'pre'};
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
describe('#getSettings()', () => {
it('should have a moderation field defined', () => {
return Setting.getSettings().then(settings => {
expect(settings).to.have.property('moderation').and.to.equal('pre');
});
});
});
describe('#updateSettings()', () => {
it('should update the settings with a passed object', () => {
const mockSettings = {moderation: 'post'};
return Setting.updateSettings(mockSettings).then(updatedSettings => {
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
});
});
});
});
+298 -8
View File
@@ -14,13 +14,65 @@ const Comment = require('../../../../models/comment');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
describe('Get /comments', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456'
}];
const users = [{
id: '123',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Maria',
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('should return all the comments', function(done){
chai.request(app)
.get('/api/v1/comments')
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
done();
});
});
});
describe('Post /comments', () => {
const users = [{
id: '123',
display_name: 'John',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Paul',
display_name: 'Maria',
}];
const actions = [{
@@ -40,13 +92,13 @@ describe('Post /comments', () => {
it('it should create a comment', function(done) {
chai.request(app)
.post('/api/v1/comments')
.query({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
.end(function(err, res){
expect(res).to.have.status(201);
expect(res).to.have.status(200);
expect(res.body).to.have.property('id');
done();
});
});
});
describe('Get /:comment_id', () => {
@@ -68,10 +120,10 @@ describe('Get /:comment_id', () => {
const users = [{
id: '123',
display_name: 'John',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Paul',
display_name: 'Maria',
}];
const actions = [{
@@ -97,7 +149,245 @@ describe('Get /:comment_id', () => {
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
if (err) {return done(err);}
expect(res.body[0]).to.have.property('body');
expect(res.body[0].body).to.equal('comment 10');
done();
});
});
});
describe('Put /:comment_id', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456'
}];
const users = [{
id: '123',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Maria',
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('it should update comment', function(done) {
chai.request(app)
.post('/api/v1/comments/abc')
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_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.');
done();
});
});
});
describe('Delete /:comment_id', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456'
}];
const users = [{
id: '123',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Maria',
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('it should remove comment', function(done) {
chai.request(app)
.delete('/api/v1/comments/abc')
.end(function(err, res){
expect(res).to.have.status(201);
Comment.findById({'id': 'abc'}).then((comment) => {
expect(comment).to.be.null;
});
done();
});
});
});
describe('Post /:comment_id/status', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: ''
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
}, {
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'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('it should update status', function(done) {
chai.request(app)
.post('/api/v1/comments/abc/status')
.send({'status': 'accepted'})
.end(function(err, res){
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');
done();
});
});
});
describe('Post /:comment_id/actions', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: ''
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
}, {
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'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Comment.create(comments).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
it('it should update actions', function(done) {
chai.request(app)
.post('/api/v1/comments/abc/actions')
.send({'user_id': '456', 'action_type': 'flag'})
.end(function(err, res){
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');
done();
});
});
+62
View File
@@ -0,0 +1,62 @@
process.env.NODE_ENV = 'test';
require('../../../utils/mongoose');
const app = require('../../../../app');
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const expect = chai.expect;
const Setting = require('../../../../models/setting');
const defaults = {id: '1', moderation: 'pre'};
describe('GET /settings', () => {
beforeEach(() => {
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
it('should return a settings object', done => {
chai.request(app)
.get('/api/v1/settings')
.end((err, res) => {
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');
done(err);
});
});
});
// update the settings.
describe('update settings', () => {
before(() => {
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
it('should respond to a PUT with new settings', () => {
chai.request(app)
.put('/api/v1/settings')
.send({moderation: 'post'}, (err, res) => {
expect(err).to.be.null;
expect(res).to.have.status(204);
done(err);
});
});
it('should have updates settings', () => {
chai.request(app)
.get('/api/v1/settings')
.end((err, res) => {
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('post');
});
});
});