diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js
new file mode 100644
index 000000000..f71730663
--- /dev/null
+++ b/client/coral-admin/src/actions/settings.js
@@ -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});
+ });
+};
diff --git a/client/coral-admin/src/containers/Configure.css b/client/coral-admin/src/containers/Configure.css
index 18f12cacd..98cb0e254 100644
--- a/client/coral-admin/src/containers/Configure.css
+++ b/client/coral-admin/src/containers/Configure.css
@@ -20,6 +20,7 @@
border-radius: 4px;
height: 90px;
margin-bottom: 10px;
+ cursor: pointer;
}
.configSettingEmbed {
diff --git a/client/coral-admin/src/containers/Configure.js b/client/coral-admin/src/containers/Configure.js
index 4c6253eaf..0d4cf58d6 100644
--- a/client/coral-admin/src/containers/Configure.js
+++ b/client/coral-admin/src/containers/Configure.js
@@ -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
-
+
+
+
Enable pre-moderation
+ {/*
Include Comment Stream Description for Readers
@@ -39,6 +63,7 @@ class Configure extends React.Component {
error='Input is not a number!'
label='Maximum Characters' />
+ */}
;
}
@@ -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 (
@@ -94,12 +123,14 @@ class Configure extends React.Component {
icon='code'>Embed Comment Stream
-
{pageTitle}
+ { 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);
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index 3c0ddc924..63176ca3b 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -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
});
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
new file mode 100644
index 000000000..20cd024f5
--- /dev/null
+++ b/client/coral-admin/src/reducers/settings.js
@@ -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);
+};
diff --git a/models/comment.js b/models/comment.js
index 3aad5d978..3f99bcb6a 100644
--- a/models/comment.js
+++ b/models/comment.js
@@ -57,13 +57,30 @@ CommentSchema.statics.findById = function(id) {
};
/**
- * Finds a comment by the asset_id.
+ * Finds ALL the comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
*/
CommentSchema.statics.findByAssetId = function(asset_id) {
return Comment.find({asset_id});
};
+/**
+ * Finds the accepted comments by the asset_id.
+ * get the comments that are accepted.
+ * @param {String} asset_id identifier of the asset which owns the comments (uuid)
+*/
+CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
+ return Comment.find({asset_id: asset_id, status:'accepted'});
+};
+
+/**
+ * Finds the new and accepted comments by the asset_id.
+ * @param {String} asset_id identifier of the asset which owns the comments (uuid)
+*/
+CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
+ return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}});
+};
+
/**
* Find comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
@@ -74,14 +91,46 @@ 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(moderation) {
+ switch(moderation){
+ // 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
//==============================================================================
diff --git a/models/setting.js b/models/setting.js
index b47b6e9e2..15c457307 100644
--- a/models/setting.js
+++ b/models/setting.js
@@ -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
diff --git a/package.json b/package.json
index 694922148..5563c1f36 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"body-parser": "^1.15.2",
"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",
diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js
index b3e2bdee7..73ae3dea3 100644
--- a/routes/api/comments/index.js
+++ b/routes/api/comments/index.js
@@ -1,6 +1,8 @@
const express = require('express');
const Comment = require('../../../models/comment');
+const Setting = require('../../../models/setting');
+
const router = express.Router();
//==============================================================================
@@ -27,6 +29,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 +38,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,9 +47,19 @@ 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) => {
- res.status(200).json(comments);
+ Setting.getModerationSetting().then(function({moderation}){
+ let moderationValue = req.query.moderation;
+ if (typeof moderationValue === 'undefined' || moderationValue === undefined) {
+ moderationValue = moderation;
+ }
+ Comment.moderationQueue(moderationValue).then((comments) => {
+ res.status(200).json(comments);
+ });
}).catch(error => {
next(error);
});
@@ -62,13 +76,6 @@ router.post('/', (req, res, next) => {
}).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) => {
diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js
index 67216a252..4fe266294 100644
--- a/routes/api/stream/index.js
+++ b/routes/api/stream/index.js
@@ -4,12 +4,26 @@ const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
+const Setting = require('../../../models/setting');
+
const router = express.Router();
+// Find all the comments by a specific asset_id.
+// . if pre: get the comments that are accepted.
+// . if post: get the comments that are new and accepted.
router.get('/', (req, res, next) => {
+ const commentsPromise = Setting.getModerationSetting().then(({moderation}) => {
+ switch(moderation){
+ case 'pre':
+ return Comment.findAcceptedByAssetId(req.query.asset_id);
+ case 'post':
+ return Comment.findAcceptedAndNewByAssetId(req.query.asset_id);
+ default:
+ throw new Error('Moderation setting not found.');
+ }
+ });
- const commentsPromise = Comment.findByAssetId(req.query.asset_id);
-
+ // Get all the users and actions for those comments.
commentsPromise.then(comments => {
return Promise.all([
comments,
@@ -17,7 +31,7 @@ router.get('/', (req, res, next) => {
Action.findByItemIdArray(comments.map((comment) => comment.id))
]);
}).then(([comments, users, actions]) => {
- res.json([...comments, ...users, ...actions]);
+ res.status(200).json([...comments, ...users, ...actions]);
}).catch(error => {
next(error);
});
diff --git a/tests/models/comment.js b/tests/models/comment.js
index b3f0c0f30..2a8225c72 100644
--- a/tests/models/comment.js
+++ b/tests/models/comment.js
@@ -1,49 +1,136 @@
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: '',
+ parent_id: '',
+ author_id: '456',
+ id: '3'
+ }, {
+ body: 'comment 40',
+ asset_id: '123',
+ status: 'rejected',
+ parent_id: '',
+ author_id: '456',
+ id: '4'
+ }];
+
+ const users = [{
+ id: '123',
+ display_name: 'Ana',
+ }, {
+ id: '456',
+ display_name: 'Maria',
+ }];
+
+ const actions = [{
+ action_type: 'flag',
+ item_id: '3',
+ item_type: 'comment',
+ user_id: '123'
+ }, {
+ action_type: 'like',
+ item_id: '1',
+ 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');
});
});
});
describe('#findByAssetId()', () => {
- it('should find an array of comments by asset id', () => {
+ it('should find an array of all comments by asset id', () => {
return Comment.findByAssetId('123').then((result) => {
+ expect(result).to.have.length(3);
+ result.sort((a, b) => {
+ if (a.body < b.body) {return -1;}
+ else {return 1;}
+ });
+ expect(result[0]).to.have.property('body', 'comment 10');
+ expect(result[1]).to.have.property('body', 'comment 20');
+ expect(result[2]).to.have.property('body', 'comment 40');
+ });
+ });
+ it('should find an array of accepted comments by asset id', () => {
+ return Comment.findAcceptedByAssetId('123').then((result) => {
+ expect(result).to.have.length(1);
+ result.sort((a, b) => {
+ if (a.body < b.body) {return -1;}
+ else {return 1;}
+ });
+ expect(result[0]).to.have.property('body', 'comment 20');
+ });
+ });
+ it('should find an array of new and accepted comments by asset id', () => {
+ return Comment.findAcceptedAndNewByAssetId('123').then((result) => {
expect(result).to.have.length(2);
result.sort((a, b) => {
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');
});
});
});
-
- // });
+ describe('#moderationQueue()', () => {
+ it('should find an array of new comments to moderate when pre-moderation', () => {
+ return Comment.moderationQueue('pre').then((result) => {
+ expect(result).to.not.be.null;
+ expect(result).to.have.lengthOf(2);
+ });
+ });
+ it('should find an array of new comments to moderate when post-moderation', () => {
+ return Comment.moderationQueue('post').then((result) => {
+ expect(result).to.not.be.null;
+ expect(result).to.have.lengthOf(1);
+ expect(result[0]).to.have.property('body', 'comment 30');
+ });
+ });
+ // it('should fail when the moderation is not pre or post', () => {
+ // return Comment.moderationQueue('any').catch(function(error) {
+ // expect(error).to.not.be.null;
+ // });
+ // });
+ });
});
diff --git a/tests/models/setting.js b/tests/models/setting.js
index e2600fb0f..f50110635 100644
--- a/tests/models/setting.js
+++ b/tests/models/setting.js
@@ -29,4 +29,11 @@ describe('Setting: model', () => {
});
});
+ describe('#getModerationSetting', () => {
+ it('should return the moderation settings', () => {
+ return Setting.getModerationSetting().then(({moderation}) => {
+ expect(moderation).not.to.be.null;
+ });
+ });
+ });
});
diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js
index 378860bcb..35959a182 100644
--- a/tests/routes/api/comments/index.js
+++ b/tests/routes/api/comments/index.js
@@ -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',
@@ -133,6 +140,30 @@ describe('Get moderation queues rejected, pending, flags', () => {
});
});
+ it('should return all the pending comments as pre moderated', function(done){
+ chai.request(app)
+ .get('/api/v1/comments/status/pending')
+ .query({'moderation': 'pre'})
+ .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 pending comments as post moderated', function(done){
+ chai.request(app)
+ .get('/api/v1/comments/status/pending')
+ .query({'moderation': 'post'})
+ .end(function(err, res){
+ expect(err).to.be.null;
+ expect(res).to.have.status(200);
+ expect(res.body).to.have.lengthOf(0);
+ done();
+ });
+ });
+
it('should return all the flagged comments', function(done){
chai.request(app)
.get('/api/v1/comments/action/flag')
diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js
index b1348e88a..97d9b3af9 100644
--- a/tests/routes/api/stream/index.js
+++ b/tests/routes/api/stream/index.js
@@ -12,21 +12,38 @@ const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const Comment = require('../../../../models/comment');
+const Setting = require('../../../../models/setting');
+
describe('api/stream: routes', () => {
+
+ const settings = {id: '1', moderation: 'pre'};
+
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
- author_id: '123'
+ author_id: '123',
+ parent_id: '',
+ status: 'accepted'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
- author_id: '456'
+ author_id: '456',
+ parent_id: '',
+ status: ''
+ }, {
+ id: 'uio',
+ body: 'comment 30',
+ asset_id: 'asset',
+ author_id: '456',
+ parent_id: '',
+ status: ''
}, {
id: 'hij',
- body: 'comment 30',
- asset_id: '456'
+ body: 'comment 40',
+ asset_id: '456',
+ status: 'rejected'
}];
const users = [{
@@ -46,10 +63,12 @@ describe('api/stream: routes', () => {
}];
beforeEach(() => {
- return Comment.create(comments).then(() => {
- return User.create(users);
- }).then(() => {
- return Action.create(actions);
+ return Setting.create(settings).then(() => {
+ return Comment.create(comments).then(() => {
+ return User.create(users);
+ }).then(() => {
+ return Action.create(actions);
+ });
});
});
@@ -60,6 +79,7 @@ describe('api/stream: routes', () => {
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
+ expect(res.body.length).to.equal(3);
done();
});
});