mirror of
https://github.com/wassname/talk.git
synced 2026-10-02 12:50:49 +08:00
Merge branch 'master' of github.com:coralproject/talk into e2e-tests
This commit is contained in:
commit
72d36713e9
52 files changed
+618
-67
No files matched your search
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
client/lib
|
||||
**/*.html
|
||||
@@ -21,6 +21,8 @@
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": [2],
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
|
||||
@@ -22,6 +22,7 @@ if (app.get('env') !== 'test') {
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// We disable frameward on helmet to allow crossdomain injection of the embed
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
|
||||
@@ -60,11 +60,13 @@ function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => {
|
||||
|
||||
return (
|
||||
<Dialog className={styles.dialog} open={open} onClose={() => handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={() => handleClose()}>×</span>
|
||||
<span className={styles.close} onClick={() => handleClose()}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h3>
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class CommentList extends React.Component {
|
||||
// add key handlers and gestures
|
||||
componentDidMount () {
|
||||
this.bindKeyHandlers();
|
||||
|
||||
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
|
||||
}
|
||||
|
||||
@@ -80,6 +81,7 @@ export default class CommentList extends React.Component {
|
||||
|
||||
const {commentIds} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// check boundaries
|
||||
if (active === null || !commentIds.length) {
|
||||
this.setState({active: commentIds[0]});
|
||||
@@ -102,6 +104,7 @@ export default class CommentList extends React.Component {
|
||||
// TODO: In the future this can be improved and look at the actual state to
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction (action, id, author_id) {
|
||||
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const {commentIds} = this.props;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
@@ -12,6 +13,12 @@ import {
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
days: 60 * 60 * 24,
|
||||
hours: 60 * 60
|
||||
};
|
||||
|
||||
const updateCharCountEnable = (updateSettings, charCountChecked) => () => {
|
||||
const charCountEnable = !charCountChecked;
|
||||
updateSettings({charCountEnable});
|
||||
@@ -47,11 +54,32 @@ const updateClosedMessage = (updateSettings) => (event) => {
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <List>
|
||||
// If we are changing the measure we need to recalculate using the old amount
|
||||
// Same thing if we are just changing the amount
|
||||
const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
if (isMeasure) {
|
||||
const amount = getTimeoutAmount(ts);
|
||||
const closedTimeout = amount * TIMESTAMPS[event];
|
||||
updateSettings({closedTimeout});
|
||||
} else {
|
||||
const val = event.target.value;
|
||||
const measure = getTimeoutMeasure(ts);
|
||||
const closedTimeout = val * TIMESTAMPS[measure];
|
||||
updateSettings({closedTimeout});
|
||||
}
|
||||
};
|
||||
|
||||
const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => {
|
||||
if (fetchingSettings) {
|
||||
/* maybe a spinner here at some point */
|
||||
return <p>Loading settings...</p>;
|
||||
}
|
||||
|
||||
return <List>
|
||||
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateModeration(updateSettings, settings.moderation)}
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
@@ -64,7 +92,7 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
checked={settings.charCountEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
@@ -91,7 +119,7 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
checked={settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
@@ -110,6 +138,29 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.close-after')}
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={lang.t('configure.closed-comments-label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
|
||||
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{lang.t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
@@ -121,7 +172,24 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>;
|
||||
};
|
||||
|
||||
export default CommentSettings;
|
||||
|
||||
// To see if we are talking about weeks, days or hours
|
||||
// We talk the remainder of the division and see if it's 0
|
||||
const getTimeoutMeasure = ts => {
|
||||
if (ts % TIMESTAMPS['weeks'] === 0) {
|
||||
return 'weeks';
|
||||
} else if (ts % TIMESTAMPS['days'] === 0) {
|
||||
return 'days';
|
||||
} else if (ts % TIMESTAMPS['hours'] === 0) {
|
||||
return 'hours';
|
||||
}
|
||||
};
|
||||
|
||||
// Dividing the amount by it's measure (hours, days, weeks) we
|
||||
// obtain the amount of time
|
||||
const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -63,6 +63,11 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.configTimeoutSelect {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.charCountTexfield {
|
||||
width: 4em;
|
||||
padding: 0px;
|
||||
|
||||
@@ -78,6 +78,7 @@ class Configure extends React.Component {
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
fetchingSettings={this.props.fetchingSettings}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}
|
||||
errors={this.state.errors}
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Icon} from 'react-mdl';
|
||||
import key from 'keymaster';
|
||||
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
||||
@@ -47,6 +48,7 @@ class ModerationQueue extends React.Component {
|
||||
// Hack for dynamic mdl tabs
|
||||
componentDidMount () {
|
||||
if (typeof componentHandler !== 'undefined') {
|
||||
|
||||
// FIXME: fix this hack
|
||||
componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef
|
||||
}
|
||||
@@ -54,6 +56,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// Dispatch the update status action
|
||||
onCommentAction (action, id) {
|
||||
|
||||
// If not banning then change the status to approved or flagged as action = status
|
||||
this.props.dispatch(updateStatus(action, id));
|
||||
}
|
||||
@@ -70,6 +73,10 @@ class ModerationQueue extends React.Component {
|
||||
this.props.dispatch(banUser('banned', userId, commentId));
|
||||
}
|
||||
|
||||
showShortcuts = () => {
|
||||
this.setState({modalOpen: true});
|
||||
}
|
||||
|
||||
onTabClick (activeTab) {
|
||||
this.setState({activeTab});
|
||||
}
|
||||
@@ -93,6 +100,11 @@ class ModerationQueue extends React.Component {
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
|
||||
<a href='#flagged' onClick={() => this.onTabClick('flagged')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
|
||||
<a href='#shortcuts' onClick={this.showShortcuts}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${styles.showShortcuts}`}>
|
||||
<Icon name='keyboard' />
|
||||
<span>{lang.t('modqueue.showshortcuts')}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
<CommentList
|
||||
@@ -110,7 +122,7 @@ class ModerationQueue extends React.Component {
|
||||
handleClose={() => this.hideBanUserDialog()}
|
||||
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
|
||||
user={comments.banUser}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
<CommentList
|
||||
isActive={activeTab === 'rejected'}
|
||||
@@ -124,7 +136,7 @@ class ModerationQueue extends React.Component {
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
||||
<CommentList
|
||||
isActive={activeTab === 'rejected'}
|
||||
isActive={activeTab === 'flagged'}
|
||||
singleView={singleView}
|
||||
commentIds={flaggedIds}
|
||||
comments={comments.byId}
|
||||
@@ -133,11 +145,14 @@ class ModerationQueue extends React.Component {
|
||||
actions={['reject', 'approve']}
|
||||
loading={comments.loading} />
|
||||
</div>
|
||||
<ModerationKeysModal open={modalOpen}
|
||||
onClose={() => this.setState({modalOpen: false})} />
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='shortcuts'>
|
||||
<ModerationKeysModal open={modalOpen}
|
||||
onClose={() => this.setState({modalOpen: false})} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ Promise.all([
|
||||
coralApi('/comments?action_type=flag')
|
||||
])
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
|
||||
/* Combine seperate calls into a single object */
|
||||
let all = {};
|
||||
all.comments = pending.comments
|
||||
@@ -55,6 +56,7 @@ Promise.all([
|
||||
return all;
|
||||
})
|
||||
.then(all => {
|
||||
|
||||
/* Post comments and users to redux store. Actions will be posted when they are needed. */
|
||||
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
users: all.users});
|
||||
@@ -62,6 +64,7 @@ Promise.all([
|
||||
comments: all.comments});
|
||||
|
||||
});
|
||||
|
||||
// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
|
||||
|
||||
// Update a comment. Now to update a comment we need to send back the whole object
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"nextcomment": "Go to the next comment",
|
||||
"prevcomment": "Go to the previous comment",
|
||||
"singleview": "Toggle single comment edit view",
|
||||
"thismenu": "Open this menu"
|
||||
"thismenu": "Open this menu",
|
||||
"showshortcuts": "Show Shortcuts"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
@@ -57,6 +58,10 @@
|
||||
"community": "Community",
|
||||
"closed-comments-desc": "Write a message for closed threads",
|
||||
"closed-comments-label": "Write a message...",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"close-after": "Close comments after",
|
||||
"comment-count-header": "Limit Comment Length",
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
@@ -91,7 +96,8 @@
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"showshortcuts": "Mostrar atajos"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
@@ -117,6 +123,11 @@
|
||||
"community": "Comunidad",
|
||||
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
|
||||
"closed-comments-label": "Escribe un mensaje...",
|
||||
"never": "Nunca",
|
||||
"hours": "Horas",
|
||||
"days": "Días",
|
||||
"weeks": "Semanas",
|
||||
"close-after": "Cerrar comentarios luego de",
|
||||
"comment-count-header": "Limitar el largo del comentario",
|
||||
"comment-count-text-pre": "El largo de comentarios será ",
|
||||
"comment-count-text-post": " caracteres",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {I18n} from '../../coral-framework';
|
||||
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
|
||||
const lang = new I18n();
|
||||
|
||||
class ConfigureStreamContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -47,8 +50,15 @@ class ConfigureStreamContainer extends Component {
|
||||
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
|
||||
}
|
||||
|
||||
getClosedIn () {
|
||||
const {closedTimeout} = this.props.config;
|
||||
const {created_at} = this.props.asset;
|
||||
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {status} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConfigureCommentStream
|
||||
@@ -59,6 +69,7 @@ class ConfigureStreamContainer extends Component {
|
||||
/>
|
||||
<hr />
|
||||
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
|
||||
{status === 'open' ? <p>The comment stream will close in {this.getClosedIn()}.</p> : ''}
|
||||
<CloseCommentsInfo
|
||||
onClick={this.toggleStatus}
|
||||
status={status}
|
||||
@@ -69,7 +80,11 @@ class ConfigureStreamContainer extends Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
config: state.config.toJS()
|
||||
config: state.config.toJS(),
|
||||
asset: state.items
|
||||
.get('assets')
|
||||
.first()
|
||||
.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
@@ -58,10 +58,15 @@ class CommentStream extends Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
this.pym = new Pym.Child({polling: 100});
|
||||
|
||||
const path = this.pym.parentUrl.split('#')[0];
|
||||
let path = this.pym.parentUrl.split('#')[0];
|
||||
|
||||
if (!path) {
|
||||
path = window.location.href.split('#')[0];
|
||||
}
|
||||
|
||||
this.props.getStream(path || window.location);
|
||||
this.path = path;
|
||||
|
||||
@@ -2,6 +2,10 @@ import coralApi from '../helpers/response';
|
||||
import {fromJS} from 'immutable';
|
||||
import {UPDATE_CONFIG} from '../constants/config';
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
@@ -114,6 +118,7 @@ export function getStream (assetUrl) {
|
||||
/* Sort comments by date*/
|
||||
json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
const rels = json.comments.reduce((h, item) => {
|
||||
|
||||
/* Check for root and child comments. */
|
||||
if (
|
||||
item.asset_id === assetId &&
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as actions from '../constants/user';
|
||||
import * as assetActions from '../constants/assets';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {addItem} from '../actions/items';
|
||||
import coralApi from '../helpers/response';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -19,3 +21,30 @@ export const saveBio = (user_id, formData) => dispatch => {
|
||||
})
|
||||
.catch(error => dispatch(saveBioFailure(error)));
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Get a list of comments by a single user
|
||||
*
|
||||
* @param {string} user_id
|
||||
* @returns Promise
|
||||
*/
|
||||
export const fetchCommentsByUserId = userId => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
|
||||
return coralApi(`/comments?user_id=${userId}`)
|
||||
.then(({comments, assets}) => {
|
||||
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
|
||||
|
||||
assets.forEach(asset => dispatch(addItem(asset, 'assets')));
|
||||
|
||||
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
|
||||
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.stack);
|
||||
console.error('FAILURE_COMMENTS_BY_USER', error);
|
||||
dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error});
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const MULTIPLE_ASSETS_REQUEST = 'MULTIPLE_ASSETS_REQUEST';
|
||||
export const MULTIPLE_ASSETS_SUCCESS = 'MULTIPLE_ASSETS_SUCCESS';
|
||||
export const MULTIPLE_ASSSETS_FAILURE = 'MULTIPLE_ASSSETS_FAILURE';
|
||||
@@ -1,3 +1,6 @@
|
||||
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
|
||||
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
|
||||
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
|
||||
export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST';
|
||||
export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS';
|
||||
export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE';
|
||||
@@ -9,6 +9,7 @@ import get from 'lodash/get';
|
||||
|
||||
class i18n {
|
||||
constructor (translations) {
|
||||
|
||||
/**
|
||||
* Register locales
|
||||
*/
|
||||
@@ -16,6 +17,7 @@ class i18n {
|
||||
this.locales = {'en': 'en', 'es': 'es'};
|
||||
timeago.register('es_ES', esTA);
|
||||
this.timeagoInstance = new timeago();
|
||||
|
||||
/**
|
||||
* Load translations
|
||||
*/
|
||||
@@ -55,6 +57,7 @@ class i18n {
|
||||
this.t = (key, ...replacements) => {
|
||||
if (has(this.translations, key)) {
|
||||
let translation = get(this.translations, key);
|
||||
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as actions from '../actions/items';
|
||||
const initialState = fromJS({
|
||||
comments: {},
|
||||
users: {},
|
||||
assets: {},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as authActions from '../constants/auth';
|
||||
import * as actions from '../constants/user';
|
||||
import * as assetActions from '../constants/assets';
|
||||
|
||||
const initialState = Map({
|
||||
displayName: '',
|
||||
profiles: [],
|
||||
settings: {}
|
||||
settings: {},
|
||||
myComments: [],
|
||||
myAssets: [] // the assets from which myComments (above) originated
|
||||
});
|
||||
|
||||
const purge = user => {
|
||||
@@ -30,6 +33,10 @@ export default function user (state = initialState, action) {
|
||||
case actions.SAVE_BIO_SUCCESS:
|
||||
return state
|
||||
.set('settings', action.settings);
|
||||
case actions.COMMENTS_BY_USER_SUCCESS:
|
||||
return state.set('myComments', action.comments);
|
||||
case assetActions.MULTIPLE_ASSETS_SUCCESS:
|
||||
return state.set('myAssets', action.assets);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import has from 'lodash/has';
|
||||
import reduce from 'lodash/reduce';
|
||||
const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0;
|
||||
if (items.assets[id] && items.assets[id].comments) {
|
||||
if (has(items, `assets.${id}.comments`)) {
|
||||
count += items.assets[id].comments.length;
|
||||
}
|
||||
const itemKeys = Object.keys(items.comments);
|
||||
for (let i = 0; i < itemKeys.length; i++) {
|
||||
const item = items.comments[itemKeys[i]];
|
||||
if (item.children) {
|
||||
count += item.children.length;
|
||||
|
||||
// lodash reduce works on {}
|
||||
count += reduce(items.comments, (accum, comment) => {
|
||||
if (comment.children) {
|
||||
accum += comment.children.length;
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
}, 0);
|
||||
|
||||
return <div className={`${name}-text`}>
|
||||
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
|
||||
|
||||
@@ -103,7 +103,7 @@ class CommentBox extends Component {
|
||||
<div className={`${name}-button-container`}>
|
||||
{ author && (
|
||||
<Button
|
||||
cStyle={length > charCount ? 'lightGrey' : 'darkGrey'}
|
||||
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
|
||||
className={`${name}-button`}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
|
||||
@@ -32,7 +32,7 @@ class FlagButton extends Component {
|
||||
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
|
||||
const {itemType, field, detail, step, otherText, posted} = this.state;
|
||||
|
||||
//Proceed to the next step or close the menu if we've reached the end
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
this.setState({showMenu: false});
|
||||
} else {
|
||||
@@ -41,6 +41,7 @@ class FlagButton extends Component {
|
||||
|
||||
// If itemType and detail are both set, post the action
|
||||
if (itemType && detail && !posted) {
|
||||
|
||||
// Set the text from the "other" field if it exists.
|
||||
const updatedDetail = otherText || detail;
|
||||
let item_id;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.myComment {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.myComment:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.assetURL {
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.commentBody {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const Comment = props => {
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
<p className="myCommentAsset">
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href={`${props.asset.url}#${props.comment.id}`}>{props.asset.url}</a>
|
||||
</p>
|
||||
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
body: PropTypes.string
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
url: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
File renamed without changes.
@@ -0,0 +1,26 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './Comment';
|
||||
import styles from './CommentHistory.css';
|
||||
|
||||
const CommentHistory = props => {
|
||||
return (
|
||||
<div className={`${styles.header} commentHistory`}>
|
||||
<div className="commentHistory__list">
|
||||
{props.comments.map((comment, i) => {
|
||||
const asset = props.assets.find(asset => asset.id === comment.asset_id);
|
||||
return <Comment
|
||||
key={i}
|
||||
comment={comment}
|
||||
asset={asset} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
assets: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default CommentHistory;
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentHistory.css';
|
||||
|
||||
export default ({comments = []}) => (
|
||||
<div className={styles.header}>
|
||||
<h1>Comments</h1>
|
||||
<ul>
|
||||
{comments.map(() => (
|
||||
<li>
|
||||
{/* Comment Data*/}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {saveBio} from 'coral-framework/actions/user';
|
||||
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
|
||||
|
||||
import BioContainer from './BioContainer';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import {TabBar, Tab, TabContent} from '../../coral-ui';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
import CommentHistory from 'coral-plugin-history/CommentHistory';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import RestrictedContent from 'coral-framework/components/RestrictedContent';
|
||||
|
||||
@@ -21,7 +21,9 @@ class SignInContainer extends Component {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
|
||||
// Fetch commentHistory
|
||||
this.props.fetchCommentsByUserId(this.props.userData.id);
|
||||
}
|
||||
|
||||
handleTabChange(tab) {
|
||||
@@ -31,17 +33,34 @@ class SignInContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loggedIn, userData, showSignInDialog} = this.props;
|
||||
const {loggedIn, userData, showSignInDialog, items, user} = this.props;
|
||||
const {activeTab} = this.state;
|
||||
|
||||
const commentsMostRecentFirst = user
|
||||
.myComments.map(id => items.comments[id])
|
||||
.sort(({created_at:a}, {created_at:b}) => {
|
||||
|
||||
// descending order, created_at
|
||||
// js date strings can be sorted lexigraphically.
|
||||
const aLessThanB = a < b ? 1 : 0;
|
||||
return a > b ? -1 : aLessThanB;
|
||||
});
|
||||
|
||||
return (
|
||||
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
|
||||
<SettingsHeader {...this.props} />
|
||||
<TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
|
||||
<Tab>All Comments (120)</Tab>
|
||||
<Tab>All Comments ({user.myComments.length})</Tab>
|
||||
<Tab>Profile Settings</Tab>
|
||||
</TabBar>
|
||||
<TabContent show={activeTab === 0}>
|
||||
<CommentHistory {...this.props}/>
|
||||
{
|
||||
user.myComments.length && user.myAssets.length
|
||||
? <CommentHistory
|
||||
comments={commentsMostRecentFirst}
|
||||
assets={user.myAssets.map(id => items.assets[id])} />
|
||||
: <p>Loading comment history...</p>
|
||||
}
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
|
||||
@@ -51,12 +70,14 @@ class SignInContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
const mapStateToProps = state => ({
|
||||
items: state.items.toJS(),
|
||||
user: state.user.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
|
||||
getHistory: () => dispatch(),
|
||||
fetchCommentsByUserId: userId => dispatch(fetchCommentsByUserId(userId))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dialogPolyfill from 'dialog-polyfill';
|
||||
import 'dialog-polyfill/dialog-polyfill.css';
|
||||
|
||||
@@ -19,7 +18,7 @@ export default class Dialog extends Component {
|
||||
};
|
||||
|
||||
componentDidMount(){
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
const dialog = this.dialog;
|
||||
dialogPolyfill.registerDialog(dialog);
|
||||
|
||||
if (this.props.open) {
|
||||
@@ -31,7 +30,7 @@ export default class Dialog extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
const dialog = this.dialog;
|
||||
if (this.props.open !== prevProps.open) {
|
||||
if (this.props.open) {
|
||||
dialog.showModal();
|
||||
@@ -42,7 +41,7 @@ export default class Dialog extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
const dialog = this.dialog;
|
||||
dialog.removeEventListener('cancel', this.props.onCancel);
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ export default class Dialog extends Component {
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref="dialog"
|
||||
ref={el => { this.dialog = el; }}
|
||||
className={`mdl-dialog ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ const maxRecursion = 3;
|
||||
* payload response first based on user and role.
|
||||
*/
|
||||
module.exports = (req, res, next) => {
|
||||
|
||||
/**
|
||||
* Updates the original document based on filtering out for roles.
|
||||
* @param {Mixed} o original object to be modified
|
||||
|
||||
@@ -152,6 +152,16 @@ AssetSchema.statics.search = (value) => value.length === 0 ? Asset.find({}) : As
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds multiple assets with matching ids
|
||||
* @param {Array} ids an array of Strings of asset_id
|
||||
* @return {Promise} resolves to list of Assets
|
||||
*/
|
||||
AssetSchema.statics.findMultipleById = function (ids) {
|
||||
const query = ids.map(id => ({id}));
|
||||
return Asset.find(query);
|
||||
};
|
||||
|
||||
const Asset = mongoose.model('Asset', AssetSchema);
|
||||
|
||||
module.exports = Asset;
|
||||
@@ -324,6 +324,15 @@ CommentSchema.statics.removeAction = (item_id, user_id, action_type) => Action.r
|
||||
*/
|
||||
CommentSchema.statics.all = () => Comment.find();
|
||||
|
||||
/**
|
||||
* Returns all the comments by user
|
||||
* probably to be paginated at some point in the future
|
||||
* @return {Promise} array resolves to an array of comments by that user
|
||||
*/
|
||||
CommentSchema.statics.findByUserId = function (author_id) {
|
||||
return Comment.find({author_id});
|
||||
};
|
||||
|
||||
// Comment model.
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
|
||||
+10
-1
@@ -88,6 +88,7 @@ const UserSchema = new mongoose.Schema({
|
||||
// Status provides a string that says in which state the account is.
|
||||
// When the account is banned, the user login is disabled.
|
||||
status: {type: String, enum: USER_STATUS, default: 'active'},
|
||||
|
||||
// User's settings
|
||||
settings: {
|
||||
bio: {
|
||||
@@ -136,7 +137,13 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
*/
|
||||
UserSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'displayName', 'settings', 'created_at', 'updated_at']);
|
||||
let allowed = ['id', 'displayName', 'settings', 'created_at', 'updated_at'];
|
||||
|
||||
if (user && user.id === this.id) {
|
||||
allowed.push('roles');
|
||||
}
|
||||
|
||||
return _.pick(this.toJSON(), allowed);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
@@ -495,6 +502,7 @@ UserService.createPasswordResetToken = function (email) {
|
||||
.then(user => {
|
||||
|
||||
if (user === null) {
|
||||
|
||||
// since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the endpoint
|
||||
return Promise.resolve(null);
|
||||
@@ -522,6 +530,7 @@ UserService.verifyPasswordResetToken = token => {
|
||||
});
|
||||
})
|
||||
.then(decoded => {
|
||||
|
||||
/**
|
||||
* TODO: check the jti from this decoded token in redis
|
||||
* and make an entry if it does not exist.
|
||||
|
||||
+5
-1
@@ -9,7 +9,7 @@
|
||||
"build-watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "./node_modules/.bin/eslint bin/* .",
|
||||
"lint-fix": "./node_modules/.bin/eslint bin/* . --fix",
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive tests",
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
|
||||
"test-watch": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"pree2e": "NODE_ENV=test ./scripts/pree2e.sh",
|
||||
"e2e": "NODE_ENV=test ./node_modules/.bin/nightwatch",
|
||||
@@ -92,6 +92,7 @@
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"dialog-polyfill": "^0.4.4",
|
||||
"enzyme": "^2.6.0",
|
||||
"eslint": "^3.12.1",
|
||||
"eslint-config-postcss": "^2.0.2",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
@@ -105,8 +106,10 @@
|
||||
"exports-loader": "^0.6.3",
|
||||
"fetch-mock": "^5.5.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"immutable": "^3.8.1",
|
||||
"imports-loader": "^0.6.5",
|
||||
"jsdom": "^9.8.3",
|
||||
"json-loader": "^0.5.4",
|
||||
"keymaster": "^1.6.2",
|
||||
"material-design-lite": "^1.2.1",
|
||||
@@ -121,6 +124,7 @@
|
||||
"precss": "^1.4.0",
|
||||
"pym.js": "^1.1.1",
|
||||
"react": "15.3.2",
|
||||
"react-addons-test-utils": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react-linkify": "^0.1.3",
|
||||
"react-mdl": "^1.7.2",
|
||||
|
||||
@@ -4,6 +4,7 @@ const router = express.Router();
|
||||
// Get /password-reset expects a signed token (JWT) in the hash.
|
||||
// Links to this endpoint are generated by /views/password-reset-email.ejs.
|
||||
router.get('/password-reset', (req, res, next) => {
|
||||
|
||||
// TODO: store the redirect uri in the token or something fancy.
|
||||
// admins and regular users should probably be redirected to different places.
|
||||
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
|
||||
|
||||
@@ -82,6 +82,7 @@ router.post('/:asset_id/scrape', (req, res, next) => {
|
||||
});
|
||||
|
||||
router.put('/:asset_id/settings', (req, res, next) => {
|
||||
|
||||
// Override the settings for the asset.
|
||||
Asset
|
||||
.overrideSettings(req.params.asset_id, req.body)
|
||||
|
||||
@@ -9,14 +9,27 @@ const _ = require('lodash');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
router.get('/', (req, res, next) => {
|
||||
|
||||
const {
|
||||
status = null,
|
||||
action_type = null,
|
||||
asset_id = null
|
||||
asset_id = null,
|
||||
user_id = null
|
||||
} = req.query;
|
||||
|
||||
// everything on this route requires admin privileges besides listing comments for owner of said comments
|
||||
if (!authorization.has(req.user, 'admin') && !user_id) {
|
||||
next(authorization.ErrNotAuthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the user is not an admin, only return comment list for the owner of the comments
|
||||
if (req.user.id !== user_id && !authorization.has(req.user, 'admin')) {
|
||||
next(authorization.ErrNotAuthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds the asset_id requirement to the query if the asset_id is defined.
|
||||
*/
|
||||
@@ -30,7 +43,12 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
|
||||
let query;
|
||||
|
||||
if (status) {
|
||||
// the check for user_id MUST be first here.
|
||||
// otherwise this will be a vulnerability if you pass user_id and something else,
|
||||
// the app will return admin-level data without the proper checks
|
||||
if (user_id) {
|
||||
query = Comment.findByUserId(user_id);
|
||||
} else if (status) {
|
||||
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
|
||||
} else if (action_type) {
|
||||
query = Comment
|
||||
@@ -47,13 +65,15 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
query.then((comments) => {
|
||||
return Promise.all([
|
||||
comments,
|
||||
Asset.findMultipleById(comments.map(comment => comment.asset_id)),
|
||||
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
|
||||
]);
|
||||
})
|
||||
.then(([comments, users, actions])=>
|
||||
.then(([comments, assets, users, actions]) =>
|
||||
res.status(200).json({
|
||||
comments,
|
||||
assets,
|
||||
users,
|
||||
actions
|
||||
}))
|
||||
@@ -87,6 +107,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new Error(`asset has commenting closed because: ${asset.closedMessage}`));
|
||||
}
|
||||
@@ -97,6 +118,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
// 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, charCountEnable, charCount}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
@@ -113,6 +135,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
author_id: req.user.id
|
||||
}))
|
||||
.then((comment) => {
|
||||
|
||||
// The comment was created! Send back the created comment.
|
||||
res.status(201).json(comment);
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ router.get('/', (req, res, next) => {
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
|
||||
// Find or create the asset by url.
|
||||
Asset.findOrCreateByUrl(asset_url)
|
||||
|
||||
@@ -70,6 +71,7 @@ router.get('/', (req, res, next) => {
|
||||
settings
|
||||
]);
|
||||
})
|
||||
|
||||
// Get all the users and actions for those comments.
|
||||
.then(([comments, asset, settings]) => {
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ router.post('/request-password-reset', (req, res, next) => {
|
||||
to: email,
|
||||
html: resetEmailTemplate({
|
||||
token,
|
||||
|
||||
// probably more clear to explicitly pass this
|
||||
rootURL: process.env.TALK_ROOT_URL
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ router.use('/:embed', (req, res, next) => {
|
||||
case 'stream':
|
||||
return res.render('embed/stream', {});
|
||||
default:
|
||||
|
||||
// will return a 404.
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const mongoose = require('mongoose');
|
||||
const debug = require('debug')('talk:db');
|
||||
const enabled = require('debug').enabled;
|
||||
|
||||
//Append '-test' to the db if node_env === 'test'
|
||||
// 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') {
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('itemActions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
//Disabling tests for this function until is is used again.
|
||||
// Disabling tests for this function until is is used again.
|
||||
xdescribe('getItemsArray', () => {
|
||||
const response = {items: [{type: 'comment', id: '123'}, {type: 'comment', id: '456'}]};
|
||||
const ids = [1, 2];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import {shallow, mount} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import Comment from '../../../client/coral-plugin-history/Comment';
|
||||
|
||||
describe('coral-plugin-history/Comment', () => {
|
||||
let render;
|
||||
const comment = {body: 'this is a comment', id: '123'};
|
||||
const asset = {url: 'https://google.com'};
|
||||
|
||||
beforeEach(() => {
|
||||
render = shallow(<Comment asset={asset} comment={comment} />);
|
||||
});
|
||||
|
||||
it('should render the provided comment body', () => {
|
||||
const wrapper = mount(<Comment asset={asset} comment={comment} />);
|
||||
expect(wrapper.find('.myCommentBody')).to.have.length(1);
|
||||
expect(wrapper.find('.myCommentBody').text()).to.equal('this is a comment');
|
||||
});
|
||||
|
||||
it('should render the asset url as a link', () => {
|
||||
const wrapper = mount(<Comment asset={asset} comment={comment} />);
|
||||
expect(wrapper.find('.myCommentAnchor')).to.have.length(1);
|
||||
expect(wrapper.find('.myCommentAnchor').text()).to.equal('https://google.com');
|
||||
expect(wrapper.find('.myCommentAnchor').props().href).to.equal('https://google.com#123');
|
||||
});
|
||||
|
||||
it('should render the comment with styles', () => {
|
||||
expect(render.props().style).to.be.defined;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import {shallow, mount} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import CommentHistory from '../../../client/coral-plugin-history/CommentHistory';
|
||||
|
||||
describe('coral-plugin-history/CommentHistory', () => {
|
||||
let render;
|
||||
const comments = [{body: 'a comment or something', 'status_history':[{'type':'premod', 'created_at':'2016-12-09T01:40:53.327Z', 'assigned_by':null}, {'created_at':'2016-12-09T22:52:44.888Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-09T01:40:53.360Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T22:52:44.893Z', 'id':'3962c2ea-4ec4-42e4-b9bd-c571ff30f56b'}, {'body':'another comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-09T22:53:43.148Z', 'assigned_by':null}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-09T22:53:43.158Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'premod', '__v':0, 'updated_at':'2016-12-09T22:53:43.158Z', 'id':'b51e27af-bcfd-4932-91be-e3f01a4802e6'}, {'body':'can I comment?', 'status_history':[{'type':'premod', 'created_at':'2016-12-13T23:23:47.123Z', 'assigned_by':null}, {'created_at':'2016-12-13T23:23:58.487Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'cef81015-1b53-4d70-b9af-6eca680f22fc', 'created_at':'2016-12-13T23:23:47.131Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-13T23:23:58.493Z', 'id':'dc9d7be1-b911-4dc3-8e1e-400e8b8d110e'}, {'body':'pre-mod comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T21:34:56.994Z', 'assigned_by':null}, {'created_at':'2016-12-08T21:38:04.961Z', 'type':'rejected', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T21:34:56.997Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'rejected', '__v':0, 'updated_at':'2016-12-08T21:38:04.965Z', 'id':'6f02af16-a8f8-4ead-80ea-0d48824eb74d'}, {'body':'a flagged commetn', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T21:38:26.342Z', 'assigned_by':null}, {'created_at':'2016-12-09T23:47:27.009Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T21:38:26.344Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T23:47:27.018Z', 'id':'784c5f91-36b9-4bda-b4ca-a114cef2c9f0'}, {'body':'a post mod comment', 'status_history':[{'type':'premod', 'created_at':'2016-12-08T22:19:05.870Z', 'assigned_by':null}, {'created_at':'2016-12-09T23:26:41.427Z', 'type':'accepted', 'assigned_by':'92256159-1164-4f66-9970-c7f23de7e461'}], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T22:19:05.874Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':'accepted', '__v':0, 'updated_at':'2016-12-09T23:26:41.450Z', 'id':'e8b86039-f850-4e53-bd9d-f8c9186a9637'}, {'body':'an actual post-mod comment here', 'status_history':[], 'asset_id':'96fddf96-7c83-4008-80ad-50091997d006', 'created_at':'2016-12-08T22:20:11.147Z', 'author_id':'92256159-1164-4f66-9970-c7f23de7e461', 'status':null, '__v':0, 'updated_at':'2016-12-08T22:20:11.147Z', 'id':'cff1a318-50c6-431e-9a63-de7a7b7136bf'}];
|
||||
const assets = [{'settings': null, 'created_at':'2016-12-06T21:36:09.302Z', 'url':'localhost:3000/', 'scraped':null, 'status':'open', 'updated_at':'2016-12-08T02:11:15.943Z', '_id':'58472f499e775a38f23d5da0', 'type':'article', 'closedMessage':null, 'id':'7302e637-f884-47c0-9723-02cc10a18617', 'closedAt':null}, {'settings':null, 'created_at':'2016-12-07T02:25:31.983Z', 'url':'http://localhost:3000/', 'scraped':null, 'status':'open', 'updated_at':'2016-12-13T22:58:36.061Z', '_id':'5847731b9e775a38f23d5da1', 'type':'article', 'closedMessage':null, 'id':'96fddf96-7c83-4008-80ad-50091997d006', 'closedAt':null}, {'settings':null, 'created_at':'2016-12-12T19:04:05.770Z', 'url':'http://localhost:3000/embed/stream', 'scraped':null, 'updated_at':'2016-12-14T20:13:21.934Z', '_id':'584ef4a59e775a38f23d5e86', 'type':'article', 'closedMessage':null, 'id':'cef81015-1b53-4d70-b9af-6eca680f22fc', 'closedAt':null}];
|
||||
|
||||
beforeEach(() => {
|
||||
render = shallow(<CommentHistory comments={comments} assets={assets} />);
|
||||
});
|
||||
|
||||
it('should render Comments as children when given comments and assets', () => {
|
||||
const wrapper = mount(<CommentHistory comments={comments} assets={assets} />);
|
||||
expect(wrapper.find('.commentHistory__list').children()).to.have.length(7);
|
||||
});
|
||||
|
||||
it('should render with styles', () => {
|
||||
expect(render.props().style).to.be.defined;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,12 +18,13 @@ module.exports = {
|
||||
client.perform((client, done) => {
|
||||
mocks.settings({moderation: 'post'})
|
||||
.then(() => {
|
||||
//Load Page
|
||||
|
||||
// Load Page
|
||||
client.resizeWindow(1200, 800)
|
||||
.url(client.globals.baseUrl)
|
||||
.frame('coralStreamIframe')
|
||||
|
||||
//Register and Log In
|
||||
// Register and Log In
|
||||
.waitForElementVisible('#commentBox', 1000)
|
||||
.waitForElementVisible('#coralSignInButton', 2000)
|
||||
.click('#coralSignInButton')
|
||||
@@ -44,7 +45,7 @@ module.exports = {
|
||||
.click('.coral-plugin-commentbox-button')
|
||||
.waitForElementVisible('.comment', 1000)
|
||||
|
||||
//Verify that it appears
|
||||
// Verify that it appears
|
||||
.assert.containsText('.comment', mockComment);
|
||||
done();
|
||||
})
|
||||
@@ -58,7 +59,8 @@ module.exports = {
|
||||
client.perform((client, done) => {
|
||||
mocks.settings({moderation: 'pre'})
|
||||
.then(() => {
|
||||
//Load Page
|
||||
|
||||
// Load Page
|
||||
client.url(client.globals.baseUrl)
|
||||
.frame('coralStreamIframe');
|
||||
|
||||
@@ -68,7 +70,7 @@ module.exports = {
|
||||
.click('.coral-plugin-commentbox-button')
|
||||
.waitForElementVisible('#coral-notif', 1000)
|
||||
|
||||
//Verify that it appears
|
||||
// Verify that it appears
|
||||
.assert.containsText('#coral-notif', 'moderation team');
|
||||
done();
|
||||
})
|
||||
@@ -82,7 +84,8 @@ module.exports = {
|
||||
client.perform((client, done) => {
|
||||
mocks.settings({moderation: 'post'})
|
||||
.then(() => {
|
||||
//Load Page
|
||||
|
||||
// Load Page
|
||||
client.resizeWindow(1200, 800)
|
||||
.url(client.globals.baseUrl)
|
||||
.frame('coralStreamIframe');
|
||||
@@ -100,7 +103,7 @@ module.exports = {
|
||||
.click('.coral-plugin-replies-textarea button')
|
||||
.waitForElementVisible('.reply', 2000)
|
||||
|
||||
//Verify that it appears
|
||||
// Verify that it appears
|
||||
.assert.containsText('.reply', mockReply);
|
||||
done();
|
||||
})
|
||||
@@ -132,7 +135,8 @@ module.exports = {
|
||||
}]);
|
||||
})
|
||||
.then(() => {
|
||||
//Load Page
|
||||
|
||||
// Load Page
|
||||
client.resizeWindow(1200, 800)
|
||||
.url(client.globals.baseUrl)
|
||||
.frame('coralStreamIframe');
|
||||
@@ -145,7 +149,7 @@ module.exports = {
|
||||
.click('.coral-plugin-replies-textarea button')
|
||||
.waitForElementVisible('#coral-notif', 1000)
|
||||
|
||||
//Verify that it appears
|
||||
// Verify that it appears
|
||||
.assert.containsText('#coral-notif', 'moderation team');
|
||||
done();
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const jsdom = require('jsdom').jsdom;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Storage Mock
|
||||
function storageMock() {
|
||||
const storage = {};
|
||||
|
||||
return {
|
||||
setItem: function(key, value) {
|
||||
storage[key] = value || '';
|
||||
},
|
||||
getItem: function(key) {
|
||||
return storage[key] || null;
|
||||
},
|
||||
removeItem: function(key) {
|
||||
delete storage[key];
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(storage).length;
|
||||
},
|
||||
key: function(i) {
|
||||
const keys = Object.keys(storage);
|
||||
return keys[i] || null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
global.document = jsdom(fs.readFileSync(path.resolve(__dirname, 'index.test.html')));
|
||||
global.window = document.defaultView;
|
||||
|
||||
// these lines are required for react-mdl
|
||||
global.window.CustomEvent = undefined;
|
||||
require('react-mdl/extra/material');
|
||||
|
||||
global.Element = global.window.Element;
|
||||
|
||||
global.navigator = {
|
||||
userAgent: 'node.js'
|
||||
};
|
||||
|
||||
global.documentRef = document;
|
||||
global.localStorage = {};
|
||||
global.sessionStorage = storageMock();
|
||||
global.XMLHttpRequest = storageMock();
|
||||
|
||||
global.Headers = function(headers) {
|
||||
return headers;
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Coral - (Beta)</title>
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<style>
|
||||
/**
|
||||
* material-design-lite - Material Design Components in CSS, JS and HTML
|
||||
* @version v1.1.3
|
||||
* @license Apache-2.0
|
||||
* @copyright 2015 Google, Inc.
|
||||
* @link https://github.com/google/material-design-lite
|
||||
*/
|
||||
@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%;margin:0}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:#ff4081;font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weigLine truncated
|
||||
</style>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/img/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
animation: load 2s infinite ease;
|
||||
}
|
||||
|
||||
@keyframes load {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animatedBackground {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 0 100%; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="skin-blue sidebar-mini">
|
||||
<div id='root'>
|
||||
<!-- Coral logo spinner -->
|
||||
<svg class="loader" width="20%" height="20%" viewBox="0 0 381 391"version=1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><g stroke=none stroke-width=1 fill=none fill-rule=evenodd><g id=Wordmark-Round transform="translate(-1833.000000, -411.000000)"stroke=#FFFFFF stroke-width=22 stroke-linecap=round stroke-linejoin=round><g id=coralProjectLogo-2-Copy-2 transform="translate(1842.000000, 421.000000)"><g id=Layer_2 transform="translate(2.268750, 1.133903)"><rect id=Rectangle-1 fill=#F47E6B x=0 y=0 width=358.4625 height=368.518519 rx=40></rect><path d="M0.226875,105.679772 C13.7259375,122.688319 41.29125,131.986325 56.71875,116.792023 C67.0415625,106.586895 62.5040625,93.0934473 76.910625,63.3851852 C83.94375,48.7578348 90.523125,38.3259259 101.980313,37.5321937 C115.139063,36.6250712 131.814375,50.3452991 134.31,69.3948718 C137.826563,95.1344729 115.479375,105.339601 121.15125,123.822222 C127.390313,144.119088 157.110938,140.377208 166.52625,165.096296 C170.042813,174.394302 171.630938,190.382336 163.463438,198.319658 C149.170313,212.266667 120.584063,186.073504 80.7675,198.319658 C73.280625,200.587464 56.3784375,205.803419 51.500625,219.523647 C46.73625,233.130484 55.584375,251.046154 67.60875,260.797721 C93.245625,281.661538 119.79,254.674644 159.379688,271.909972 C181.840313,281.661538 203.053125,303.319088 208.725,330.305983 C211.674375,344.593162 209.6325,356.952707 207.704063,364.549858"id=Shape></path></g><g id=Layer_3 transform="translate(43.106250, 289.145299)"><path d="M90.4096875,72.5698006 C78.8390625,41.3874644 64.9996875,31.4091168 54.1096875,28.234188 C41.8584375,24.7190883 30.7415625,29.0279202 17.8096875,21.2039886 C8.394375,15.4210826 3.403125,6.57663818 0.680625,0"id=Shape></path></g><g id=Layer_4 transform="translate(220.068750, 209.772080)"><path d="M81.7884375,152.963533 C74.9821875,122.007977 61.8234375,104.77265 50.593125,94.5675214 C31.8759375,77.6723647 14.746875,77.1054131 5.218125,57.2621083 C4.4240625,55.5612536 -5.7853125,33.4501425 5.218125,17.008547 C14.52,3.06153846 33.350625,1.47407407 41.518125,0.907122507 C64.3190625,-0.907122507 73.280625,9.52478632 100.959375,13.039886 C117.180938,15.0809117 130.793438,13.4934473 139.30125,12.0193732"id=Shape></path></g><g id=Layer_5 transform="translate(285.862500, 289.145299)"><path d="M74.415,2.04102564 C66.3609375,0.793732194 51.046875,-0.566951567 33.12375,5.1025641 C17.5828125,9.97834758 6.80625,18.0290598 0.9075,23.2450142"id=Shape></path></g><g id=Layer_6 transform="translate(174.693750, 1.133903)"><path d="M184.109063,151.035897 C170.950313,174.507692 158.699063,180.290598 149.850938,181.311111 C133.85625,183.011966 129.091875,168.724786 104.475938,163.168661 C79.2928125,157.499145 72.2596875,169.745299 52.0678125,163.168661 C30.0609375,155.911681 18.7171875,134.821083 12.705,123.822222 C-1.588125,97.4022792 -0.3403125,71.6626781 0.5671875,51.2524217 C1.588125,29.4814815 6.125625,12.0193732 9.6421875,0.907122507"id=Shape></path><path d="M183.541875,69.3948718 C170.496563,56.6951567 157.564688,45.8096866 149.28375,39.0062678 C143.385,34.1304843 134.990625,33.2233618 128.297813,36.8518519 C128.070938,36.9652422 127.844063,37.0786325 127.730625,37.1920228 C122.739375,40.1401709 119.449688,45.3561254 118.8825,51.1390313 C118.201875,59.0763533 117.0675,71.4358974 114.912188,82.8883191 C114.118125,87.0837607 107.085,103.865527 89.7290625,110.668946 C77.2509375,115.544729 62.0503125,110.668946 53.4290625,101.597721 C40.38375,87.7641026 44.8078125,66.9002849 47.416875,55.2210826 C53.5425,26.760114 73.3940625,9.07122507 82.6959375,1.81424501"id=Shape></path></g><g id=Layer_7 transform="translate(3.403125, 179.156695)"><path d="M0.1134375,0.226780627 C2.949375,6.34985755 8.394375,16.1014245 18.2634375,25.3994302 C27.67875,34.2438746 37.3209375,39.0062678 43.4465625,41.5008547"id=Shape></path></g></g></g></g></svg>
|
||||
</div>
|
||||
<script>
|
||||
/**
|
||||
* material-design-lite - Material Design Components in CSS, JS and HTML
|
||||
* @version v1.1.3
|
||||
* @license Apache-2.0
|
||||
* @copyright 2015 Google, Inc.
|
||||
* @link https://github.com/google/material-design-lite
|
||||
*/
|
||||
!function(){"use strict";function e(e,t){if(e){if(t.element_.classList.contains(t.CssClasses_.MDL_JS_RIPPLE_EFFECT)){var s=document.createElement("span");s.classList.add(t.CssClasses_.MDL_RIPPLE_CONTAINER),s.classList.add(t.CssClasses_.MDL_JS_RIPPLE_EFFECT);var i=document.createElement("span");i.classList.add(t.CssClasses_.MDL_RIPPLE),s.appendChild(i),e.appendChild(s)}e.addEventListener("click",function(s){s.preventDefault();var i=e.href.split("#")[1],n=t.element_.querySelector("#"+i);t.resetTabState_(),t.resetPanelState_(),e.classList.add(t.CssClasses_.ACTIVE_CLASS),n.classList.add(t.CssClasses_.ACTIVE_CLASS)})}}function t(e,t,s,i){function n(){var n=e.href.split("#")[1],a=i.content_.querySelector("#"+n);i.resetTabState_(t),i.resetPanelState_(s),e.classList.add(i.CssClasses_.IS_ACTIVE),a.classList.add(i.CssClasses_.IS_ACTIVE)}if(i.tabBar_.classList.contains(i.CssClasses_.JS_RIPPLE_EFFECT)){var a=document.createElement("span");a.classList.add(i.CssClasses_.RIPPLE_CONTAINER),a.classList.add(i.CssClasses_.JS_RIPPLE_EFFECT);var l=document.createElement("span");l.classList.add(i.CssClasses_.RIPPLE),a.appendChild(l),e.appendChild(a)}e.addEventListener("click",function(t){"#"===e.getAttribute("href").charAt(0)&&(t.preventDefault(),n())}),e.show=n}if("undefined"!=typeof window){var s={upgradeDom:function(e,t){},upgradeElement:function(e,t){},upgradeElements:function(e){},upgradeAllRegistered:function(){},registerUpgradedCallback:function(e,t){},register:function(e){},downgradeElements:function(e){}};s=function(){function e(e,t){for(var s=0;s<h.length;s++)if(h[s].className===e)return"undefined"!=typeof t&&(h[s]=t),h[s];return!1}function t(e){var t=e.getAttribute("data-upgraded");return null===t?[""]:t.split(",")}function s(e,s){var i=t(e);return-1!==i.indexOf(s)}function i(t,s){if("undefined"==typeof t&&"undefined"==typeof s)for(var a=0;a<h.length;a++)i(h[a].className,h[a].cssClass);else{var l=t;if("undefined"==typeof s){var o=e(l);o&&(s=o.cssClass)}for(var r=document.querySelectorAll("."+s),_=0;_<r.length;_++)n(r[_],l)}}function n(i,n){if(!("object"==typeof i&&i instanceof Element))throw new Error("Invalid argument provided to upgrade MDL element.");var a=t(i),l=[];if(n)s(i,n)||l.push(e(n));else{var o=i.classList;h.forEach(function(e){o.contains(e.cssClass)&&-1===l.indexOf(e)&&!s(i,e.className)&&l.push(e)})}for(var r,_=0,d=l.length;d>_;_++){if(r=l[_],!r)throw new Error("Unable to find a registered component for the given class.");a.push(r.className),i.setAttribute("data-upgraded",a.join(","));var C=new r.classConstructor(i);C[p]=r,c.push(C);for(var u=0,E=r.callbacks.length;E>u;u++)r.callbacks[u](i);r.widget&&(i[r.className]=C);var m;"CustomEvent"in window&&"function"==typeof window.CustomEvent?m=new Event("mdl-componentupgraded",{bubbles:!0,cancelable:!1}):(m=document.createEvent("Events"),m.initEvent("mdl-componentupgraded",!0,!0)),i.dispatchEvent(m)}}function a(e){Array.isArray(e)||(e="function"==typeof e.item?Array.prototype.slice.call(e):[e]);for(var t,s=0,i=e.length;i>s;s++)t=e[s],t instanceof HTMLElement&&(n(t),t.children.length>0&&a(t.children))}function l(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(h.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(p))throw new Error("MDL component classes must not have "+p+" defined as a property.");var a=e(t.classAsString,n);a||h.push(n)}function o(t,s){var i=e(t);i&&i.callbacks.push(s)}function r(){for(var e=0;e<h.length;e++)i(h[e].className)}function _(e){if(e){var t=c.indexOf(e);c.splice(t,1);var s=e.element_.getAttribute("data-upgraded").split(","),i=s.indexOf(e[p].classAsString);s.splice(i,1),e.element_.setAttribute("data-upgraded",s.join(","));var n;"CustomEvent"in window&&"function"==typeof window.CustomEvent?n=new Event("mdl-componentdowngraded",{bubbles:!0,cancelable:!1}):(n=document.createEvent("Events"),n.initEvent("mdl-componentdowngraded",!0,!0))}}function d(e){var t=function(e){c.filter(function(t){return t.element_===e}).forEach(_)};if(e instanceof Array||e instanceof NodeList)for(var s=0;s<e.length;s++)t(e[s]);else{if(!(e instanceof Node))throw new Error("Invalid argument provided to downgrade MDL nodes.");t(e)}}var h=[],c=[],p="mdlComponentConfigInternal_";return{upgradeDom:i,upgradeElement:n,upgradeElements:a,upgradeAllRegistered:r,registerUpgradedCallback:o,register:l,downgradeElements:d}}(),s.ComponentConfigPublic,s.ComponentConfig,s.Component,s.upgradeDom=s.upgradeDom,s.upgradeElement=s.upgradeElement,s.upgradeElements=s.upgradeElements,s.upgradeAllRegistered=s.upgradeAllRegistered,s.registerUpgradedCallback=s.registerUpgradLine truncated
|
||||
this.actionElement_.addEventListener("click",this.actionHandler_),this.setActionHidden_(!1)),this.textElement_.textContent=this.message_,this.element_.classList.add(this.cssClasses_.ACTIVE),this.element_.setAttribute("aria-hidden","false"),setTimeout(this.cleanup_.bind(this),this.timeout_)},C.prototype.showSnackbar=function(e){if(void 0===e)throw new Error("Please provide a data object with at least a message to display.");if(void 0===e.message)throw new Error("Please provide a message to be displayed.");if(e.actionHandler&&!e.actionText)throw new Error("Please provide action text with the handler.");this.active?this.queuedNotifications_.push(e):(this.active=!0,this.message_=e.message,e.timeout?this.timeout_=e.timeout:this.timeout_=2750,e.actionHandler&&(this.actionHandler_=e.actionHandler),e.actionText&&(this.actionText_=e.actionText),this.displaySnackbar_())},C.prototype.showSnackbar=C.prototype.showSnackbar,C.prototype.checkQueue_=function(){this.queuedNotifications_.length>0&&this.showSnackbar(this.queuedNotifications_.shift())},C.prototype.cleanup_=function(){this.element_.classList.remove(this.cssClasses_.ACTIVE),setTimeout(function(){this.element_.setAttribute("aria-hidden","true"),this.textElement_.textContent="",Boolean(this.actionElement_.getAttribute("aria-hidden"))||(this.setActionHidden_(!0),this.actionElement_.textContent="",this.actionElement_.removeEventListener("click",this.actionHandler_)),this.actionHandler_=void 0,this.message_=void 0,this.actionText_=void 0,this.active=!1,this.checkQueue_()}.bind(this),this.Constant_.ANIMATION_LENGTH)},C.prototype.setActionHidden_=function(e){e?this.actionElement_.setAttribute("aria-hidden","true"):this.actionElement_.removeAttribute("aria-hidden")},s.register({constructor:C,classAsString:"MaterialSnackbar",cssClass:"mdl-js-snackbar",widget:!0});var u=function(e){this.element_=e,this.init()};window.MaterialSpinner=u,u.prototype.Constant_={MDL_SPINNER_LAYER_COUNT:4},u.prototype.CssClasses_={MDL_SPINNER_LAYER:"mdl-spinner__layer",MDL_SPINNER_CIRCLE_CLIPPER:"mdl-spinner__circle-clipper",MDL_SPINNER_CIRCLE:"mdl-spinner__circle",MDL_SPINNER_GAP_PATCH:"mdl-spinner__gap-patch",MDL_SPINNER_LEFT:"mdl-spinner__left",MDL_SPINNER_RIGHT:"mdl-spinner__right"},u.prototype.createLayer=function(e){var t=document.createElement("div");t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER),t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER+"-"+e);var s=document.createElement("div");s.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),s.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);var i=document.createElement("div");i.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);var n=document.createElement("div");n.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),n.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);for(var a=[s,i,n],l=0;l<a.length;l++){var o=document.createElement("div");o.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE),a[l].appendChild(o)}t.appendChild(s),t.appendChild(i),t.appendChild(n),this.element_.appendChild(t)},u.prototype.createLayer=u.prototype.createLayer,u.prototype.stop=function(){this.element_.classList.remove("is-active")},u.prototype.stop=u.prototype.stop,u.prototype.start=function(){this.element_.classList.add("is-active")},u.prototype.start=u.prototype.start,u.prototype.init=function(){if(this.element_){for(var e=1;e<=this.Constant_.MDL_SPINNER_LAYER_COUNT;e++)this.createLayer(e);this.element_.classList.add("is-upgraded")}},s.register({constructor:u,classAsString:"MaterialSpinner",cssClass:"mdl-js-spinner",widget:!0});var E=function(e){this.element_=e,this.init()};window.MaterialSwitch=E,E.prototype.Constant_={TINY_TIMEOUT:.001},E.prototype.CssClasses_={INPUT:"mdl-switch__input",TRACK:"mdl-switch__track",THUMB:"mdl-switch__thumb",FOCUS_HELPER:"mdl-switch__focus-helper",RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",RIPPLE_CONTAINER:"mdl-switch__ripple-container",RIPPLE_CENTER:"mdl-ripple--center",RIPPLE:"mdl-ripple",IS_FOCUSED:"is-focused",IS_DISABLED:"is-disabled",IS_CHECKED:"is-checked"},E.prototype.onChange_=function(e){this.updateClasses_()},E.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},E.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},E.prototype.onMouseUp_=function(e){this.blur_()},E.prototype.updateClasses_=function(){this.checkDisabled(),this.checkToggleState()},E.prototype.blur_=function(){window.setTimeout(function(){this.inputElement_.blur()}.bind(this),this.Constant_.TINY_TIMEOUT)},E.prototype.checkDisabled=function(){this.inputElement_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},E.prototype.checkDisabled=E.prototype.checkDisabled,E.prototype.checkToggleState=function(){this.inputElement_.checked?this.element_.classList.add(this.CssClasses_.IS_CHECKED):this.element_.classList.remove(this.CssLine truncated
|
||||
|
||||
</script>
|
||||
<script src="/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -85,6 +85,29 @@ describe('/api/v1/comments', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return only the owner’s comments if the user is not an admin', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?user_id=456')
|
||||
.set(passport.inject({id: '456', roles: []}))
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.comments).to.have.length(2);
|
||||
expect(res.body.comments[1]).to.have.property('author_id', '456');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if a non-admin requests comments not owned by them', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?user_id=456')
|
||||
.set(passport.inject({id: '123', roles: []}))
|
||||
.then((res) => {
|
||||
expect(res).to.be.empty;
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.have.property('status', 401);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all the comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments')
|
||||
|
||||
@@ -3,8 +3,10 @@ const mongoose = require('../../services/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() {});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user