Merge branch 'master' into auth

This commit is contained in:
Wyatt Johnson
2016-11-09 16:28:39 -07:00
committed by GitHub
13 changed files with 290 additions and 33 deletions
@@ -0,0 +1,66 @@
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
const base = '/api/v1';
const getInit = (method, body) => {
const headers = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
});
const init = {method, headers};
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
}
return init;
};
const handleResp = res => {
if (res.status === 401) {
throw new Error('Not Authorized to make this request');
} else if (res.status > 399) {
throw new Error('Error! Status ', res.status);
} else if (res.status === 204) {
return res.text();
} else {
return res.json();
}
};
export const fetchSettings = () => dispatch => {
dispatch({type: SETTINGS_LOADING});
fetch(`${base}/settings`, getInit('GET'))
.then(handleResp)
.then(settings => {
dispatch({type: SETTINGS_RECEIVED, settings});
})
.catch(error => {
dispatch({type: SETTINGS_FETCH_ERROR, error});
});
};
export const updateSettings = settings => {
return {type: SETTINGS_UPDATED, settings};
};
export const saveSettingsToServer = () => (dispatch, getState) => {
const settings = getState().settings.toJS().settings;
dispatch({type: SAVE_SETTINGS_LOADING});
fetch(`${base}/settings`, getInit('PUT', settings))
.then(handleResp)
.then(() => {
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
})
.catch(error => {
dispatch({type: SAVE_SETTINGS_FAILED, error});
});
};
@@ -20,6 +20,7 @@
border-radius: 4px;
height: 90px;
margin-bottom: 10px;
cursor: pointer;
}
.configSettingEmbed {
+37 -5
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {connect} from 'react-redux';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../actions/settings';
import {
List,
ListItem,
ListItemContent,
ListItemAction,
Textfield,
//Textfield,
Checkbox,
Button,
Icon
@@ -17,16 +19,38 @@ import translations from '../translations';
class Configure extends React.Component {
constructor (props) {
super(props);
this.state = {activeSection: 'comments', copied: false};
this.copyToClipBoard = this.copyToClipBoard.bind(this);
this.updateModeration = this.updateModeration.bind(this);
this.saveSettings = this.saveSettings.bind(this);
}
componentWillMount () {
this.props.dispatch(fetchSettings());
}
updateModeration () {
const moderation = this.props.settings.moderation === 'pre' ? 'post' : 'pre';
this.props.dispatch(updateSettings({moderation}));
}
saveSettings () {
this.props.dispatch(saveSettingsToServer());
}
getCommentSettings () {
return <List>
<ListItem className={styles.configSetting}>
<ListItemAction><Checkbox /></ListItemAction>
<ListItemAction>
<Checkbox
onClick={this.updateModeration}
checked={this.props.settings.moderation === 'pre'} />
</ListItemAction>
Enable pre-moderation
</ListItem>
{/*
<ListItem className={styles.configSetting}>
<ListItemAction><Checkbox /></ListItemAction>
Include Comment Stream Description for Readers
@@ -39,6 +63,7 @@ class Configure extends React.Component {
error='Input is not a number!'
label='Maximum Characters' />
</ListItem>
*/}
</List>;
}
@@ -75,10 +100,14 @@ class Configure extends React.Component {
}
render () {
const pageTitle = this.state.activeSection === 'comments'
let pageTitle = this.state.activeSection === 'comments'
? 'Comment Settings'
: 'Embed Comment Stream';
if (this.props.fetchingSettings) {
pageTitle += ' - Loading...';
}
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
@@ -94,12 +123,14 @@ class Configure extends React.Component {
icon='code'>Embed Comment Stream</ListItemContent>
</ListItem>
</List>
<Button raised colored>
<Button raised colored onClick={this.saveSettings}>
<Icon name='save' /> Save Changes
</Button>
</div>
<div className={styles.mainContent}>
<h1>{pageTitle}</h1>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{
this.state.activeSection === 'comments'
? this.getCommentSettings()
@@ -111,6 +142,7 @@ class Configure extends React.Component {
}
}
export default connect(x => x)(Configure);
const mapStateToProps = state => state.settings.toJS();
export default connect(mapStateToProps)(Configure);
const lang = new I18n(translations);
+1 -1
View File
@@ -39,7 +39,7 @@ const updateStatus = (state, action) => {
// Flag a comment
const flag = (state, action) => {
const byId = state.get('byId');
const data = byId.get(action.id).get('data').set('flagged', true);
const data = byId.get(action.id).set('flagged', true);
const comment = byId.get(action.id).set('data', data);
return state.set('byId', byId.set(action.id, comment));
};
+2
View File
@@ -1,8 +1,10 @@
import {combineReducers} from 'redux';
import comments from 'reducers/comments';
import settings from 'reducers/settings';
// Combine all reducers into a main one
export default combineReducers({
settings,
comments
});
@@ -0,0 +1,43 @@
import {Map} from 'immutable';
import * as types from '../actions/settings';
const initialState = Map({
settings: Map(),
saveSettingsError: null,
fetchSettingsError: null,
fetchingSettings: false
});
// Handle the comment actions
export default (state = initialState, action) => {
switch (action.type) {
case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null);
case types.SETTINGS_RECEIVED: return updateSettings(state, action);
case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action);
case types.SETTINGS_UPDATED: return updateSettings(state, action);
case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
default: return state;
}
};
const updateSettings = (state, action) => {
const s = state.set('fetchingSettings', false).set('fetchSettingsError', null);
const settings = s.get('settings').merge(action.settings);
return s.set('settings', settings);
};
const saveComplete = (state, action) => {
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
const settings = s.get('settings').merge(action.settings);
return s.set('settings', settings);
};
const settingsFetchFailed = (state, action) => {
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
};
const settingsSaveFailed = (state, action) => {
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
};
@@ -29,15 +29,20 @@ export default store => next => action => {
// Get comments to fill each of the three lists on the mod queue
const fetchModerationQueueComments = store =>
fetch('/api/v1/queue')
.then(res => res.json())
Promise.all([fetch('/api/v1/comments/status/pending'), fetch('/api/v1/comments/status/rejected'), fetch('/api/v1/comments/action/flag')])
.then(res => Promise.all(res.map(r => r.json())))
.then(res => {
res[2] = res[2].map(comment => { comment.flagged = true; return comment; });
return res.reduce((prev, curr) => prev.concat(curr), []);
})
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: res}))
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
// Update a comment. Now to update a comment we need to send back the whole object
const updateComment = (store, comment) =>
fetch(`/api/v1/comments/${comment._id}/status`, {
fetch('/api/v1/comments/${comment._id}/status', {
method: 'POST',
body: JSON.stringify({status: comment.status})
})
+40 -1
View File
@@ -1,6 +1,7 @@
const mongoose = require('../mongoose');
const uuid = require('uuid');
const Action = require('./action');
const Setting = require('./setting');
const Schema = mongoose.Schema;
@@ -74,14 +75,52 @@ CommentSchema.statics.findByActionType = function(action_type) {
});
};
/**
* Find not moderated comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
* @param {String} status the status of the comment to search for
*/
CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
return Action.findCommentsIdByActionType(action_type, 'comment').then((actions) => {
return Comment.find({'status': status, 'id': {'$in': actions.map(function(a){return a.item_id;})}});
});
};
/**
* Find comments by their status.
* @param {String} status the status to search for
* @param {String} status the status of the comment to search for
*/
CommentSchema.statics.findByStatus = function(status) {
return Comment.find({'status': status});
};
/**
* Find comments that need to be moderated (aka moderation queue).
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
*/
CommentSchema.statics.moderationQueue = function(moderationValue) {
return Setting.getModerationSetting().then(function({moderation}){
if (typeof moderationValue === 'undefined' || moderationValue === undefined) {
moderationValue = moderation;
}
switch(moderationValue){
// Pre-moderation: New comments are shown in the moderator queues immediately.
case 'pre':
return Comment.findByStatus('').then((comments) => {
return comments;
});
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
case 'post':
return Comment.findByStatusByActionType('', 'flag').then((comments) => {
return comments;
});
default:
throw new Error('Moderation setting not found.');
}
});
};
//==============================================================================
// Update Statics
//==============================================================================
+8 -1
View File
@@ -5,7 +5,6 @@ const SettingSchema = new Schema({
id: {type: String, default: '1'},
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'}
}, {
_id: false,
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
@@ -20,6 +19,14 @@ SettingSchema.statics.getSettings = function () {
return this.findOne({id: '1'});
};
/**
* gets the moderation settings and sends it back
* @return {Promise} moderation the settings for how to moderate comments
*/
SettingSchema.statics.getModerationSetting = function () {
return this.findOne({id: '1'}).select('moderation');
};
/**
* this will update the settings object with whatever you pass in
* @param {object} setting a hash of whatever settings you want to update
+3
View File
@@ -49,6 +49,7 @@
"commander": "^2.9.0",
"debug": "^2.2.0",
"ejs": "^2.5.2",
"eslint-config-postcss": "^2.0.2",
"express": "^4.14.0",
"mongoose": "^4.6.5",
"morgan": "^1.7.0",
@@ -62,8 +63,10 @@
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-plugin-transform-class-properties": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-object-assign": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-polyfill": "^6.16.0",
"babel-preset-es2015": "^6.18.0",
+7 -1
View File
@@ -27,6 +27,7 @@ router.get('/:comment_id', (req, res, next) => {
// Moderation Queues Routes
//==============================================================================
// Get all the comments that have that action_type over them.
router.get('/action/:action_type', (req, res, next) => {
Comment.findByActionType(req.params.action_type).then((comments) => {
res.status(200).json(comments);
@@ -35,6 +36,7 @@ router.get('/action/:action_type', (req, res, next) => {
});
});
// Get all the comments that were rejected.
router.get('/status/rejected', (req, res, next) => {
Comment.findByStatus('rejected').then((comments) => {
res.status(200).json(comments);
@@ -43,8 +45,12 @@ router.get('/status/rejected', (req, res, next) => {
});
});
// Returns back all the comments that are in the moderation queue. The moderation queue is pre or post moderated,
// depending on the settings. The :moderation overwrites this settings.
// Pre-moderation: New comments are shown in the moderator queues immediately.
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
router.get('/status/pending', (req, res, next) => {
Comment.findByStatus('').then((comments) => {
Comment.moderationQueue(req.query.moderation).then((comments) => {
res.status(200).json(comments);
}).catch(error => {
next(error);
+67 -21
View File
@@ -1,30 +1,73 @@
require('../utils/mongoose');
const Comment = require('../../models/comment');
const User = require('../../models/user');
const Action = require('../../models/action');
const Setting = require('../../models/setting');
const settings = {id: '1', moderation: 'pre'};
const expect = require('chai').expect;
describe('Comment: models', () => {
let mockComments;
const comments = [{
body: 'comment 10',
asset_id: '123',
status: '',
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
status: 'accepted',
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
status: 'rejected',
parent_id: '',
author_id: '456',
id: '3'
}];
const users = [{
id: '123',
display_name: 'Ana',
}, {
id: '456',
display_name: 'Maria',
}];
const actions = [{
action_type: 'flag',
item_id: comments[0].id,
item_type: 'comment',
user_id: '123'
}, {
action_type: 'like',
item_id: comments[1].id,
item_type: 'comment',
user_id: '456'
}];
beforeEach(() => {
return Comment.create([{
body: 'comment 10',
asset_id: '123'
}, {
body: 'comment 20',
asset_id: '123'
}, {
body: 'comment 30',
asset_id: '456'
}]).then((comments) => {
mockComments = comments;
return Setting.create(settings).then(() => {
return Comment.create(comments);
}).then(() => {
return User.create(users);
}).then(() => {
return Action.create(actions);
});
});
describe('#findById()', () => {
it('should find a comment by id', () => {
return Comment.findById(mockComments[0].id).then((result) => {
expect(result).to.have.property('body')
.and.to.equal('comment 10');
return Comment.findById('1').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('body', 'comment 10');
});
});
});
@@ -37,13 +80,16 @@ describe('Comment: models', () => {
if (a.body < b.body) {return -1;}
else {return 1;}
});
expect(result[0]).to.have.property('body')
.and.to.equal('comment 10');
expect(result[1]).to.have.property('body')
.and.to.equal('comment 20');
expect(result[0]).to.have.property('body', 'comment 10');
expect(result[1]).to.have.property('body', 'comment 20');
});
});
});
// });
describe('#moderationQueue()', () => {
it('should find an array of new comments to moderate when pre-moderation');
it('should find an array of new comments to moderate when post-moderation');
it('should find an array of new comments to moderate when pre-moderation in settings');
it('should find an array of new comments to moderate when post-moderation in settings');
it('should fail when the moderation is not pre or post');
});
});
+7
View File
@@ -14,6 +14,13 @@ const Comment = require('../../../../models/comment');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
beforeEach(() => {
return Setting.create(settings);
});
describe('Get /comments', () => {
const comments = [{
id: 'abc',