Merge branch 'wordlist' of github.com:coralproject/talk into wordlist

This commit is contained in:
David Jay
2016-12-06 12:54:39 -05:00
48 changed files with 1582 additions and 595 deletions
+3 -1
View File
@@ -1,10 +1,12 @@
node_modules
npm-debug.log
npm-debug.log*
dist
!dist/coral-admin
dist/coral-admin/bundle.js
tests/e2e/reports
.DS_Store
*.iml
dump.rdb
.env
gaba.cfg
.idea/
+95
View File
@@ -0,0 +1,95 @@
# Contribution Guide
We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together.
## Doing the Work
We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content.
### Code Reviews
One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants.
Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand.
Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace.
**Is the code clear, efficient and a pleasure to read?**
Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code.
Thinking like a computer, you could ask:
* Is the code using memory efficiently?
* Is data being moved around unnecessarily?
* Are multiple network requests being made where fewer would do?
* Is there excess processing happening in a synchronous flow that may disrupt user experience?
* Are there large libraries included for small gains?
Then, returning to your human roots... Is the code readable?
* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward?
* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices?
* Are the files separated logically such that each one contains a clear concept of code?
**Is the API documentation up to date? Are all client calls written against the docs?**
We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation.
* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs.
* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled?
**Is there sufficient test coverage?**
Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests)
* Can you a sense of the logic behind the code by reading the tests?
* Can you see both what should happen and what should _never, ever_ be allowed to happen?
* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)?
### Forking, Branching and Merging
Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added.
Releases are _tagged_ off the master branch.
Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution.
* At the outset of a piece of work, a branch or fork is made from master.
* The work is done in that fork.
* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.)
* At least one other contributor to the project must review all code (see Code Reviews below.)
* If there are merge conflicts with master, merge master into the branch.
* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.)
* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master.
* Merge to master, but _you're not quite done yet!_
* Deploy master to staging (or have a core member do so.)
* Ensure that all your changes are working on staging.
* Have your reviewer verify the same.
* ... aaaand the work is delivered!
## Continuous Integration
We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk)
Our pipeline will _test_, _lint_, and _build_ all pushes to the repo.
Any branch not passing CI will not be merged into master.
If you're working in a fork, please run each of the steps locally before submitting a PR.
## Coding Style
### API Design
When building APIs, we follow these principles:
* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations.
* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object.
* Put non-required, flexible variables into query params, required/identity based values in request params.
+6 -1
View File
@@ -17,7 +17,8 @@ const util = require('../util');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.TALK_PORT || '3000');
const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000'));
app.set('port', port);
@@ -95,6 +96,10 @@ function startApp() {
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
+15
View File
@@ -76,6 +76,21 @@ cache.get = (key) => new Promise((resolve, reject) => {
});
});
/**
* This invalidates a cached entry in the cache.
* @param {Mixed} key Either an array of items composing a key or a string
* @return {Promise}
*/
cache.invalidate = (key) => new Promise((resolve, reject) => {
cache.client.del(keyfunc(key), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
/**
* This sets a value on the key with the expiry and then resolves once it is
* done.
+155 -134
View File
@@ -7,6 +7,7 @@ import {
Notification,
notificationActions,
authActions,
configActions
} from '../../coral-framework';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
@@ -21,14 +22,17 @@ import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import CloseCommentsInfo from '../../coral-framework/components/CloseCommentsInfo';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout} = authActions;
const {logout, showSignInDialog} = authActions;
const {updateOpenStatus} = configActions;
class CommentStream extends Component {
@@ -40,6 +44,7 @@ class CommentStream extends Component {
};
this.changeTab = this.changeTab.bind(this);
this.toggleStatus = this.toggleStatus.bind(this);
}
changeTab (tab) {
@@ -48,6 +53,10 @@ class CommentStream extends Component {
});
}
toggleStatus () {
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
}
static propTypes = {
items: PropTypes.object.isRequired,
addItem: PropTypes.func.isRequired,
@@ -56,31 +65,22 @@ class CommentStream extends Component {
componentDidMount () {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
const pym = new Pym.Child({polling: 100});
this.pym = new Pym.Child({polling: 100});
if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) {
this.props.getStream(pym.parentUrl);
} else {
this.props.getStream(window.location);
}
const path = /https?\:\/\/([^?#]+)/.exec(this.pym.parentUrl);
this.props.getStream(path[1] || window.location);
this.path = path;
this.pym.sendMessage('childReady');
}
render () {
if (Object.keys(this.props.items).length === 0) {
// Loading mock asset
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest');
}
// TODO: Replace teststream id with id from params
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {actions, users, comments} = this.props.items;
const {loggedIn, user, showSignInDialog} = this.props.auth;
const {status} = this.props.config;
const {activeTab} = this.state;
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
@@ -90,131 +90,145 @@ class CommentStream extends Component {
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
<Tab>Settings</Tab>
<Tab>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
{/* Add to the restricted param a boolean if the user is suspended*/}
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
<TabContent show={activeTab === 0}>
<div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
{
status === 'open'
? <div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
author={user}
/>
</div>
: <p>Comments are closed for this thread.</p>
}
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
showSignInDialog={this.props.showSignInDialog}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
articleURL={this.path}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={commentId}
id={rootItemId}
author={user}
parent_id={commentId}
premod={this.props.config.moderation}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
articleURL={this.path}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
premod={this.props.config.moderation}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId} id={`c_${replyId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId} id={`c_${replyId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={replyId}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
</div>;
})
</div>;
})
}
</div>;
})
}
})
}
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
@@ -222,7 +236,12 @@ class CommentStream extends Component {
userData={this.props.userData}
showSignInDialog={this.props.handleSignInDialog}
/>
{!loggedIn && <SignInContainer noButton/>}
</TabContent>
<TabContent show={activeTab === 2}>
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
<RestrictedContent restricted={!loggedIn}>
<CloseCommentsInfo onClick={this.toggleStatus} status={status} />
</RestrictedContent>
</TabContent>
</RestrictedContent>
<Notification
@@ -232,7 +251,7 @@ class CommentStream extends Component {
/>
</div>
:
<Spinner/>
<Spinner/>
}
</div>;
}
@@ -253,11 +272,13 @@ const mapDispatchToProps = (dispatch) => ({
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
showSignInDialog: () => dispatch(showSignInDialog()),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout()),
updateStatus: status => dispatch(updateOpenStatus(status))
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
+43 -2
View File
@@ -56,12 +56,14 @@ hr {
/* Info Box Styles */
.coral-plugin-infobox-info {
position: fixed;
top: 0;
border: 0;
background: rgb(105,105,105);
color: white;
border-radius: 2px;
width: 100%;
text-align: center;
padding: 10px;
margin-bottom: 10px;
font-weight: bold;
display: block;
}
@@ -195,3 +197,42 @@ hr {
float: right;
margin: 8px;
}
/* Close comments */
.close-comments-intro-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.close-comments-intro-wrapper button {
width: 300px;
margin-left: 20px;
}
.close-comments-intro-wrapper button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.close-comments-message {
box-sizing: border-box;
width: 100%;
height: 100px;
}
.close-comments-confirm-wrapper {
float: right;
}
.close-comments-alert {
background-color: #d65344;
color: white;
font-size: 16px;
padding: 5px;
}
.close-comments-alert i.material-icons {
font-size: 16px !important;
}
+18
View File
@@ -0,0 +1,18 @@
import coralApi from '../helpers/response';
/* Config Actions */
/**
* Action name constants
*/
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
export const updateOpenStatus = status => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'})
.then(() => dispatch({type: status === 'open' ? OPEN_COMMENTS : CLOSE_COMMENTS}));
};
@@ -0,0 +1,23 @@
import React from 'react';
import {Button} from 'coral-ui';
export default ({status, onClick}) => (
status === 'open' ? (
<div className="close-comments-intro-wrapper">
<p>
This comment stream is currently open. By closing this comment stream,
no new comments may be submitted and all previous comments will still
be displayed.
</p>
<Button onClick={onClick}>Close Stream</Button>
</div>
) : (
<div className="close-comments-intro-wrapper">
<p>
This comment stream is currently closed. By opening this comment stream,
new comments may be submitted and displayed
</p>
<Button onClick={onClick}>Open Stream</Button>
</div>
)
);
+2
View File
@@ -4,6 +4,7 @@ import * as itemActions from './actions/items';
import I18n from './modules/i18n/i18n';
import * as notificationActions from './actions/notification';
import * as authActions from './actions/auth';
import * as configActions from './actions/config';
export {
Notification,
@@ -12,4 +13,5 @@ export {
I18n,
notificationActions,
authActions,
configActions
};
+14 -3
View File
@@ -1,19 +1,30 @@
/* @flow */
import {Map} from 'immutable';
import * as actions from '../actions/items';
import * as actions from '../actions/config';
const initialState = Map({
features: Map({})
features: Map({}),
status: 'open'
});
export default (state = initialState, action) => {
switch(action.type) {
// Override config if worked
case actions.UPDATE_SETTINGS:
return action.config;
case actions.OPEN_COMMENTS:
return state.set('status', 'open');
case actions.CLOSE_COMMENTS:
return state.set('status', 'closed');
case actions.ADD_ITEM:
return action.item_type === 'assets' ?
state.set('status', action.item.status)
: state;
default:
return state;
}
@@ -82,6 +82,7 @@ class CommentBox extends Component {
{ author && (
<Button
cStyle='darkGrey'
className={`${name}-button`}
onClick={this.postComment}>
{lang.t('post')}
</Button>
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!flagged) {
+2 -1
View File
@@ -4,10 +4,11 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
showSignInDialog();
return;
}
if (!liked) {
@@ -44,7 +44,7 @@ const SignInContent = ({handleChange, formData, ...props}) => (
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button type="submit" cStyle="black" className={styles.signInButton} full>
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
@@ -56,7 +56,7 @@ const SignInContent = ({handleChange, formData, ...props}) => (
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')}>
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
@@ -69,7 +69,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
/>
<div className={styles.action}>
{ !props.auth.isLoading && !props.auth.successSignUp && (
<Button type="submit" cStyle="black" className={styles.signInButton} full>
<Button type="submit" cStyle="black" id='coralSignUpButton' className={styles.signInButton} full>
{lang.t('signIn.signUp')}
</Button>
)}
@@ -132,7 +132,9 @@ class SignInContainer extends Component {
const {auth, showSignInDialog, noButton} = this.props;
return (
<div>
{!noButton && <Button onClick={showSignInDialog} full> Sign in to comment</Button>}
{!noButton && <Button id='coralSignInButton' onClick={showSignInDialog} full>
Sign in to comment
</Button>}
<SignDialog
open={auth.showSignInDialog}
view={auth.view}
+29
View File
@@ -1,5 +1,6 @@
const mongoose = require('../mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const Schema = mongoose.Schema;
const ActionSchema = new Schema({
@@ -66,6 +67,34 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
});
};
/**
* Fetches the action summaries for the given asset, and comments around the
* given user id.
* @param {[type]} asset_id [description]
* @param {[type]} comments [description]
* @param {String} [current_user_id=''] [description]
* @return {[type]} [description]
*/
ActionSchema.statics.getActionSummariesFromComments = (asset_id = '', comments, current_user_id = '') => {
// Get the user id's from the author id's as a unique array that gets
// sorted.
let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
// Fetch the actions for pretty much everything at this point.
return Action.getActionSummaries(_.uniq([
// Actions can be on assets...
asset_id,
// Comments...
...comments.map((comment) => comment.id),
// Or Authors...
...userIDs
].filter((e) => e)), current_user_id);
};
/**
* Returns summaries of actions for an array of ids
* @param {String} ids array of user identifiers (uuid)
+26 -6
View File
@@ -1,6 +1,8 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const Setting = require('./setting');
const uuid = require('uuid');
const AssetSchema = new Schema({
@@ -34,7 +36,11 @@ const AssetSchema = new Schema({
subsection: String,
author: String,
publication_date: Date,
modified_date: Date
modified_date: Date,
status: {
type: String,
default: 'open'
}
}, {
versionKey: false,
timestamps: {
@@ -54,11 +60,6 @@ AssetSchema.index({
background: true
});
/**
* Search for assets. Currently only returns all.
*/
AssetSchema.statics.search = (query) => Asset.find(query);
/**
* Finds an asset by its id.
* @param {String} id identifier of the asset (uuid).
@@ -71,6 +72,25 @@ AssetSchema.statics.findById = (id) => Asset.findOne({id});
*/
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
/**
* Retrieves the settings given an asset query and rectifies it against the
* global settings.
* @param {Promise} assetQuery an asset query that returns a single asset.
* @return {Promise}
*/
AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
Setting.retrieve(),
assetQuery
]).then(([settings, asset]) => {
// If the asset exists and has settings then return the merged object.
if (asset && asset.settings) {
return Object.assign({}, settings, asset.settings);
}
return settings;
});
/**
* Finds a asset by its url.
*
+204 -92
View File
@@ -1,9 +1,37 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const Action = require('./action');
const Schema = mongoose.Schema;
/**
* The Mongo schema for a Comment Status.
* @type {Schema}
*/
const StatusSchema = new Schema({
type: {
type: String,
enum: [
'accepted',
'rejected',
'premod',
],
},
// The User ID of the user that assigned the status.
assigned_by: {
type: String,
default: null
},
created_at: Date
}, {
_id: false
});
/**
* The Mongo schema for a Comment.
* @type {Schema}
*/
const CommentSchema = new Schema({
id: {
type: String,
@@ -17,11 +45,7 @@ const CommentSchema = new Schema({
},
asset_id: String,
author_id: String,
status: {
type: String,
enum: ['accepted', 'rejected', ''],
default: ''
},
status: [StatusSchema],
parent_id: String
}, {
timestamps: {
@@ -30,90 +54,168 @@ const CommentSchema = new Schema({
}
});
//==============================================================================
// Find Statics
//==============================================================================
/**
* toJSON overrides to remove fields from the json
* output.
*/
CommentSchema.options.toJSON = {};
CommentSchema.options.toJSON.hide = '_id status';
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* toJSON overrides to remove fields from the json
* output.
*/
CommentSchema.options.toJSON = {};
CommentSchema.options.toJSON.hide = '_id';
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* Sets up a virtual getter function on a comment such that when you try and
* access the `comment.last_status` it returns the last status in the array
* of status's on the comment, or `null` if there was no status.
*/
CommentSchema.virtual('last_status').get(function() {
if (this.status && this.status.length > 0) {
return this.status[this.status.length - 1].type;
}
return null;
});
/**
* Creates a new Comment that came from a public source.
* @param {Mixed} comment either a single comment or an array of comments.
* @return {Promise}
*/
CommentSchema.statics.publicCreate = (comment) => {
// Check to see if this is an array of comments, if so map it out.
if (Array.isArray(comment)) {
return Promise.all(comment.map(Comment.publicCreate));
}
const {
body,
asset_id,
parent_id,
status = false,
author_id
} = comment;
comment = new Comment({
body,
asset_id,
parent_id,
status: status ? [{
type: status,
created_at: new Date()
}] : [],
author_id
});
return comment.save();
};
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
* @return {Promise}
*/
CommentSchema.statics.findById = function(id) {
return Comment.findOne({'id': id});
};
CommentSchema.statics.findById = (id) => Comment.findOne({id});
/**
* Finds ALL the comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
* @return {Promise}
*/
CommentSchema.statics.findByAssetId = function(asset_id) {
return Comment.find({asset_id});
};
CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
asset_id
});
/**
* Finds the accepted comments by the asset_id.
* get the comments that are accepted.
* 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)
* @return {Promise}
*/
CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
return Comment.find({asset_id: asset_id, status:'accepted'});
};
CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
asset_id,
'status.type': 'accepted'
});
/**
* Finds the new and accepted comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
* @return {Promise}
*/
CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}});
};
CommentSchema.statics.findAcceptedAndNewByAssetId = (asset_id) => Comment.find({
asset_id,
$or: [
{
'status.type': 'accepted'
},
{
status: {
$size: 0
}
}
]
});
/**
* Find comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
* @return {Promise}
*/
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;})}
});
});
};
CommentSchema.statics.findByActionType = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => Comment.find({
id: {
$in: actions.map((a) => a.item_id)
}
}));
/**
* Find not moderated comments by an action that was performed on them.
* Find comment id's where the action type matches the argument.
* @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
* @return {Promise}
*/
CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
return Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => {
return Comment.find({
status: status,
id: {
$in: actions.map(a => a.item_id)
}
});
});
};
CommentSchema.statics.findIdsByActionType = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => actions.map(a => a.item_id));
/**
* Find comments by their status.
* @param {String} status the status of the comment to search for
* @return {Promise}
*/
CommentSchema.statics.findByStatus = function(status) {
return Comment.find({
status: status === 'new' ? '' : status
});
CommentSchema.statics.findByStatus = (status = false) => {
let q = {};
if (status) {
q['status.type'] = status;
} else {
q.status = {$size: 0};
}
return Comment.find(q);
};
/**
@@ -121,39 +223,59 @@ CommentSchema.statics.findByStatus = function(status) {
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
* @return {Promise}
*/
CommentSchema.statics.moderationQueue = function(moderation) {
switch(moderation){
CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
// Pre-moderation: New comments are shown in the moderator queues immediately.
case 'pre':
return Comment.findByStatus('').then((comments) => {
return comments;
});
/**
* This adds the asset_id requirement to the query if the asset_id is defined.
*/
const assetIDWrap = (query) => {
if (asset_id) {
query = query.where('asset_id', asset_id);
}
// 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;
});
return query;
};
default:
return Promise.reject(Error('Moderation setting not found.'));
// Decide on whether or not we need to load extended options for the
// moderation based on the moderation options.
let comments;
if (moderation === 'pre') {
// Pre-moderation: New comments are shown in the moderator queues immediately.
comments = assetIDWrap(CommentSchema.statics.findByStatus('premod'));
} else {
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
comments = CommentSchema.statics.findIdsByActionType('flag')
.then((ids) => assetIDWrap(Comment.find({
id: {
$in: ids
}
})));
}
};
//==============================================================================
// Update Statics
//==============================================================================
return comments;
};
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* Pushes a new status in for the user.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* @param {String} assigned_by the user id for the user who performed the
* moderation action
* @return {Promise}
*/
CommentSchema.statics.changeStatus = function(id, status) {
return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}});
};
CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
$push: {
status: {
type: status,
created_at: new Date(),
assigned_by
}
}
});
/**
* Add an action to the comment.
@@ -169,19 +291,13 @@ CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.inse
action_type
});
//==============================================================================
// 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
* @return {Promise}
*/
CommentSchema.statics.removeById = function(id) {
return Comment.remove({'id': id});
};
CommentSchema.statics.removeById = (id) => Comment.remove({id});
/**
* Remove an action from the comment.
@@ -190,22 +306,18 @@ CommentSchema.statics.removeById = function(id) {
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
return Action.remove({
action_type,
item_type: 'comment',
item_id,
user_id
});
};
CommentSchema.statics.removeAction = (item_id, user_id, action_type) => Action.remove({
action_type,
item_type: 'comment',
item_id,
user_id
});
/**
* Returns all the comments in the collection.
* @return {Promise}
*/
CommentSchema.statics.all = () => {
return Comment.find();
};
CommentSchema.statics.all = () => Comment.find();
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
+77 -41
View File
@@ -1,18 +1,33 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const cache = require('../cache');
/**
* this Schema manages application settings that get used on front and backend
* NOTE: when you set a setting here, it will not automatically be exposed to
* the front end. You must add it to the whitelist in the settings route
* in /routes/api/settings/index.js
* SettingSchema manages application settings that get used on front and backend.
* @type {Schema}
*/
const SettingSchema = new Schema({
id: {type: String, default: '1'},
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
infoBoxEnable: {type: Boolean, default: false},
infoBoxContent: {type: String, default: ''},
id: {
type: String,
default: '1'
},
moderation: {
type: String,
enum: [
'pre',
'post'
],
default: 'pre'
},
infoBoxEnable: {
type: Boolean,
default: false
},
infoBoxContent: {
type: String,
default: ''
},
wordlist: [String]
}, {
timestamps: {
@@ -22,49 +37,70 @@ const SettingSchema = new Schema({
});
/**
* this is run once when the app starts to ensure settings are populated
* @return {Promise} null initialize the global settings object
* The Mongo Mongoose object.
*/
SettingSchema.statics.init = function (defaults) {
return this.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
};
const Setting = mongoose.model('Setting', SettingSchema);
/**
* The Setting Service object exposing the Setting model.
* @type {Object}
*/
const SettingService = module.exports = {};
/**
* The selector used to uniquely identify the settings document.
*/
const selector = {id: '1'};
/**
* Cache expiry time in seconds for when the cached entry of the settings object
* expires. 2 minutes.
*/
const EXPIRY_TIME = 60 * 2;
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
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 () {
console.log('Getting moderation setting');
return this.findOne({id: '1'}).select('moderation');
};
/**
* Gets the info box settings and sends it back
* @return {Promise} content the content of the info Box
*/
SettingSchema.statics.getInfoBoxSetting = function () {
return this.findOne({id: '1'}).select('infoBoxEnable', 'infoBoxContent');
};
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => Setting.findOne(selector));
/**
* This will update the settings object with whatever you pass in
* @param {object} setting a hash of whatever settings you want to update
* @return {Promise} settings Promise that resolves to the entire (updated) settings object.
*/
SettingSchema.statics.updateSettings = function (setting) {
// There should only ever be one record unless something has gone wrong.
// In the future we may have multiple records for custom settings for objects/users.
return this.findOneAndUpdate({id: '1'}, {$set: setting}, {new: true});
SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
$set: settings
}, {
upsert: true,
new: true,
setDefaultsOnInsert: true
}).then((settings) => {
// Invalidate the settings cache.
return cache
.set('settings', settings, EXPIRY_TIME)
.then(() => settings);
});
/**
* Filters the document to ensure that the resulting document is indeed ready
* for non authenticated users.
* @param {Object} settings the source settings object
* @return {Object} the filtered settings object
*/
SettingService.public = (settings) => _.pick(settings, ['moderation', 'infoBoxEnable', 'infoBoxContent']);
/**
* This is run once when the app starts to ensure settings are populated.
* @return {Promise} null initialize the global settings object
*/
SettingService.init = (defaults) => {
// Inject the defaults on top of the passed in defaults to ensure that the new
// settings conform to the required selector.
defaults = Object.assign({}, defaults, selector);
// Actually update the settings collection.
return SettingService.update(defaults);
};
const Setting = mongoose.model('Setting', SettingSchema);
module.exports = Setting;
+1 -2
View File
@@ -9,7 +9,6 @@ const SALT_ROUNDS = 10;
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = [
'',
'admin',
'moderator'
];
@@ -106,7 +105,7 @@ UserSchema.index({
* output.
*/
UserSchema.options.toJSON = {};
UserSchema.options.toJSON.hide = 'password profiles roles disabled';
UserSchema.options.toJSON.hide = '_id password profiles roles disabled';
UserSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
+7 -1
View File
@@ -1,7 +1,13 @@
const mongoose = require('mongoose');
const debug = require('debug')('talk:db');
const enabled = require('debug').enabled;
const url = process.env.TALK_MONGO_URL || (process.env.NODE_ENV === 'test' ? 'mongodb://localhost/test' : 'mongodb://localhost/talk');
//Append '-test' to the db if node_env === 'test'
let url = process.env.TALK_MONGO_URL || 'mongodb://localhost/coral-talk';
if (process.env.NODE_ENV === 'test') {
url += '-test';
}
// Use native promises
mongoose.Promise = global.Promise;
+44
View File
@@ -0,0 +1,44 @@
require('babel-core/register');
module.exports = {
'src_folders': './tests/e2e/tests',
'output_folder': './tests/e2e/reports',
'page_objects_path': './tests/e2e/pages',
'globals_path': './tests/e2e/globals',
'custom_commands_path' : '',
'custom_assertions_path' : '',
'selenium': {
'start_process': true,
'server_path': 'node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar',
'log_path': './tests/e2e/reports',
'host': '127.0.0.1',
'port': 6666,
'cli_args': {
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver'
}
},
'test_settings': {
'default': {
'selenium_port': 6666,
'selenium_host': 'localhost',
'silent': true,
'desiredCapabilities': {
'browserName': 'chrome',
'javascriptEnabled': true,
'acceptSslCerts': true,
'webStorageEnabled' : true,
'databaseEnabled' : true,
'applicationCacheEnabled' : false,
'nativeEvents' : true
},
'screenshots' : {
'enabled' : true,
'on_failure' : true,
'on_error' : true,
'path' : './tests/e2e/reports'
},
'exclude': [
]
}
}
};
+10 -15
View File
@@ -11,18 +11,15 @@
"lint-fix": "eslint . --fix",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive tests",
"test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
"pree2e": "NODE_ENV=test ./pree2e.sh",
"e2e": "NODE_ENV=test node_modules/.bin/nightwatch",
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs"
},
"config": {
"pre-git": {
"commit-msg": [],
"pre-commit": [
"npm run lint",
"npm test"
],
"pre-push": [
"npm test"
],
"pre-commit": ["npm run lint", "npm test"],
"pre-push": ["npm test"],
"post-commit": [],
"post-merge": []
}
@@ -31,12 +28,7 @@
"type": "git",
"url": "git+https://github.com/coralproject/talk.git"
},
"keywords": [
"talk",
"coral",
"coralproject",
"ask"
],
"keywords": ["talk", "coral", "coralproject", "ask"],
"author": "",
"license": "Apache-2.0",
"bugs": {
@@ -97,6 +89,7 @@
"eslint-config-standard": "^6.2.1",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-mocha": "^4.7.0",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^6.6.0",
"eslint-plugin-standard": "^2.0.1",
@@ -110,6 +103,8 @@
"material-design-lite": "^1.2.1",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
"nightwatch": "^0.9.9",
"node-fetch": "^1.6.3",
"postcss-loader": "^1.1.0",
"postcss-modules": "^0.5.2",
"postcss-smart-import": "^0.5.1",
@@ -127,11 +122,11 @@
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"selenium-standalone": "^5.8.0",
"style-loader": "^0.13.1",
"supertest": "^2.0.1",
"timeago.js": "^2.0.3",
"webpack": "^1.13.3",
"whatwg-fetch": "^1.0.0"
"webpack": "^1.13.3"
},
"engines": {
"node": ">=7.0.0"
Executable
+2
View File
@@ -0,0 +1,2 @@
node_modules/selenium-standalone/bin/selenium-standalone install
npm start &
+8
View File
@@ -96,4 +96,12 @@ router.put('/:asset_id/settings', (req, res, next) => {
});
router.put('/:asset_id/status', (req, res, next) => {
// Update the asset status
Asset
.update({id: req.params.asset_id}, {status: req.query.status})
.then(() => res.status(204).end())
.catch((err) => next(err));
});
module.exports = router;
+63 -24
View File
@@ -1,5 +1,6 @@
const express = require('express');
const Comment = require('../../../models/comment');
const Asset = require('../../../models/asset');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const wordlist = require('../../../services/wordlist');
@@ -9,24 +10,45 @@ const _ = require('lodash');
const router = express.Router();
router.get('/', authorization.needed('admin'), (req, res, next) => {
const {
status = null,
action_type = null,
asset_id = null
} = req.query;
/**
* This adds the asset_id requirement to the query if the asset_id is defined.
*/
const assetIDWrap = (query) => {
if (asset_id) {
query = query.where('asset_id', asset_id);
}
return query;
};
let query;
if (req.query.status) {
query = Comment.findByStatus(req.query.status);
} else if (req.query.action_type) {
query = Comment.findByActionType(req.query.action_type);
if (status) {
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
} else if (action_type) {
query = Comment
.findIdsByActionType(action_type)
.then((ids) => assetIDWrap(Comment.find({
id: {
$in: ids
},
})));
} else {
query = Comment.all();
query = assetIDWrap(Comment.all());
}
query.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
]);
})
.then(([comments, users, actions])=>
@@ -48,21 +70,38 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
parent_id
} = req.body;
Comment
.create({
body,
asset_id,
parent_id,
status: req.wordlist.matched ? 'rejected' : '',
author_id: req.user.id
})
.then((comment) => {
// Decide the status based on whether or not the current asset/settings
// has pre-mod enabled or not. If the comment was rejected based on the
// wordlist, then reject it, otherwise if the moderation setting is
// premod, set it to `premod`.
let status;
res.status(201).send(comment);
})
.catch((err) => {
next(err);
});
if (req.wordlist.matched) {
status = Promise.resolve('rejected');
} else {
status = Asset
.rectifySettings(Asset.findById(asset_id))
// Return `premod` if pre-moderation is enabled and an empty "new" status
// in the event that it is not in pre-moderation mode.
.then(({moderation}) => moderation === 'pre' ? 'premod' : '');
}
status.then((status) => Comment.publicCreate({
body,
asset_id,
parent_id,
status,
author_id: req.user.id
}))
.then((comment) => {
// The comment was created! Send back the created comment.
res.status(201).send(comment);
})
.catch((err) => {
next(err);
});
});
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
@@ -99,7 +138,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
} = req.body;
Comment
.changeStatus(req.params.comment_id, status)
.pushStatus(req.params.comment_id, status, req.user.id)
.then(() => {
res.status(204).end();
})
+47 -21
View File
@@ -3,6 +3,7 @@ const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const Setting = require('../../../models/setting');
const Asset = require('../../../models/asset');
const _ = require('lodash');
const router = express.Router();
@@ -16,27 +17,52 @@ const router = express.Router();
// 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('/comments/pending', (req, res, next) => {
Setting.getModerationSetting().then(({moderation}) =>
Comment.moderationQueue(moderation))
.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions])=>
res.status(200).json({
comments,
users,
actions
}))
.catch(error => {
next(error);
});
const {
asset_id
} = req.query;
let settings = Setting.retrieve();
if (asset_id) {
// In the event that we have an asset_id, we should fetch the asset settings
// in order to actually determine if there is additional comments to parse.
settings = Promise.all([
settings,
Asset.findById(asset_id).select('settings')
]).then(([{moderation}, asset]) => {
if (asset.settings && asset.settings.moderation) {
return {moderation: asset.settings.moderation};
}
return {moderation};
});
}
settings
.then(({moderation}) => {
return Comment.moderationQueue(moderation);
}).then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]);
})
.then(([comments, users, actions]) => {
res.json({
comments,
users,
actions
});
})
.catch(error => {
next(error);
});
});
module.exports = router;
+12 -10
View File
@@ -4,19 +4,21 @@ const Setting = require('../../../models/setting');
const router = express.Router();
router.get('/', (req, res, next) => {
Setting
.getSettings()
.then(settings => {
res.json(settings);
})
.catch(next);
Setting.retrieve().then((settings) => {
res.json(settings);
})
.catch((err) => {
next(err);
});
});
router.put('/', (req, res, next) => {
Setting
.updateSettings(req.body)
.then(() => res.status(204).end())
.catch(next);
Setting.update(req.body).then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
+4 -14
View File
@@ -27,13 +27,13 @@ router.get('/', (req, res, next) => {
}),
// Get the moderation setting from the settings.
Setting.getModerationSetting()
Setting.retrieve()
])
.then(([asset, settings]) => {
// Merge the asset specific settings with the returned settings object in
// the event that the asset that was returned also had settings.
if (asset.settings) {
if (asset && asset.settings) {
settings = Object.assign({}, settings, asset.settings);
}
@@ -70,17 +70,7 @@ router.get('/', (req, res, next) => {
let users = userIDs.length > 0 ? User.findByIdArray(userIDs) : [];
// Fetch the actions for pretty much everything at this point.
let actions = Action.getActionSummaries(_.uniq([
// Actions can be on assets...
asset.id,
// Comments...
...comments.map((comment) => comment.id),
// Or Authors...
...userIDs
]), req.user ? req.user.id : false);
let actions = Action.getActionSummariesFromComments(asset.id, comments, req.user ? req.user.id : false);
return Promise.all([
@@ -108,7 +98,7 @@ router.get('/', (req, res, next) => {
comments,
users,
actions,
settings
settings: Setting.public(settings)
});
})
.catch(error => {
+1 -1
View File
@@ -20,7 +20,7 @@ const wordlist = {
*/
wordlist.init = () => {
return Setting
.getSettings()
.retrieve()
.then((settings) => {
// Insert the settings wordlist.
+5 -1
View File
@@ -4,8 +4,12 @@
"node": true,
"mocha": true
},
"plugins": [
"mocha"
],
"extends": "../.eslintrc.json",
"rules": {
"no-undef": [0]
"no-undef": [0],
"mocha/no-exclusive-tests": "warn"
}
}
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
waitForConditionTimeout: 20000,
baseUrl: 'localhost:3011/'
};
+25
View File
@@ -0,0 +1,25 @@
const Comments = require('../../models/comment');
const Users = require('../../models/user');
const Actions = require('../../models/action');
const Assets = require('../../models/asset');
const Settings = require('../../models/setting');
const globals = require('./globals');
/* Create an array of comments */
module.exports.comments = (comments) => Assets.findOrCreateByUrl(globals.baseUrl)
.then((asset) => {
comments = comments.map((comment) => {
comment.asset_id = asset.id;
return comment;
});
return Comments.create(comments);
});
/* Create an array of users */
module.exports.users = (users) => Users.createLocalUsers(users);
/* Create an array of actions */
module.exports.actions = (actions) => Actions.create(actions);
/* Update a setting */
module.exports.settings = (setting) => Settings.init().then(() => Settings.updateSettings(setting));
+45
View File
@@ -0,0 +1,45 @@
const fetch = require('node-fetch');
const embedCommands = {
ready() {
return this.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.waitForElementVisible('body', 2000)
.frame('coralStreamIframe');
},
setConfig(config, baseUrl) {
return fetch(`${baseUrl}/api/v1/settings`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(config)
});
},
enterComment() {
const comment = 'This is a test comment';
return this
.waitForElementVisible('@commentBox')
.setValue('@commentBox', comment)
.click('@postButton')
.waitForElementVisible('.comment', 1000);
},
validateComment(comment) {
return this
.assert.equal(comment, '.comment');
}
};
module.exports = {
commands: [embedCommands],
elements: {
commentBox: {
selector: '#commentBox'
},
postButton: {
selector: '#commentBox .coral-plugin-commentbox-button'
}
}
};
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
'@tags': ['app'],
'Base url and Hostname': browser => {
const {baseUrl} = browser.globals;
browser
.url(baseUrl)
.assert.title('Coral Talk')
.waitForElementPresent('body', 1000);
},
after: client => {
client.end();
}
};
+173
View File
@@ -0,0 +1,173 @@
const utils = require('../../utils/e2e-mongoose');
const mocks = require('../mocks');
const mockComment = 'This is a test comment.';
const mockReply = 'This is a test reply';
const mockUser = {
email: `${new Date().getTime()}@test.com`,
name: 'Test User',
pw: 'testtesttest'
};
module.exports = {
'@tags': ['embed-stream', 'comment', 'premodoff', 'premodon'],
before: () => {
utils.before();
},
'User registers and posts a comment with premod off': client => {
client.perform((client, done) => {
mocks.settings({moderation: 'post'})
.then(() => {
//Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamIframe')
//Register and Log In
.waitForElementVisible('#commentBox', 1000)
.waitForElementVisible('#coralSignInButton', 2000)
.click('#coralSignInButton')
.waitForElementVisible('#coralRegister', 1000)
.click('#coralRegister')
.waitForElementVisible('#email', 1000)
.setValue('#email', mockUser.email)
.setValue('#displayName', mockUser.name)
.setValue('#password', mockUser.pw)
.setValue('#confirmPassword', mockUser.pw)
.click('#coralSignUpButton')
.waitForElementVisible('#coralLogInButton', 10000)
.click('#coralLogInButton')
.waitForElementVisible('.coral-plugin-commentbox-button', 4000)
// Post a comment
.setValue('.coral-plugin-commentbox-textarea', mockComment)
.click('.coral-plugin-commentbox-button')
.waitForElementVisible('.comment', 1000)
//Verify that it appears
.assert.containsText('.comment', mockComment);
done();
})
.catch((err) => {
console.log(err);
done();
});
});
},
'User posts a comment with premod on': client => {
client.perform((client, done) => {
mocks.settings({moderation: 'pre'})
.then(() => {
//Load Page
client.url(client.globals.baseUrl)
.frame('coralStreamIframe');
// Post a comment
client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
.setValue('.coral-plugin-commentbox-textarea', mockComment)
.click('.coral-plugin-commentbox-button')
.waitForElementVisible('#coral-notif', 1000)
//Verify that it appears
.assert.containsText('#coral-notif', 'moderation team');
done();
})
.catch((err) => {
console.log(err);
done();
});
});
},
'User replies to a comment with premod off': client => {
client.perform((client, done) => {
mocks.settings({moderation: 'post'})
.then(() => {
//Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamIframe');
// Post a comment
client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
.setValue('.coral-plugin-commentbox-textarea', mockComment)
.click('.coral-plugin-commentbox-button')
// Post a reply
.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('.reply', 2000)
//Verify that it appears
.assert.containsText('.reply', mockReply);
done();
})
.catch((err) => {
console.log(err);
done();
});
});
},
'User replies to a comment with premod on': client => {
client.perform((client, done) => {
mocks.settings({moderation: 'pre'})
// Add a mock user
.then(() => {
return mocks.users([{
displayName: 'Baby Blue',
email: 'whale@tale.sea',
password: 'krill'
}]);
})
// Add a mock preapproved comment by that user
.then((user) => {
return 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('coralStreamIframe');
// 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)
.frame('coralStreamIframe');
// Verify that comment count is correct
client.waitForElementVisible('.coral-plugin-comment-count-text', 2000)
.assert.containsText('.coral-plugin-comment-count-text', '1 Comment');
done();
});
},
after: client => {
utils.after();
client.end();
}
};
+23 -24
View File
@@ -1,35 +1,34 @@
const Action = require('../../models/action');
const expect = require('chai').expect;
describe('Action: models', () => {
let mockActions;
describe('models.Action', () => {
let mockActions = [];
beforeEach(() => {
return Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
mockActions = actions;
});
});
beforeEach(() => Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
mockActions = actions;
}));
describe('#findById()', () => {
it('should find an action by id', () => {
return Action.findById(mockActions[0].id).then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('action_type', 'flag');
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ const expect = chai.expect;
// Use the chai should.
chai.should();
describe('Asset: model', () => {
describe('models.Asset', () => {
beforeEach(() => {
const defaults = {url:'http://test.com'};
+129 -23
View File
@@ -7,35 +7,57 @@ const settings = {id: '1', moderation: 'pre'};
const expect = require('chai').expect;
describe('Comment: models', () => {
describe('models.Comment', () => {
const comments = [{
body: 'comment 10',
asset_id: '123',
status: '',
status: [],
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
status: 'accepted',
status: [{
type: 'accepted'
}],
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
status: '',
status: [],
parent_id: '',
author_id: '456',
id: '3'
}, {
body: 'comment 40',
asset_id: '123',
status: 'rejected',
status: [{
type: 'rejected'
}],
parent_id: '',
author_id: '456',
id: '4'
}, {
body: 'comment 50',
asset_id: '1234',
status: [{
type: 'premod'
}],
parent_id: '',
author_id: '456',
id: '5'
}, {
body: 'comment 60',
asset_id: '1234',
status: [{
type: 'premod'
}],
parent_id: '',
author_id: '456',
id: '6'
}];
const users = [{
@@ -60,25 +82,69 @@ describe('Comment: models', () => {
user_id: '456'
}];
beforeEach(() => {
return Promise.all([
Setting.create(settings),
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
]);
beforeEach(() => Promise.all([
Setting.init(settings),
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
]));
describe('#publicCreate()', () => {
it('creates a new comment', () => {
return Comment.publicCreate({
body: 'This is a comment!',
status: 'accepted'
}).then((c) => {
expect(c).to.not.be.null;
expect(c.id).to.not.be.null;
expect(c.id).to.be.uuid;
expect(c.status).to.have.length(1);
expect(c.status[0]).to.have.property('type', 'accepted');
});
});
it('creates many new comments', () => {
return Comment.publicCreate([{
body: 'This is a comment!',
status: 'accepted'
}, {
body: 'This is another comment!'
}, {
body: 'This is a rejected comment!',
status: 'rejected'
}]).then(([c1, c2, c3]) => {
expect(c1).to.not.be.null;
expect(c1.id).to.be.uuid;
expect(c1.status).to.have.length(1);
expect(c1.status[0]).to.have.property('type', 'accepted');
expect(c2).to.not.be.null;
expect(c2.id).to.be.uuid;
expect(c2.status).to.have.length(0);
expect(c3).to.not.be.null;
expect(c3.id).to.be.uuid;
expect(c3.status).to.have.length(1);
expect(c3.status[0]).to.have.property('type', 'rejected');
});
});
});
describe('#findById()', () => {
it('should find a comment by id', () => {
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 all comments by asset id', () => {
return Comment.findByAssetId('123').then((result) => {
expect(result).to.have.length(3);
@@ -91,6 +157,7 @@ describe('Comment: models', () => {
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);
@@ -101,6 +168,7 @@ describe('Comment: models', () => {
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);
@@ -112,13 +180,16 @@ describe('Comment: models', () => {
});
});
});
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;
@@ -126,21 +197,56 @@ describe('Comment: models', () => {
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;
// });
// });
});
describe('#removeAction', () => {
it('should remove an action', () => {
return Comment.removeAction('3', '123', 'flag').then(() => {
return Action.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
return Comment.removeAction('3', '123', 'flag')
.then(() => {
return Action.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
});
});
describe('#changeStatus', () => {
it('should change the status of a comment from no status', () => {
let comment_id = comments[0].id;
return Comment.findById(comment_id)
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(0);
return Comment.pushStatus(comment_id, 'rejected', '123');
})
.then(() => Comment.findById(comment_id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(1);
expect(c.status[0]).to.have.property('type', 'rejected');
expect(c.status[0]).to.have.property('assigned_by', '123');
});
});
it('should change the status of a comment from accepted', () => {
return Comment.pushStatus(comments[1].id, 'rejected', '123')
.then(() => Comment.findById(comments[1].id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.have.length(2);
expect(c.status[0]).to.have.property('type', 'accepted');
expect(c.status[0]).to.have.property('assigned_by', null);
expect(c.status[1]).to.have.property('type', 'rejected');
expect(c.status[1]).to.have.property('assigned_by', '123');
});
});
});
});
+9 -12
View File
@@ -1,32 +1,29 @@
const Setting = require('../../models/setting');
const expect = require('chai').expect;
describe('Setting: model', () => {
describe('models.Setting', () => {
beforeEach(() => {
const defaults = {id: 1};
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
beforeEach(() => Setting.init({moderation: 'pre'}));
describe('#getSettings()', () => {
describe('#retrieve()', () => {
it('should have a moderation field defined', () => {
return Setting.getSettings().then(settings => {
return Setting.retrieve().then(settings => {
expect(settings).to.have.property('moderation').and.to.equal('pre');
});
});
it('should have two infoBox fields defined', () => {
return Setting.getSettings().then(settings => {
return Setting.retrieve().then(settings => {
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
});
});
});
describe('#updateSettings()', () => {
describe('#update()', () => {
it('should update the settings with a passed object', () => {
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
return Setting.updateSettings(mockSettings).then(updatedSettings => {
return Setting.update(mockSettings).then(updatedSettings => {
expect(updatedSettings).to.be.an('object');
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
expect(updatedSettings).to.have.property('infoBoxEnable', true);
@@ -35,9 +32,9 @@ describe('Setting: model', () => {
});
});
describe('#getModerationSetting', () => {
describe('#get', () => {
it('should return the moderation settings', () => {
return Setting.getModerationSetting().then(({moderation}) => {
return Setting.retrieve().then(({moderation}) => {
expect(moderation).not.to.be.null;
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
const User = require('../../models/user');
const expect = require('chai').expect;
describe('User: models', () => {
describe('models.User', () => {
let mockUsers;
beforeEach(() => {
return User.createLocalUsers([{
+101 -65
View File
@@ -10,6 +10,7 @@ chai.use(require('chai-http'));
const wordlist = require('../../../../services/wordlist');
const Comment = require('../../../../models/comment');
const Asset = require('../../../../models/asset');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
@@ -17,63 +18,71 @@ const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
describe('/api/v1/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: 'def-rejected',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
author_id: '456',
status: 'accepted'
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions),
wordlist.insert([
'bad words'
]),
Setting.create(settings)
]);
});
describe('#get', () => {
const comments = [{
body: 'comment 10',
asset_id: 'asset',
author_id: '123'
}, {
body: 'comment 20',
asset_id: 'asset',
author_id: '456'
}, {
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: [{
type: 'rejected'
}]
}, {
body: 'comment 30',
asset_id: '456',
status: [{
type: 'accepted'
}]
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments).then((newComments) => {
newComments.forEach((comment, i) => {
comments[i].id = comment.id;
});
actions[0].item_id = comments[0].id;
actions[1].item_id = comments[1].id;
return Action.create(actions);
}),
User.createLocalUsers(users),
wordlist.insert([
'bad words'
]),
Setting.init(settings)
]);
});
it('should return all the comments', () => {
return chai.request(app)
.get('/api/v1/comments')
@@ -91,7 +100,9 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('id', 'def-rejected');
expect(res.body).to.have.property('comments');
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', comments[2].id);
});
});
@@ -102,7 +113,7 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', 'hij');
expect(res.body.comments[0]).to.have.property('id', comments[3].id);
});
});
@@ -124,8 +135,7 @@ describe('/api/v1/comments', () => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('id', 'abc');
expect(res.body.comments[0]).to.have.property('id', comments[0].id);
});
});
});
@@ -151,7 +161,31 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', 'rejected');
expect(res.body).to.have.property('status').and.to.have.length(1);
expect(res.body.status[0]).to.have.property('type', 'rejected');
});
});
it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {moderation: 'pre'})
.then(() => asset);
})
.then((asset) => {
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('asset_id');
expect(res.body).to.have.property('status').and.to.have.length(1);
expect(res.body.status[0]).to.have.property('type', 'premod');
});
});
});
@@ -267,18 +301,22 @@ describe('/api/v1/comments/:comment_id/actions', () => {
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: ''
status: []
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected'
status: [{
type: 'rejected'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
status: [{
type: 'accepted'
}]
}];
const users = [{
@@ -316,10 +354,8 @@ describe('/api/v1/comments/:comment_id/actions', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
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');
});
});
});
+62 -62
View File
@@ -15,56 +15,54 @@ const User = require('../../../../models/user');
const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
beforeEach(() => {
return Setting.create(settings);
});
describe('/api/v1/queue', () => {
const comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: [{
type: 'rejected'
}]
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: [{
type: 'premod'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: [{
type: 'accepted'
}]
}];
describe('Get moderation queues rejected, pending, flags', () => {
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
describe('/api/v1/queue', () => {
let comments;
const actions = [{
action_type: 'flag',
item_id: 'abc',
item_type: 'comment'
}, {
action_type: 'like',
item_id: 'hij',
item_type: 'comment'
}];
const users = [{
id: '456',
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123'
}, {
id: '123',
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123'
}];
let actions;
beforeEach(() => {
comments = [{
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
status: 'rejected'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset'
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted'
}];
actions = [{
action_type: 'flag',
item_type: 'comment'
}, {
action_type: 'like',
item_type: 'comment'
}];
return User.createLocalUsers(users)
beforeEach(() => {
return User.createLocalUsers(users)
.then((u) => {
comments[0].author_id = u[0].id;
comments[1].author_id = u[1].id;
@@ -76,22 +74,24 @@ describe('Get moderation queues rejected, pending, flags', () => {
actions[0].item_id = c[0].id;
actions[1].item_id = c[1].id;
return Action.create(actions);
return Promise.all([
Action.create(actions),
Setting.init(settings)
]);
});
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
});
+2 -2
View File
@@ -12,7 +12,7 @@ const defaults = {id: '1', moderation: 'pre'};
describe('/api/v1/settings', () => {
beforeEach(() => Setting.create(defaults));
beforeEach(() => Setting.init(defaults));
describe('#get', () => {
@@ -40,7 +40,7 @@ describe('/api/v1/settings', () => {
.then((res) => {
expect(res).to.have.status(204);
return Setting.getSettings();
return Setting.retrieve();
})
.then((settings) => {
expect(settings).to.have.property('moderation', 'post');
+33 -29
View File
@@ -20,7 +20,37 @@ describe('/api/v1/stream', () => {
moderation: 'post'
};
let comments;
const comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: []
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: [{
type: 'accepted'
}]
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: [{
type: 'rejected'
}]
}];
const users = [{
displayName: 'Ana',
@@ -41,33 +71,6 @@ describe('/api/v1/stream', () => {
}];
beforeEach(() => {
comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: 'accepted'
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: ''
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: 'accepted'
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: 'rejected'
}];
return Promise.all([
User.createLocalUsers(users),
Asset.findOrCreateByUrl('http://test.com'),
@@ -94,10 +97,11 @@ describe('/api/v1/stream', () => {
return Promise.all([
Comment.create(comments),
Action.create(actions),
Setting.init().then(() => Setting.updateSettings(settings))
Setting.init(settings)
]);
});
});
it('should return a stream with comments, users and actions for an existing asset', () => {
return chai.request(app)
.get('/api/v1/stream')
+34
View File
@@ -0,0 +1,34 @@
const mongoose = require('../../mongoose');
// Ensure the NODE_ENV is set to 'test',
// this is helpful when you would like to change behavior when testing.
function clearDB() {
// console.log('Clearing DB', mongoose.connection);
for (let i in mongoose.connection.collections) {
// console.log('Clearing', i);
mongoose.connection.collections[i].remove(function() {});
}
}
module.exports = {
before: () => {
clearDB();
},
beforeEach: () => {
if (mongoose.connection.readyState === 0) {
mongoose.on('open', function() {
if (err) {
throw err;
}
return clearDB();
});
} else {
return clearDB();
}
},
after: () => {
clearDB();
mongoose.disconnect();
}
};
+2 -1
View File
@@ -15,6 +15,7 @@
width:500px;
}
</style>
<title><%= title %></title>
</head>
<body>
<main>
@@ -35,7 +36,7 @@
<script type='text/javascript' src='<%= basePath %>/pym.v1.min.js'></script>
<script>
var ready = false;
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments', id:'coralStreamIframe'});
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})
pymParent.onMessage('childReady', function () {
var interval = setInterval(function () {