Merge branch 'master' into use-js-while-rendering

This commit is contained in:
David Jay
2016-12-13 14:06:37 -05:00
committed by Kim Gardner
13 changed files with 260 additions and 60 deletions
+4 -1
View File
@@ -26,7 +26,10 @@ export const updateSettings = settings => {
};
export const saveSettingsToServer = () => (dispatch, getState) => {
const settings = getState().settings.toJS().settings;
let settings = getState().settings.toJS().settings;
if (settings.charCount) {
settings.charCount = parseInt(settings.charCount);
}
dispatch({type: SAVE_SETTINGS_LOADING});
coralApi('/settings', {method: 'PUT', body: settings})
.then(() => {
@@ -8,9 +8,25 @@ import {
ListItemContent,
ListItemAction,
Textfield,
Checkbox
Checkbox,
Icon
} from 'react-mdl';
const updateCharCountEnable = (updateSettings, charCountChecked) => () => {
const charCountEnable = !charCountChecked;
updateSettings({charCountEnable});
};
const updateCharCount = (updateSettings, settingsError) => (event) => {
const charCount = event.target.value;
if (charCount.match(/[^0-9]/) || charCount.length === 0) {
settingsError('charCount', true);
} else {
settingsError('charCount', false);
}
updateSettings({charCount: charCount});
};
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'pre' ? 'post' : 'pre';
updateSettings({moderation});
@@ -31,48 +47,80 @@ const updateClosedMessage = (updateSettings) => (event) => {
updateSettings({closedMessage});
};
const CommentSettings = (props) => <List>
<ListItem className={styles.configSetting}>
<ListItemAction>
<Checkbox
onClick={updateModeration(props.updateSettings, props.settings.moderation)}
checked={props.settings.moderation === 'pre'} />
</ListItemAction>
{lang.t('configure.enable-pre-moderation')}
</ListItem>
<ListItem threeLine className={styles.configSettingInfoBox}>
<ListItemAction>
<Checkbox
onClick={updateInfoBoxEnable(props.updateSettings, props.settings.infoBoxEnable)}
checked={props.settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <List>
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSettingInfoBox} ${props.settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={updateInfoBoxContent(props.updateSettings)}
value={props.settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.closed-comments-desc')}
<Textfield
onChange={updateClosedMessage(props.updateSettings)}
value={props.settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>;
</ListItem>
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</ListItemContent>
</ListItem>
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onClick={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.closed-comments-desc')}
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>;
export default CommentSettings;
@@ -18,9 +18,27 @@
.configSetting {
border: 1px solid #ccc;
border-radius: 4px;
height: 90px;
height: 95px;
margin-bottom: 10px;
cursor: pointer;
align-items: flex-start;
}
.settingsError {
color: #d50000;
}
.settingsError i {
font-size: 14px;
margin-right: 3px;
}
.settingsHeader {
margin-top: 3px;
margin-bottom: 10px;
}
.disabledSettingText {
color: #ccc;
}
.configSettingInfoBox {
@@ -45,6 +63,22 @@
display: block;
}
.charCountTexfield {
width: 4em;
padding: 0px;
border-color: #ccc;
border-style: solid;
border-width: 0px 0px 1px 0px;
}
.charCountTexfieldEnabled {
border-color: #4caf50;
}
.charCountTexfield:focus {
outline: none;
}
.changedSave {
background-color:#4caf50;
}
@@ -84,7 +118,15 @@
margin-bottom:3px;
}
.enabledSetting {
border-left-color: #4caf50;
border-left-style: solid;
border-left-width: 7px;
}
.disabledSetting {
padding-left: 22px;
}
.hidden {
display: none;
@@ -22,7 +22,8 @@ class Configure extends React.Component {
this.state = {
activeSection: 'comments',
wordlist: [],
changed: false
changed: false,
errors: {}
};
}
@@ -64,12 +65,23 @@ class Configure extends React.Component {
this.props.dispatch(updateSettings(setting));
}
// Sets an arbitrary error string and a boolean state.
// This allows the system to track multiple errors.
onSettingError = (error, state) => {
this.setState((prevState) => {
prevState.errors[error] = state;
return prevState;
});
}
getSection = (section) => {
switch(section){
case 'comments':
return <CommentSettings
settings={this.props.settings}
updateSettings={this.onSettingUpdate}/>;
updateSettings={this.onSettingUpdate}
errors={this.state.errors}
settingsError={this.onSettingError}/>;
case 'embed':
return <EmbedLink/>;
case 'wordlist':
@@ -94,6 +106,9 @@ class Configure extends React.Component {
let pageTitle = this.getPageTitle(this.state.activeSection);
const section = this.getSection(this.state.activeSection);
const showSave = Object.keys(this.state.errors).reduce(
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
if (this.props.fetchingSettings) {
pageTitle += ' - Loading...';
}
@@ -119,7 +134,7 @@ class Configure extends React.Component {
</ListItem>
</List>
{
this.state.changed ?
showSave ?
<Button
raised
onClick={this.saveSettings}
+12 -2
View File
@@ -41,6 +41,7 @@
},
"configure": {
"enable-pre-moderation": "Enable pre-moderation",
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
@@ -55,7 +56,11 @@
"configure": "Configure",
"community": "Community",
"closed-comments-desc": "Write a message for closed threads",
"closed-comments-label": "Write a message..."
"closed-comments-label": "Write a message...",
"comment-count-header": "Limit Comment Length",
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number."
},
"bandialog": {
"ban_user": "Ban User?",
@@ -96,6 +101,7 @@
},
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
"include-text": "Incluir tu texto aqui.",
@@ -110,7 +116,11 @@
"configure": "Configurar",
"community": "Comunidad",
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
"closed-comments-label": "Escribe un mensaje..."
"closed-comments-label": "Escribe un mensaje...",
"comment-count-header": "Limitar el largo del comentario",
"comment-count-text-pre": "El largo de comentarios será ",
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido."
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
@@ -90,7 +90,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 +128,7 @@ class CommentStream extends Component {
currentUser={this.props.auth.user}
banned={banned}
author={user}
/>
charCount={charCountEnable && charCount}/>
</RestrictedContent>
</div>
: <p>{closedMessage}</p>
@@ -198,6 +198,7 @@ class CommentStream extends Component {
parent_id={commentId}
premod={moderation}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={comment.showReply}/>
{
comment.children &&
@@ -257,6 +258,7 @@ class CommentStream extends Component {
premod={moderation}
banned={banned}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={reply.showReply}/>
</div>;
})
+11 -1
View File
@@ -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;
+25 -4
View File
@@ -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 ? '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"
}
}
+11
View File
@@ -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;
+9 -1
View File
@@ -38,7 +38,15 @@ const SettingSchema = new Schema({
type: String,
default: ''
},
wordlist: [String]
wordlist: [String],
charCount: {
type: Number,
default: 5000
},
charCountEnable: {
type: Boolean,
default: false
}
}, {
timestamps: {
createdAt: 'created_at',
+7 -1
View File
@@ -97,7 +97,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({
+22
View File
@@ -197,6 +197,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),