Merge branch 'master' into locale-local-storage

This commit is contained in:
Kim Gardner
2018-01-18 11:21:20 -05:00
committed by GitHub
15 changed files with 202 additions and 102 deletions
@@ -4,7 +4,7 @@ import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({ children, body }) => {
const CommentAnimatedEdit = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -27,9 +27,9 @@ const CommentBodyHighlighter = ({ children, body }) => {
);
};
CommentBodyHighlighter.propTypes = {
CommentAnimatedEdit.propTypes = {
children: PropTypes.node,
body: PropTypes.string,
};
export default CommentBodyHighlighter;
export default CommentAnimatedEdit;
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
@@ -62,16 +63,47 @@ function markLinks(body) {
return content;
}
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
const CommentFormatter = ({
body,
suspectWords,
bannedWords,
className = 'comment',
...rest
}) => {
// Breaking the body by line break
const textbreaks = body.split('\n');
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
return (
<span className={`${className}-text`} {...rest}>
{textbreaks.map((line, i) => {
const content = markLinks(line).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<span key={i} className={`${className}-line`}>
{content}
{i !== textbreaks.length - 1 && (
<br className={`${className}-linebreak`} />
)}
</span>
);
})}
</span>
);
};
CommentFormatter.propTypes = {
className: PropTypes.string,
bannedWords: PropTypes.array,
suspectWords: PropTypes.array,
body: PropTypes.string,
};
export default CommentFormatter;
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import CommentAnimatedEdit from './CommentAnimatedEdit';
@@ -78,11 +78,12 @@ class UserDetailComment extends React.Component {
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<div className={styles.body}>
<CommentBodyHighlighter
<CommentFormatter
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>{' '}
className="talk-admin-user-detail-comment"
/>
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -8,7 +8,7 @@ import styles from './Comment.css';
import CommentLabels from 'coral-admin/src/components/CommentLabels';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
@@ -126,11 +126,12 @@ class Comment extends React.Component {
<CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}>
<div className={styles.body}>
<CommentBodyHighlighter
<CommentFormatter
suspectWords={settings.wordlist.suspect}
bannedWords={settings.wordlist.banned}
className="talk-admin-comment"
body={comment.body}
/>{' '}
/>
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
+13 -3
View File
@@ -265,6 +265,15 @@ const ErrCannotIgnoreStaff = new APIError('Cannot ignore staff members.', {
status: 400,
});
// ErrParentDoesNotVisible is returned when the user tries to reply to a comment
// that isn't visible.
const ErrParentDoesNotVisible = new APIError(
'Cannot reply to a comment that is not visible',
{
translation_key: 'COMMENT_PARENT_NOT_VISIBLE',
}
);
module.exports = {
APIError,
ErrAlreadyExists,
@@ -276,24 +285,25 @@ module.exports = {
ErrContainsProfanity,
ErrEditWindowHasEnded,
ErrEmailTaken,
ErrEmailVerificationToken,
ErrInstallLock,
ErrInvalidAssetURL,
ErrLoginAttemptMaximumExceeded,
ErrMaxRateLimit,
ErrMissingEmail,
ErrMissingPassword,
ErrEmailVerificationToken,
ErrPasswordResetToken,
ErrMissingUsername,
ErrNotAuthorized,
ErrNotFound,
ErrNotVerified,
ErrParentDoesNotVisible,
ErrPasswordResetToken,
ErrPasswordTooShort,
ErrPermissionUpdateUsername,
ErrSameUsernameProvided,
ErrSettingsInit,
ErrSettingsNotInit,
ErrSpecialChars,
ErrUsernameTaken,
ErrSameUsernameProvided,
ExtendableError,
};
+1
View File
@@ -198,6 +198,7 @@ en:
embedlink:
copy: "Copy to Clipboard"
error:
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
COMMENT_TOO_SHORT: "Comments should be more than one character, please revise your comment and try again."
+1
View File
@@ -182,6 +182,7 @@ es:
embedlink:
copy: "Copiar al portapapeles"
error:
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
+11
View File
@@ -37,6 +37,17 @@ const ActionSchema = new Schema(
}
);
// Create an index on the `item_id` field so that queries looking for
// actions based on the item id can resolve faster.
ActionSchema.index(
{
item_id: 1,
},
{
background: true,
}
);
const Action = mongoose.model('Action', ActionSchema);
module.exports = Action;
+9 -10
View File
@@ -68,17 +68,16 @@ then
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
fi
if [ "$CIRCLE_BRANCH" = "master" ]
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
then
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
then
deploy_tag
else
deploy_latest
fi
deploy_tag
else
deploy_branch
if [ "$CIRCLE_BRANCH" = "master" ]
then
deploy_latest
else
deploy_branch
fi
fi
fi
+20 -15
View File
@@ -7,24 +7,31 @@ const SettingsService = require('./settings');
const cloneDeep = require('lodash/cloneDeep');
const errors = require('../errors');
const events = require('./events');
const merge = require('lodash/merge');
const { COMMENTS_NEW, COMMENTS_EDIT } = require('./events/constants');
module.exports = class CommentsService {
/**
* Creates a new Comment that came from a public source.
* @param {Mixed} comment either a single comment or an array of comments.
* @param {Object} input either a single comment or an array of comments.
* @return {Promise}
*/
static async publicCreate(comment) {
// Check to see if this is an array of comments, if so map it out.
if (Array.isArray(comment)) {
return Promise.all(comment.map(CommentsService.publicCreate));
static async publicCreate(input) {
// Extract the parent_id from the comment, if there is one.
const { status = 'NONE', parent_id = null } = input;
// Check to see if we are replying to a comment, and if that comment is
// visible.
if (parent_id !== null) {
const parent = await CommentModel.findOne({ id: parent_id });
if (parent === null || !parent.visible) {
throw errors.ErrParentDoesNotVisible;
}
}
const { status = 'NONE' } = comment;
const commentModel = new CommentModel(
Object.assign(
// Create the comment in the database.
const comment = await CommentModel.create(
merge(
{
status_history: status
? [
@@ -36,21 +43,19 @@ module.exports = class CommentsService {
: [],
body_history: [
{
body: comment.body,
body: input.body,
created_at: new Date(),
},
],
},
comment
input
)
);
const savedCommentModel = await commentModel.save();
// Emit that the comment was created!
await events.emitAsync(COMMENTS_NEW, savedCommentModel);
await events.emitAsync(COMMENTS_NEW, comment);
return savedCommentModel;
return comment;
}
/**
+1 -1
View File
@@ -7,6 +7,6 @@
],
"extends": "../.eslintrc.json",
"rules": {
"mocha/no-exclusive-tests": "warn"
"mocha/no-exclusive-tests": "error"
}
}
@@ -116,6 +116,8 @@ describe('graph.mutations.createComment', () => {
}
expect(data.createComment).to.have.property('errors').null;
expect(data.createComment).to.have.property('comment').not.null;
expect(data.createComment.comment).to.have.property('id').not
.null;
}
}
);
+8 -6
View File
@@ -34,12 +34,14 @@ describe('graph.queries.asset', () => {
username: 'usernameC',
},
]);
comments = await CommentsService.publicCreate(
[0, 0, 1, 1].map(idx => ({
author_id: users[idx].id,
asset_id: assets[idx].id,
body: `hello there! ${String(Math.random()).slice(2)}`,
}))
comments = await Promise.all(
[0, 0, 1, 1].map(idx =>
CommentsService.publicCreate({
author_id: users[idx].id,
asset_id: assets[idx].id,
body: `hello there! ${String(Math.random()).slice(2)}`,
})
)
);
});
+24 -22
View File
@@ -176,28 +176,30 @@ describe('services.AssetsService', () => {
);
// Create some comments on both assets.
await CommentsService.publicCreate([
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
]);
await Promise.all(
[
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '1',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
asset_id: '2',
body: 'This is a comment!',
status: 'ACCEPTED',
},
].map(comment => CommentsService.publicCreate(comment))
);
// Merge all the comments from asset 1 into asset 2, followed by deleting
// asset 1.
+58 -25
View File
@@ -131,6 +131,64 @@ describe('services.CommentsService', () => {
});
describe('#publicCreate()', () => {
describe('does not allow replies to comments that are not visible', () => {
it('parent not found', async () => {
try {
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: 'does not exist',
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
it('parent REJECTED', async () => {
try {
const parent = await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'REJECTED',
});
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: parent.id,
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
it('parent SYSTEM_WITHHELD', async () => {
try {
const parent = await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'SYSTEM_WITHHELD',
});
await CommentsService.publicCreate({
body: 'This is a comment!',
status: 'ACCEPTED',
parent_id: parent.id,
});
throw new Error('comment should not have been created');
} catch (err) {
expect(err).to.have.property(
'translation_key',
'COMMENT_PARENT_NOT_VISIBLE'
);
}
});
});
it('creates a new comment', async () => {
const c = await CommentsService.publicCreate({
body: 'This is a comment!',
@@ -142,31 +200,6 @@ describe('services.CommentsService', () => {
expect(c.id).to.be.uuid;
expect(c.status).to.be.equal('ACCEPTED');
});
it('creates many new comments', async () => {
const [c1, c2, c3] = await CommentsService.publicCreate([
{
body: 'This is a comment!',
status: 'ACCEPTED',
},
{
body: 'This is another comment!',
},
{
body: 'This is a rejected comment!',
status: 'REJECTED',
},
]);
expect(c1).to.not.be.null;
expect(c1.status).to.be.equal('ACCEPTED');
expect(c2).to.not.be.null;
expect(c2.status).to.be.equal('NONE');
expect(c3).to.not.be.null;
expect(c3.status).to.be.equal('REJECTED');
});
});
describe('#edit', () => {