mirror of
https://github.com/wassname/talk.git
synced 2026-08-15 12:55:10 +08:00
Merge branch 'master' of github.com:coralproject/talk into admin-asset-list
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015-minimal"
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
["transform-decorators-legacy"],
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
client/lib
|
||||
**/*.html
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,6 +54,21 @@ const updateClosedMessage = (updateSettings) => (event) => {
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
// 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 = ({updateSettings, settingsError, settings, errors}) => <List>
|
||||
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
@@ -110,6 +132,27 @@ 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 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')}
|
||||
@@ -124,4 +167,20 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
|
||||
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;
|
||||
|
||||
@@ -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';
|
||||
@@ -70,6 +71,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 +98,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='#' 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
|
||||
|
||||
@@ -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",
|
||||
@@ -58,6 +59,10 @@
|
||||
"streams": "Streams",
|
||||
"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.",
|
||||
@@ -108,7 +113,8 @@
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"showshortcuts": "Mostrar atajos"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
@@ -134,6 +140,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 => ({
|
||||
|
||||
@@ -61,7 +61,11 @@ class CommentStream extends Component {
|
||||
// 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;
|
||||
@@ -90,7 +94,7 @@ class CommentStream extends Component {
|
||||
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 {status, moderation, closedMessage} = this.props.config;
|
||||
const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const {activeTab} = this.state;
|
||||
const banned = (this.props.userData.status === 'banned');
|
||||
@@ -128,7 +132,7 @@ class CommentStream extends Component {
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}
|
||||
author={user}
|
||||
/>
|
||||
charCount={charCountEnable && charCount}/>
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>{closedMessage}</p>
|
||||
@@ -198,6 +202,7 @@ class CommentStream extends Component {
|
||||
parent_id={commentId}
|
||||
premod={moderation}
|
||||
currentUser={user}
|
||||
charCount={charCountEnable && charCount}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.children &&
|
||||
@@ -257,6 +262,7 @@ class CommentStream extends Component {
|
||||
premod={moderation}
|
||||
banned={banned}
|
||||
currentUser={user}
|
||||
charCount={charCountEnable && charCount}
|
||||
showReply={reply.showReply}/>
|
||||
</div>;
|
||||
})
|
||||
|
||||
@@ -95,7 +95,7 @@ hr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#coralStream .coral-plugin-commentbox-button {
|
||||
.coral-plugin-commentbox-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
padding: 5px 10px;
|
||||
@@ -111,6 +111,16 @@ hr {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.coral-plugin-commentbox-char-count {
|
||||
color: #ccc;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.coral-plugin-commentbox-char-max {
|
||||
color: #d50000;
|
||||
}
|
||||
|
||||
/* Comment styles */
|
||||
.comment {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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')}`}
|
||||
|
||||
@@ -23,7 +23,18 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
postComment = () => {
|
||||
const {postItem, updateItem, id, parent_id, child_id, addNotification, appendItemArray, premod, author} = this.props;
|
||||
const {
|
||||
postItem,
|
||||
updateItem,
|
||||
id,
|
||||
parent_id,
|
||||
child_id,
|
||||
addNotification,
|
||||
appendItemArray,
|
||||
premod,
|
||||
author
|
||||
} = this.props;
|
||||
|
||||
let comment = {
|
||||
body: this.state.body,
|
||||
asset_id: id,
|
||||
@@ -42,6 +53,10 @@ class CommentBox extends Component {
|
||||
if (child_id || parent_id) {
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
}
|
||||
|
||||
if (this.props.charCount && this.state.body.length > this.props.charCount) {
|
||||
return;
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then((postedComment) => {
|
||||
const commentId = postedComment.id;
|
||||
@@ -59,8 +74,8 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply, author} = this.props;
|
||||
// How to handle language in plugins? Should we have a dependency on our central translation file?
|
||||
const {styles, reply, author, charCount} = this.props;
|
||||
const length = this.state.body.length;
|
||||
return <div>
|
||||
<div
|
||||
className={`${name}-container`}>
|
||||
@@ -79,10 +94,16 @@ class CommentBox extends Component {
|
||||
onChange={(e) => this.setState({body: e.target.value})}
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={`${name}-char-count ${length > charCount ? `${name}-char-max` : ''}`}>
|
||||
{
|
||||
charCount &&
|
||||
`${charCount - length} ${lang.t('characters-remaining')}`
|
||||
}
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
{ author && (
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
|
||||
className={`${name}-button`}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"name": "Name",
|
||||
"comment-post-notif": "Your comment has been posted.",
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
|
||||
"comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
|
||||
"comment-post-banned-word": "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team.",
|
||||
"characters-remaining": " characters remaining"
|
||||
},
|
||||
"es": {
|
||||
"post": "Publicar",
|
||||
@@ -15,6 +16,7 @@
|
||||
"name": "Nombre",
|
||||
"comment-post-notif": "Tu comentario ha sido publicado.",
|
||||
"comment-post-notif-premod": "Gracias por comentar. Nuestro equipo de moderación va a revisarlo muy pronto.",
|
||||
"comment-post-banned-word": "Tu comentario contiene una o más palabras que no estan permitidasen nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
|
||||
"comment-post-banned-word": "Tu comentario contiene una o más palabras que no estan permitidasen nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación.",
|
||||
"characters-remaining": "carácteres restantes"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -22,6 +22,7 @@ class SignInContainer extends Component {
|
||||
|
||||
componentWillMount () {
|
||||
// Fetch commentHistory
|
||||
this.props.fetchCommentsByUserId(this.props.userData.id);
|
||||
}
|
||||
|
||||
handleTabChange(tab) {
|
||||
@@ -31,17 +32,33 @@ 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 +68,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(
|
||||
|
||||
@@ -77,6 +77,17 @@
|
||||
background: #696969;
|
||||
}
|
||||
|
||||
.type--lightGrey {
|
||||
color: white;
|
||||
background: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.type--lightGrey:hover {
|
||||
color: white;
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.type--green {
|
||||
color: white;
|
||||
background: #00897B;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+12
-19
@@ -84,7 +84,15 @@ SettingSchema.method('merge', function(src) {
|
||||
*/
|
||||
SettingSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['moderation', 'infoBoxEnable', 'infoBoxContent']);
|
||||
return _.pick(this.toJSON(), [
|
||||
'moderation',
|
||||
'infoBoxEnable',
|
||||
'infoBoxContent',
|
||||
'closeTimeout',
|
||||
'closedMessage',
|
||||
'charCountEnable',
|
||||
'charCount'
|
||||
]);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
@@ -106,11 +114,6 @@ const SettingService = module.exports = {};
|
||||
*/
|
||||
const selector = {id: '1'};
|
||||
|
||||
/**
|
||||
* The list of settings that can be viewed publicly.
|
||||
*/
|
||||
const publicSettings = 'moderation infoBoxEnable infoBoxContent closeTimeout closedMessage charCountEnable charCount';
|
||||
|
||||
/**
|
||||
* Cache expiry time in seconds for when the cached entry of the settings object
|
||||
* expires. 2 minutes.
|
||||
@@ -125,14 +128,6 @@ SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => {
|
||||
return Setting.findOne(selector);
|
||||
}).then((setting) => new Setting(setting));
|
||||
|
||||
/**
|
||||
* Gets publicly available settings records
|
||||
* @return {Promise} settings the publicly viewable settings record
|
||||
*/
|
||||
SettingService.public = () => cache.wrap('publicSettings', EXPIRY_TIME, () => {
|
||||
return Setting.findOne(selector, publicSettings);
|
||||
}).then((setting) => new Setting(setting));
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
* @param {object} setting a hash of whatever settings you want to update
|
||||
@@ -147,11 +142,9 @@ SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
|
||||
}).then((settings) => {
|
||||
|
||||
// Invalidate the settings cache.
|
||||
return Promise.all([
|
||||
cache.set('settings', settings, EXPIRY_TIME),
|
||||
cache.invalidate('publicSettings')
|
||||
])
|
||||
.then(() => settings);
|
||||
return cache
|
||||
.set('settings', settings, EXPIRY_TIME)
|
||||
.then(() => settings);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+7
-1
@@ -136,7 +136,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();
|
||||
|
||||
+5
-2
@@ -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",
|
||||
@@ -86,13 +86,13 @@
|
||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||
"babel-polyfill": "^6.16.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-es2015-minimal": "^2.1.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"chai": "^3.5.0",
|
||||
"chai-http": "^3.0.0",
|
||||
"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",
|
||||
@@ -106,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",
|
||||
@@ -122,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",
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
@@ -96,7 +116,13 @@ 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}) => moderation === 'pre' ? 'premod' : '');
|
||||
.then(({moderation, charCountEnable, charCount}) => {
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : '';
|
||||
});
|
||||
}
|
||||
|
||||
status.then((status) => Comment.publicCreate({
|
||||
|
||||
@@ -38,7 +38,7 @@ router.get('/', (req, res, next) => {
|
||||
}),
|
||||
|
||||
// Get the moderation setting from the settings.
|
||||
Setting.public()
|
||||
Setting.retrieve()
|
||||
])
|
||||
.then(([asset, settings]) => {
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -24,11 +24,12 @@ describe('models.Asset', () => {
|
||||
});
|
||||
|
||||
describe('#findByUrl', ()=> {
|
||||
beforeEach(() => Asset.findOrCreateByUrl('http://test.com'));
|
||||
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
expect(asset).to.have.property('url', 'http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,20 +20,6 @@ describe('models.Setting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#public()', () => {
|
||||
it('should have a moderation field defined', () => {
|
||||
return Setting.public().then(settings => {
|
||||
expect(settings).to.have.property('moderation').and.to.equal('pre');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have the wordlist field defined', () => {
|
||||
return Setting.public().then(settings => {
|
||||
expect(settings).to.have.property('wordlist').and.to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update()', () => {
|
||||
it('should update the settings with a passed object', () => {
|
||||
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
|
||||
|
||||
+24
-25
@@ -95,12 +95,17 @@ describe('models.User', () => {
|
||||
});
|
||||
|
||||
describe('#ban', () => {
|
||||
|
||||
let mockComment;
|
||||
|
||||
beforeEach(() => {
|
||||
return Promise.all([
|
||||
Comment.create([{body: 'testing the comment for that user if it is rejected.', id: mockUsers[0].id}])
|
||||
return Comment.create([
|
||||
{
|
||||
body: 'testing the comment for that user if it is rejected.',
|
||||
id: mockUsers[0].id
|
||||
}
|
||||
])
|
||||
.then((comment) => {
|
||||
.then(([comment]) => {
|
||||
mockComment = comment;
|
||||
});
|
||||
});
|
||||
@@ -109,11 +114,11 @@ describe('models.User', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
|
||||
.then(() => {
|
||||
User.findById(mockUsers[0].id)
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('banned');
|
||||
});
|
||||
return User.findById(mockUsers[0].id);
|
||||
})
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('banned');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,23 +126,20 @@ describe('models.User', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
|
||||
.then(() => {
|
||||
Comment.findById(mockComment.id)
|
||||
.then((comment) => {
|
||||
expect(comment).to.have.property('status')
|
||||
.and.to.equal('rejected');
|
||||
});
|
||||
return Comment.findById(mockComment.id);
|
||||
})
|
||||
.then((comment) => {
|
||||
expect(comment).to.have.property('status')
|
||||
.and.to.equal('rejected');
|
||||
});
|
||||
});
|
||||
|
||||
it('should still disable and ban the user if there is no comment', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'banned', '')
|
||||
.then(() => {
|
||||
User.findById(mockUsers[0].id)
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('banned');
|
||||
});
|
||||
.then(() => User.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'banned');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -156,12 +158,9 @@ describe('models.User', () => {
|
||||
it('should set the status to active', () => {
|
||||
return User
|
||||
.setStatus(mockUsers[0].id, 'active', mockComment.id)
|
||||
.then(() => {
|
||||
User.findById(mockUsers[0].id)
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status')
|
||||
.and.to.equal('active');
|
||||
});
|
||||
.then(() => User.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
@@ -197,6 +220,28 @@ describe('/api/v1/comments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a rejected comment if the body is above the character count', () => {
|
||||
return Asset
|
||||
.findOrCreateByUrl('https://coralproject.net/article1')
|
||||
.then((asset) => {
|
||||
return Asset
|
||||
.overrideSettings(asset.id, {charCountEnable: true, charCount: 10})
|
||||
.then(() => asset);
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'This is way way way way way too long.', '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', 'rejected');
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t create a comment when the asset has expired commenting', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(0),
|
||||
|
||||
Reference in New Issue
Block a user