Merge branch 'master' into update-on-post-comment

This commit is contained in:
David Jay
2017-02-15 17:56:44 -05:00
committed by GitHub
30 changed files with 194 additions and 257 deletions
-2
View File
@@ -4,7 +4,6 @@ import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-ro
import Streams from 'containers/Streams/Streams';
import Configure from 'containers/Configure/Configure';
import LayoutContainer from 'containers/LayoutContainer';
import CommentStream from 'containers/CommentStream/CommentStream';
import InstallContainer from 'containers/Install/InstallContainer';
import CommunityContainer from 'containers/Community/CommunityContainer';
@@ -16,7 +15,6 @@ const routes = (
<Route exact path="/admin/install" component={InstallContainer}/>
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={ModerationContainer} />
<Route path='embed' component={CommentStream} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
-103
View File
@@ -1,103 +0,0 @@
import coralApi from '../../../coral-framework/helpers/response';
import * as commentTypes from '../constants/comments';
import * as actionTypes from '../constants/actions';
function addUsersCommentsActions (dispatch, {comments, users, actions}) {
dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions});
}
// Get comments to fill each of the three lists on the mod queue
export const fetchModerationQueueComments = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return Promise.all([
coralApi('/queue/comments/premod'),
coralApi('/queue/users/flagged'),
coralApi('/queue/comments/rejected'),
coralApi('/queue/comments/flagged')
])
.then(([premodComments, pendingUsers, rejected, flagged]) => {
/* Combine seperate calls into a single object */
flagged.comments.forEach(comment => comment.flagged = true);
return {
comments: [...premodComments.comments, ...rejected.comments, ...flagged.comments],
users: [...premodComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users],
actions: [...premodComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions]
};
})
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
export const fetchPremodQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/comments/premod')
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
export const fetchPendingUsersQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/users/flagged')
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
export const fetchRejectedQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/comments/rejected')
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
export const fetchFlaggedQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/comments/flagged')
.then(results => {
results.comments.forEach(comment => comment.flagged = true);
return results;
})
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
// Create a new comment
export const createComment = (name, body) => {
return (dispatch) => {
const formData = {body, name};
return coralApi('/comments', {method: 'POST', body: formData})
.then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res}))
.catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error}));
};
};
/**
* Action disptacher related to comments
*/
// Update a comment. Now to update a comment we need to send back the whole object
export const updateStatus = (status, comment) => {
return dispatch => {
dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, id: comment.id, status});
return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}})
.then(res => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_SUCCESS, res}))
.catch(error => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_FAILURE, error}));
};
};
export const flagComment = id => (dispatch, getState) => {
dispatch({type: commentTypes.COMMENT_FLAG, id});
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
};
@@ -1,13 +0,0 @@
@custom-media --big-viewport (min-width: 780px);
.container {
max-width: 860px;
margin: 0 auto;
}
@media (--big-viewport) {
.tab {
flex: none;
}
}
@@ -1,66 +0,0 @@
import React from 'react';
import styles from './CommentStream.css';
import {Snackbar} from 'react-mdl';
import {connect} from 'react-redux';
import {createComment, flagComment} from 'actions/comments';
import ModerationList from 'components/ModerationList';
import CommentBox from 'components/CommentBox';
/**
* Renders a comment stream using a ModerationList component
* and adds a box for adding a new comment
*/
class CommentStream extends React.Component {
constructor (props) {
super(props);
this.state = {snackbar: false, snackbarMsg: ''};
this.onSubmit = this.onSubmit.bind(this);
this.onClickAction = this.onClickAction.bind(this);
}
// Fetch the comments before mounting
componentWillMount () {
this.props.dispatch({type: 'COMMENT_STREAM_FETCH'});
}
// Submit the new comment
onSubmit (comment) {
this.props.dispatch(createComment(comment.name, comment.body));
}
// The only action for now is flagging
onClickAction (action, id) {
if (action === 'flag') {
this.props.dispatch(flagComment(id));
clearTimeout(this._snackTimeout);
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
this._snackTimeout = setTimeout(() => this.setState({snackbar: false}), 30000);
}
}
// Render the comment box along with the ModerationList
render ({comments, users}, {snackbar, snackbarMsg}) {
return (
<div className={styles.container}>
<CommentBox onSubmit={this.onSubmit} />
<ModerationList isActive hideActive
singleView={false}
commentIds={comments.ids}
comments={comments.byId}
users={users.byId}
onClickAction={this.onClickAction}
actions={['flag']}
loading={comments.loading} />
<Snackbar active={snackbar}>{snackbarMsg}</Snackbar>
</div>
);
}
}
const mapStateToProps = state => ({
comments: state.comments.toJS(),
users: state.users.toJS()
});
export default connect(mapStateToProps)(CommentStream);
@@ -9,12 +9,13 @@ const ModerationQueue = ({activeTab = 'premod', ...props}) => {
<ul>
{
props.data[activeTab].map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
key={i}
index={i}
comment={comment}
suspectWords={props.suspectWords}
actions={actionsMap[comment.status]}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
@@ -318,6 +318,7 @@ span {
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
font-size: 14px;
@@ -1,6 +1,6 @@
export const actionsMap = {
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
FLAGGED: ['REJECT', 'APPROVE'],
FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
REJECTED: ['APPROVE']
};
@@ -9,7 +9,8 @@ query ModQueue ($asset_id: ID!) {
}
flagged: comments(query: {
action_type: FLAG,
asset_id: $asset_id
asset_id: $asset_id,
statuses: [NONE, PREMOD]
}) {
...commentView
action_summaries {
+6
View File
@@ -26,6 +26,7 @@ import ChangeDisplayNameContainer from '../../coral-sign-in/containers/ChangeDis
import SettingsContainer from 'coral-settings/containers/SettingsContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import LoadMore from './LoadMore';
class Embed extends Component {
@@ -157,6 +158,11 @@ class Embed extends Component {
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
<LoadMore
assetId={asset.id}
comments={asset.comments}
moreComments={asset.commentCount > asset.comments.length}
loadMore={this.props.loadMore}/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
+40
View File
@@ -0,0 +1,40 @@
import React, {PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
import {Button} from 'coral-ui';
const lang = new I18n(translations);
const loadMoreComments = (assetId, comments, loadMore) => {
if (!comments.length) {
return;
}
loadMore({
limit: 10,
cursor: comments[comments.length - 1].created_at,
assetId,
sort: 'REVERSE_CHRONOLOGICAL'
});
};
const LoadMore = ({assetId, comments, loadMore, moreComments}) => (
moreComments
? <Button
className='coral-load-more'
onClick={() => loadMoreComments(assetId, comments, loadMore)}>
{
lang.t('loadMore')
}
</Button>
: null
);
LoadMore.propTypes = {
assetId: PropTypes.string.isRequired,
comments: PropTypes.array.isRequired,
moreComments: PropTypes.bool.isRequired,
loadMore: PropTypes.func.isRequired
};
export default LoadMore;
+4 -4
View File
@@ -47,8 +47,8 @@ class Stream extends React.Component {
return (
<div>
{
comments.map(comment => {
return <Comment
comments.map(comment =>
<Comment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
@@ -63,8 +63,8 @@ class Stream extends React.Component {
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
comment={comment} />;
})
comment={comment} />
)
}
</div>
);
@@ -304,3 +304,16 @@ hr {
.close-comments-alert i.material-icons {
font-size: 16px !important;
}
/* Load More */
button.coral-load-more {
width: 100%;
text-align: center;
color: #FFF;
background-color: #2376D8;
}
button.coral-load-more:hover {
background-color: #4399FF;
}
@@ -0,0 +1,10 @@
query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) {
asset(id: $asset_id) {
id
commentCount
comments(sort: $sort, limit: $limit) {
id
replyCount
}
}
}
@@ -1,5 +1,6 @@
import {graphql} from 'react-apollo';
import STREAM_QUERY from './streamQuery.graphql';
import LOAD_MORE from './loadMore.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
function getQueryVariable(variable) {
@@ -21,6 +22,28 @@ export const queryStream = graphql(STREAM_QUERY, {
variables: {
asset_url: getQueryVariable('asset_url')
}
}),
props: ({data}) => ({
data,
loadMore: ({limit, cursor, parent_id, asset_id, sort}) => {
return data.fetchMore({
query: LOAD_MORE,
variables: {
limit,
cursor,
parent_id,
asset_id,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => ({
...oldData,
asset: {
...oldData.asset,
comments: [...oldData.asset.comments, ...new_top_level_comments]
}
})
});
}
})
});
@@ -0,0 +1,10 @@
#import "../fragments/commentView.graphql"
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER) {
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort}) {
...commentView
replies(limit: 3) {
...commentView
}
}
}
@@ -18,9 +18,9 @@ query AssetQuery($asset_url: String!) {
requireEmailConfirmation
}
commentCount
comments {
comments(limit: 10) {
...commentView
replies {
replies(limit: 3) {
...commentView
}
}
+2
View File
@@ -3,6 +3,7 @@
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
"successNameUpdate": "Your username has been updated",
"contentNotAvailable": "This content is not available",
"loadMore": "View more",
"bannedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"editName": {
"msg": "Your account is currently suspended because your username has been deemed inappropriate. To restore your account, please enter a new username. You may contact moderator@fakeurl.com for more information.",
@@ -35,6 +36,7 @@
"contentNotAvailable": "El contenido no se encuentra disponible",
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"editNameMsg": "",
"loadMore": "Ver más",
"error": {
"emailNotVerified": "Dirección de correo electrónico {0} no verificada.",
"email": "No es un email válido",
-1
View File
@@ -19,7 +19,6 @@ class Stream extends Component {
}
render() {
console.log(this.props);
const {data} = this.props;
return <div>
<button onClick={this.logMeIn.bind(this)}>Login or whatever</button>
+3 -3
View File
@@ -18,7 +18,7 @@ const getCountsByAssetID = (context, asset_ids) => {
$in: asset_ids
},
status: {
$in: [null, 'ACCEPTED']
$in: ['NONE', 'ACCEPTED']
},
parent_id: null
}
@@ -51,7 +51,7 @@ const getCountsByParentID = (context, parent_ids) => {
$in: parent_ids
},
status: {
$in: [null, 'ACCEPTED']
$in: ['NONE', 'ACCEPTED']
}
}
},
@@ -88,7 +88,7 @@ const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_
} else {
comments = comments.where({
status: {
$in: [null, 'ACCEPTED']
$in: ['NONE', 'ACCEPTED']
}
});
}
+3 -3
View File
@@ -11,10 +11,10 @@ const Wordlist = require('../../services/wordlist');
* @param {String} body body of the comment
* @param {String} asset_id asset for the comment
* @param {String} parent_id optional parent of the comment
* @param {String} [status=null] the status of the new comment
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = null) => {
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => {
return CommentsService.publicCreate({
body,
asset_id,
@@ -105,7 +105,7 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
if (charCountEnable && body.length > charCount) {
return 'REJECTED';
}
return moderation === 'PRE' ? 'PREMOD' : null;
return moderation === 'PRE' ? 'PREMOD' : 'NONE';
});
}
+6 -2
View File
@@ -67,6 +67,10 @@ type Tag {
# The statuses that a comment may have.
enum COMMENT_STATUS {
# The comment is not PREMOD, but was not applied a moderation status by a
# moderator.
NONE
# The comment has been accepted by a moderator.
ACCEPTED
@@ -93,7 +97,7 @@ enum ACTION_TYPE {
input CommentsQuery {
# current status of a comment.
statuses: [COMMENT_STATUS]
statuses: [COMMENT_STATUS!]
# asset that a comment is on.
asset_id: ID
@@ -152,7 +156,7 @@ type Comment {
asset: Asset
# The current status of a comment.
status: COMMENT_STATUS
status: COMMENT_STATUS!
# The time when the comment was created
created_at: Date!
+6 -2
View File
@@ -6,7 +6,7 @@ const STATUSES = [
'ACCEPTED',
'REJECTED',
'PREMOD',
null
'NONE'
];
/**
@@ -66,7 +66,11 @@ const CommentSchema = new Schema({
asset_id: String,
author_id: String,
status_history: [StatusSchema],
status: {type: String, default: null},
status: {
type: String,
enum: STATUSES,
default: 'NONE'
},
tags: [TagSchema],
parent_id: String
}, {
+2 -2
View File
@@ -49,7 +49,7 @@
},
"homepage": "https://github.com/coralproject/talk#readme",
"dependencies": {
"apollo-client": "^0.7.3",
"apollo-client": "^0.8.3",
"bcrypt": "^0.8.7",
"body-parser": "^1.15.2",
"cli-table": "^0.3.1",
@@ -83,7 +83,7 @@
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-local": "^1.0.0",
"react-apollo": "^0.8.1",
"react-apollo": "^0.10.0",
"redis": "^2.6.3",
"uuid": "^2.0.3"
},
+1 -1
View File
@@ -52,7 +52,7 @@ router.get('/', (req, res, next) => {
if (user_id) {
query = CommentsService.findByUserId(user_id, authorization.has(req.user, 'ADMIN'));
} else if (status) {
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? null : status));
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? 'NONE' : status));
} else if (action_type) {
query = CommentsService
.findIdsByActionType(action_type)
+3
View File
@@ -6,6 +6,9 @@ set -e
# install selenium
selenium-standalone install --config=./selenium.config.js
# Clear db
mongo test --eval "db.dropDatabase()"
# Init application
./bin/cli setup --defaults
+4 -3
View File
@@ -11,6 +11,7 @@ const STATUSES = [
'ACCEPTED',
'REJECTED',
'PREMOD',
'NONE',
];
module.exports = class CommentsService {
@@ -31,7 +32,7 @@ module.exports = class CommentsService {
body,
asset_id,
parent_id,
status = null,
status = 'NONE',
author_id
} = comment;
@@ -146,7 +147,7 @@ module.exports = class CommentsService {
* @param {String} status status of the comment to search for
* @return {Promise} resovles to comment array
*/
static findByStatus(status = null) {
static findByStatus(status = 'NONE') {
return CommentModel.find({status});
}
@@ -155,7 +156,7 @@ module.exports = class CommentsService {
* @param {String} asset_id
* @return {Promise}
*/
static moderationQueue(status = null, asset_id = null) {
static moderationQueue(status = 'NONE', asset_id = null) {
// Fetch the comments with statuses.
let comments = CommentModel.find({status});
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports.comments = (comments) => Assets.findOrCreateByUrl(globals.baseUrl
comment.asset_id = asset.id;
return comment;
});
return Comments.create(comments);
return Comments.publicCreate(comments);
});
/* Create an array of users */
+3 -3
View File
@@ -43,9 +43,9 @@ const embedStreamCommands = {
},
logout() {
return this
.waitForElementVisible('@logoutButton')
.waitForElementVisible('@logoutButton', 5000)
.click('@logoutButton')
.waitForElementVisible('@signInButton', 2000);
.waitForElementVisible('@signInButton', 5000);
},
postComment(comment = 'Test Comment') {
return this
@@ -108,7 +108,7 @@ module.exports = {
selector: '#coralSignInViewTrigger'
},
logoutButton: {
selector: '.commentStream #logout'
selector: '#coralStream #logout'
},
commentBox: {
selector: '.coral-plugin-commentbox-textarea'
+44 -41
View File
@@ -119,48 +119,51 @@ module.exports = {
});
});
},
'User replies to a comment with premod on': client => {
client.perform((client, done) => {
mocks.settings({moderation: 'PRE'})
// Add a mock user
.then(() => mocks.users([{
username: 'Baby Blue',
email: 'whale@tale.sea',
password: 'krill'
}]))
// Add a mock preapproved comment by that user
.then((user) => mocks.comments([{
body: 'Whales are not fish.',
status: 'accepted',
author_id: user.id
}]))
.then(() => {
// Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamEmbed_iframe');
// Post a reply
client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000)
.click('.coral-plugin-replies-reply-button')
.waitForElementVisible('#replyText')
.setValue('#replyText', mockReply)
.click('.coral-plugin-replies-textarea button')
.waitForElementVisible('#coral-notif', 1000)
// Verify that it appears
.assert.containsText('#coral-notif', 'moderation team');
done();
})
.catch((err) => {
console.log(err);
done();
});
});
},
// Commenting out this test, it fails if the notification is below the fold, which
// happens if too many previous tests have added comments
// 'User replies to a comment with premod on': client => {
// client.perform((client, done) => {
// mocks.settings({moderation: 'PRE'})
//
// // Add a mock user
// .then(() => mocks.users([{
// username: 'BabyBlue',
// email: 'whale@tale.sea',
// password: 'krillaretasty'
// }]))
//
// // Add a mock preapproved comment by that user
// .then((user) => mocks.comments([{
// body: 'Whales are not fish.',
// status: 'ACCEPTED',
// author_id: user.id
// }]))
// .then(() => {
//
// // Load Page
// client.resizeWindow(1200, 800)
// .url(client.globals.baseUrl)
// .frame('coralStreamEmbed_iframe');
//
// // Post a reply
// client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000)
// .click('.coral-plugin-replies-reply-button')
// .waitForElementVisible('#replyText')
// .setValue('#replyText', mockReply)
// .click('.coral-plugin-replies-textarea button')
// .waitForElementVisible('#coral-notif', 1000)
//
// // Verify that it appears
// .assert.containsText('#coral-notif', 'moderation team');
// done();
// })
// .catch((err) => {
// console.log(err);
// done();
// });
// });
// },
'Total comment count premod on': client => {
client.perform((client, done) => {
client.url(client.globals.baseUrl)
+2 -2
View File
@@ -131,7 +131,7 @@ describe('services.CommentsService', () => {
expect(c2).to.not.be.null;
expect(c2.id).to.be.uuid;
expect(c2.status).to.be.null;
expect(c2.status).to.be.equal('NONE');
expect(c3).to.not.be.null;
expect(c3.id).to.be.uuid;
@@ -225,7 +225,7 @@ describe('services.CommentsService', () => {
return CommentsService.findById(comment_id)
.then((c) => {
expect(c.status).to.be.null;
expect(c.status).to.be.equal('NONE');
return CommentsService.pushStatus(comment_id, 'REJECTED', '123');
})