Merge branch 'master' into i18n-var

This commit is contained in:
Riley Davis
2016-12-06 13:41:44 -10:00
committed by GitHub
45 changed files with 1478 additions and 599 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/
+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.
+153 -137
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, 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,135 +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]}
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={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}
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={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
@@ -226,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
@@ -236,7 +251,7 @@ class CommentStream extends Component {
/>
</div>
:
<Spinner/>
<Spinner/>
}
</div>;
}
@@ -263,6 +278,7 @@ const mapDispatchToProps = (dispatch) => ({
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);
@@ -197,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;
}
@@ -78,6 +78,7 @@ class CommentBox extends Component {
{ author && (
<Button
cStyle='darkGrey'
className={`${name}-button`}
onClick={this.postComment}>
{lang.t('post')}
</Button>
@@ -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 -40
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,48 +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 settings visible to the public
* @return {Promise} moderation the settings for how to moderate comments
*/
SettingSchema.statics.getPublicSettings = function () {
return this.findOne({id: '1'}).select('moderation infoBoxEnable infoBoxContent');
};
/**
* 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.getPublicSettings().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;
+5 -15
View File
@@ -26,14 +26,14 @@ router.get('/', (req, res, next) => {
return asset;
}),
// Get the public settings.
Setting.getPublicSettings()
// Get the moderation setting from the settings.
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 -17
View File
@@ -1,34 +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);
@@ -37,13 +32,10 @@ describe('Setting: model', () => {
});
});
describe('#getPublicSettings', () => {
describe('#get', () => {
it('should return the moderation settings', () => {
return Setting.getPublicSettings().then(({moderation, infoBoxEnable, infoBoxContent, wordlist}) => {
return Setting.retrieve().then(({moderation}) => {
expect(moderation).not.to.be.null;
expect(infoBoxEnable).not.to.be.null;
expect(infoBoxContent).not.to.be.null;
expect(wordlist).to.be.undefined;
});
});
});
+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 () {