Merge branch 'master' into translations-de-4.4.1

This commit is contained in:
Kim Gardner
2018-05-21 11:06:17 -04:00
committed by GitHub
42 changed files with 469 additions and 212 deletions
+2 -1
View File
@@ -7,6 +7,7 @@
"https://nodesecurity.io/advisories/594",
"https://nodesecurity.io/advisories/603",
"https://nodesecurity.io/advisories/611",
"https://nodesecurity.io/advisories/612"
"https://nodesecurity.io/advisories/612",
"https://nodesecurity.io/advisories/654"
]
}
@@ -0,0 +1,16 @@
.external {
margin-bottom: 20px;
}
.separator h5 {
text-align: center;
font-size: 1.2em;
}
.slot > * {
margin-bottom: 8px;
&:last-child {
margin-bottom: 0px;
}
}
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './External.css';
import Slot from 'coral-framework/components/Slot';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
const External = ({ slot }) => (
<IfSlotIsNotEmpty slot={slot}>
<div>
<div className={styles.external}>
<Slot fill={slot} className={styles.slot} />
</div>
<div className={styles.separator}>
<h5>Or</h5>
</div>
</div>
</IfSlotIsNotEmpty>
);
External.propTypes = {
slot: PropTypes.string.isRequired,
};
export default External;
+49 -41
View File
@@ -4,6 +4,7 @@ import styles from './SignIn.css';
import { Button, TextField, Alert } from 'coral-ui';
import cn from 'classnames';
import Recaptcha from 'coral-framework/components/Recaptcha';
import External from './External';
class SignIn extends React.Component {
recaptcha = null;
@@ -33,48 +34,55 @@ class SignIn extends React.Component {
render() {
const { email, password, errorMessage, requireRecaptcha } = this.props;
return (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<TextField
id="password"
label="Password"
value={password}
onChange={this.handlePasswordChange}
type="password"
/>
{requireRecaptcha && (
<div className={styles.recaptcha}>
<Recaptcha
ref={this.handleRecaptchaRef}
onVerify={this.props.onRecaptchaVerify}
/>
</div>
)}
<Button
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
type="submit"
cStyle="black"
full
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={this.handleForgotPasswordLink}
<div className="talk-admin-login-sign-in">
<External slot="authExternalAdminSignIn" />
<form onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<TextField
id="password"
label="Password"
value={password}
onChange={this.handlePasswordChange}
type="password"
/>
{requireRecaptcha && (
<div className={styles.recaptcha}>
<Recaptcha
ref={this.handleRecaptchaRef}
onVerify={this.props.onRecaptchaVerify}
/>
</div>
)}
<Button
className={cn(
styles.signInButton,
'talk-admin-login-sign-in-button'
)}
type="submit"
cStyle="black"
full
>
Request a new one.
</a>
</p>
</form>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
{/* TODO: translate */}
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={this.handleForgotPasswordLink}
>
Request a new one.
</a>
</p>
</form>
</div>
);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withSignIn } from 'coral-framework/hocs';
import { withSignIn, withPopupAuthHandler } from 'coral-framework/hocs';
import { compose } from 'recompose';
import SignIn from '../components/SignIn';
@@ -55,4 +55,4 @@ SignInContainer.propTypes = {
requireRecaptcha: PropTypes.bool.isRequired,
};
export default compose(withSignIn)(SignInContainer);
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
@@ -82,6 +82,20 @@ class StreamSettings extends React.Component {
this.props.updatePending({ updater });
};
updateDisableCommenting = () => {
const updater = {
disableCommenting: {
$set: !this.props.settings.disableCommenting,
},
};
this.props.updatePending({ updater });
};
updateDisableCommentingMessage = value => {
const updater = { disableCommentingMessage: { $set: value } };
this.props.updatePending({ updater });
};
updateAutoClose = () => {
const updater = {
autoCloseStream: { $set: !this.props.settings.autoCloseStream },
@@ -192,6 +206,25 @@ class StreamSettings extends React.Component {
&nbsp;
{t('configure.edit_comment_timeframe_text_post')}
</ConfigureCard>
<ConfigureCard
checked={settings.disableCommenting}
onCheckbox={this.updateDisableCommenting}
title={t('configure.disable_commenting_title')}
>
<p>{t('configure.disable_commenting_desc')}</p>
<div
className={cn(
styles.configSettingDisableCommenting,
settings.disableCommenting ? null : styles.hidden
)}
>
<MarkdownEditor
className={styles.descriptionBox}
onChange={this.updateDisableCommentingMessage}
value={settings.disableCommentingMessage}
/>
</div>
</ConfigureCard>
<ConfigureCard
checked={settings.autoCloseStream}
onCheckbox={this.updateAutoClose}
@@ -39,6 +39,8 @@ export default compose(
autoCloseStream
closedTimeout
closedMessage
disableCommenting
disableCommentingMessage
${getSlotFragmentSpreads(slots, 'settings')}
}
`,
+27 -23
View File
@@ -3,32 +3,36 @@ import { getStaticConfiguration } from 'coral-framework/services/staticConfigura
import { createPostMessage } from 'coral-framework/services/postMessage';
document.addEventListener('DOMContentLoaded', () => {
try {
const staticConfig = getStaticConfiguration();
const { STATIC_ORIGIN: origin } = staticConfig;
const postMessage = createPostMessage(origin);
const staticConfig = getStaticConfiguration();
const { STATIC_ORIGIN: origin } = staticConfig;
const postMessage = createPostMessage(origin);
// Get the auth element and parse it as JSON by decoding it.
const auth = document.getElementById('auth');
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = auth.innerText;
// Get the auth element and parse it as JSON by decoding it.
const auth = document.getElementById('auth');
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = auth.innerText;
// Auth state is contained within the node.
const { err, data } = JSON.parse(doc.body.textContent);
if (err) {
// TODO: send back the error message.
console.error(err);
// Auth state is contained within the node.
const { err, data } = JSON.parse(doc.body.textContent);
if (err) {
const errDiv = document.createElement('div');
if (err.message) {
errDiv.innerText = `${err.name}: ${err.message}`;
} else {
// The data will contain a user and a token.
const { user, token } = data;
// Send the state back.
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
errDiv.innerText = JSON.stringify(err);
}
} finally {
// Always close the window.
setTimeout(() => {
window.close();
}, 50);
document.body.appendChild(errDiv);
throw err;
}
// The data will contain a user and a token.
const { user, token } = data;
// Send the state back.
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
// Close the window when all went well.
setTimeout(() => {
window.close();
}, 50);
});
@@ -745,10 +745,21 @@ export default class Comment extends React.Component {
const id = `c_${comment.id}`;
// props that are passed down the slots.
const slotPassthrough = {
action: 'deleted',
comment,
};
return (
<div className={rootClassName} id={id}>
{isCommentDeleted(comment) ? (
<CommentTombstone action="deleted" />
<Slot
fill="commentTombstone"
defaultComponent={CommentTombstone}
size={1}
passthrough={slotPassthrough}
/>
) : (
<div>
{this.renderComment()}
@@ -39,6 +39,7 @@ class CommentTombstone extends React.Component {
CommentTombstone.propTypes = {
action: PropTypes.string,
comment: PropTypes.object,
onUndo: PropTypes.func,
};
@@ -4,6 +4,7 @@ import StreamError from './StreamError';
import Comment from '../containers/Comment';
import BannedAccount from '../../../components/BannedAccount';
import ChangeUsername from '../containers/ChangeUsername';
import Markdown from 'coral-framework/components/Markdown';
import Slot from 'coral-framework/components/Slot';
import InfoBox from './InfoBox';
import { can } from 'coral-framework/services/perms';
@@ -181,7 +182,9 @@ class Stream extends React.Component {
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
disableReply={asset.isClosed}
disableReply={
asset.isClosed || asset.settings.disableCommenting
}
postComment={postComment}
currentUser={currentUser}
postFlag={postFlag}
@@ -215,7 +218,7 @@ class Stream extends React.Component {
currentUser,
} = this.props;
const { keepCommentBox } = this.state;
const open = !asset.isClosed;
const open = !(asset.isClosed || asset.settings.disableCommenting);
const banned = get(currentUser, 'status.banned.status');
const suspensionUntil = get(currentUser, 'status.suspension.until');
@@ -293,7 +296,13 @@ class Stream extends React.Component {
)}
</div>
) : (
<p>{asset.settings.closedMessage}</p>
<div>
{asset.isClosed ? (
<p>{asset.settings.closedMessage}</p>
) : (
<Markdown content={asset.settings.disableCommentingMessage} />
)}
</div>
)}
<Slot fill="stream" passthrough={slotPassthrough} />
@@ -24,6 +24,7 @@ const slots = [
'commentAuthorName',
'commentAuthorTags',
'commentTimestamp',
'commentTombstone',
'commentContent',
];
@@ -434,6 +434,8 @@ const fragments = {
questionBoxIcon
closedTimeout
closedMessage
disableCommenting
disableCommentingMessage
charCountEnable
charCount
requireEmailConfirmation
+1 -1
View File
@@ -116,7 +116,7 @@ export const withRemoveTag = withMutation(
asset_id: assetId,
item_type: itemType,
},
o3timisticResponse: {
optimisticResponse: {
removeTag: {
__typename: 'ModifyTagResponse',
errors: null,
@@ -56,10 +56,10 @@ export function createPostMessage(origin, scope = 'client') {
// Send the message.
target.postMessage(msg, origin);
},
subscribe: (handler, target = window) => {
subscribe(handler, target = window) {
// If this handler is already attached to the target, detach it.
if (has(listeners, [target, handler])) {
this.unsubscribeFromMessages(handler, target);
this.unsubscribe(handler, target);
}
// Wrap the listener with a origin check.
@@ -71,7 +71,7 @@ export function createPostMessage(origin, scope = 'client') {
// Attach the listener to the target.
target.addEventListener('message', listener);
},
unsubscribe: (handler, target = window) => {
unsubscribe(handler, target = window) {
if (!has(listeners, [target, handler])) {
return;
}
+20
View File
@@ -273,3 +273,23 @@ export function translateError(error) {
}
return error.toString();
}
/**
* handlePopupAuth will optionally open a popup with the requested uri if the
* window is not already a popup.
*
* @param {String} uri the url to open the window? to
* @param {String} title the title of the new window? to open
* @param {String} features the features to use when opening a window?
*/
export function handlePopupAuth(
uri,
title = 'Login', // TODO: translate
features = 'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
) {
if (window.opener) {
window.location = uri;
} else {
window.open(uri, title, features);
}
}
+1
View File
@@ -99,6 +99,7 @@ You won't have to use this to build plugins, but it's helpful to find where to e
* `commentReactions`
* `commentActions`
* `commentInputArea`
* `commentTombstone`
* `draftArea`
* `streamSettings`
+19
View File
@@ -161,6 +161,24 @@ class ErrAssetCommentingClosed extends TalkError {
}
}
// ErrCommentingDisabled is returned when a comment or action is attempted while
// commenting has been disabled site-wide.
class ErrCommentingDisabled extends TalkError {
constructor(message = null) {
super(
'asset commenting is closed',
{
status: 400,
translation_key: 'COMMENTING_DISABLED',
},
{
// Include the closedMessage in the metadata piece of the error.
message,
}
);
}
}
/**
* ErrAuthentication is returned when there is an error authenticating and the
* message is provided.
@@ -387,6 +405,7 @@ module.exports = {
ErrAuthentication,
ErrCannotIgnoreStaff,
ErrCommentTooShort,
ErrCommentingDisabled,
ErrContainsProfanity,
ErrEditWindowHasEnded,
ErrEmailAlreadyVerified,
+14
View File
@@ -837,6 +837,13 @@ type Settings {
# closed.
closedMessage: String
# disableCommenting will disable commenting site-wide.
disableCommenting: Boolean
# disableCommentingMessage will be shown above the comment stream while
# commenting is disabled site-wide.
disableCommentingMessage: String
# editCommentWindowLength is the length of time (in milliseconds) after a
# comment is posted that it can still be edited by the author.
editCommentWindowLength: Int
@@ -1300,6 +1307,13 @@ input UpdateSettingsInput {
# closed.
closedMessage: String
# disableCommenting will disable commenting site-wide.
disableCommenting: Boolean
# disableCommentingMessage will be shown above the comment stream while
# commenting is disabled site-wide.
disableCommentingMessage: String
# charCountEnable is true when the character count restriction is enabled.
charCountEnable: Boolean
+3
View File
@@ -124,6 +124,8 @@ de:
custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs"
days: Tage
description: "Ändern Sie die Einstellungen für den Kommentarbereich dieses Artikels."
disable_commenting_title: "Kommentieren global deaktivieren"
disable_commenting_desc: "Verfassen Sie eine Nachricht, die angezeigt wird, solange das Kommentieren deaktiviert ist."
domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Zugelassene Domains"
edit_info: "Information bearbeiten"
@@ -247,6 +249,7 @@ de:
LOGIN_MAXIMUM_EXCEEDED: "Sie haben zu häufig erfolglos versucht, sich anzumelden. Bitte warten Sie."
PASSWORD_REQUIRED: "Passwort ist erforderlich"
COMMENTING_CLOSED: "Kommentarbereich ist bereits geschlossen"
COMMENTING_DISABLED: "Die Kommentarfunktion ist derzeit abgeschaltet"
NOT_FOUND: "Ressource nicht gefunden"
ALREADY_EXISTS: "Ressource existiert bereits"
INVALID_ASSET_URL: "Asset-URL ist ungültig"
+3
View File
@@ -124,6 +124,8 @@ en:
custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external."
days: Days
description: "Change the comment settings on this story."
disable_commenting_title: "Deactivate commenting site-wide"
disable_commenting_desc: "Write a message that will be displayed while commenting is deactivated."
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Permitted Domains"
edit_info: "Edit Info"
@@ -247,6 +249,7 @@ en:
LOGIN_MAXIMUM_EXCEEDED: "You have made too many unsuccessful password attempts. Please wait."
PASSWORD_REQUIRED: "Must input a password"
COMMENTING_CLOSED: "Commenting is already closed"
COMMENTING_DISABLED: "Commenting is currently disabled on this site"
NOT_FOUND: "Resource not found"
ALREADY_EXISTS: "Resource already exists"
INVALID_ASSET_URL: "Assert URL is invalid"
+8
View File
@@ -68,6 +68,14 @@ const Setting = new Schema(
type: String,
default: 'Expired',
},
disableCommenting: {
type: Boolean,
default: false,
},
disableCommentingMessage: {
type: String,
default: '',
},
wordlist: {
banned: {
type: Array,
+10
View File
@@ -339,6 +339,11 @@ User.virtual('banned')
})
.set(function(status) {
this.status.banned.status = status;
if (!this.status.banned.history) {
this.status.banned.history = [];
}
this.status.banned.history.push({
status,
created_at: new Date(),
@@ -357,6 +362,11 @@ User.virtual('suspended')
})
.set(function(until) {
this.status.suspension.until = until;
if (!this.status.suspension.history) {
this.status.suspension.history = [];
}
this.status.suspension.history.push({
until,
created_at: new Date(),
+2 -2
View File
@@ -149,7 +149,7 @@
"metascraper-title": "^3.9.2",
"minimist": "^1.2.0",
"moment": "^2.18.1",
"mongoose": "^4.12.3",
"mongoose": "^5.1.1",
"ms": "^2.0.0",
"murmurhash-js": "^1.0.0",
"name-all-modules-plugin": "^1.0.1",
@@ -231,7 +231,7 @@
"husky": "^0.14.3",
"identity-obj-proxy": "^3.0.0",
"jest-junit": "^3.6.0",
"lint-staged": "^7.0.0",
"lint-staged": "^7.1.0",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
"nightwatch": "^0.9.16",
+1
View File
@@ -9,4 +9,5 @@ export {
getDefinitionName,
getShallowChanges,
createDefaultResponseFragments,
handlePopupAuth,
} from 'coral-framework/utils';
@@ -1,3 +1,5 @@
import { handlePopupAuth } from 'plugin-api/beta/client/utils';
export const loginWithFacebook = () => (dispatch, _, { rest }) => {
window.location = `${rest.uri}/auth/facebook`;
handlePopupAuth(`${rest.uri}/auth/facebook`);
};
@@ -5,6 +5,7 @@ import translations from './translations.yml';
export default {
translations,
slots: {
authExternalAdminSignIn: [SignIn],
authExternalSignIn: [SignIn],
authExternalSignUp: [SignUp],
},
@@ -1,3 +1,5 @@
import { handlePopupAuth } from 'plugin-api/beta/client/utils';
export const loginWithGoogle = () => (dispatch, _, { rest }) => {
window.location = `${rest.uri}/auth/google`;
handlePopupAuth(`${rest.uri}/auth/google`);
};
@@ -5,6 +5,7 @@ import translations from './translations.yml';
export default {
translations,
slots: {
authExternalAdminSignIn: [SignIn],
authExternalSignIn: [SignIn],
authExternalSignUp: [SignUp],
},
@@ -223,11 +223,21 @@ class Profile extends React.Component {
disabled={!usernameCanBeUpdated}
columnDisplay
>
<span className={styles.bottomText}>
{t(
'talk-plugin-local-auth.change_username.change_username_note'
<div className={styles.bottomText}>
<span>
{t(
'talk-plugin-local-auth.change_username.change_username_note'
)}
</span>
{!usernameCanBeUpdated && (
<b>
{' '}
{t(
'talk-plugin-local-auth.change_username.is_not_eligible'
)}
</b>
)}
</span>
</div>
</InputField>
<InputField
icon="email"
@@ -19,7 +19,8 @@ en:
changed_password_msg: "Changed Password - Your password has been successfully changed"
forgot_password_sent: "Forgot Password - We sent you an email to recover your password"
change_username:
change_username_note: "Usernames can only be changed once every 14 days. Your username is not currently eligible to be updated."
change_username_note: "Usernames can only be changed once every 14 days."
is_not_eligible: "You cannot currently change your username."
save: "Save"
edit_profile: "Edit Profile"
cancel: "Cancel"
@@ -151,7 +152,8 @@ es:
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
forgot_password_sent: "Contraseña Olvidada - Te enviamos un email para recuperar tu contraseña"
change_username:
change_username_note: "El usuario puede ser cambiado cada 14 días"
change_username_note: "El usuario puede ser cambiado cada 14 días."
is_not_eligible: "Ahora mismo no se puede cambiar su nombre de usuario."
save: "Guardar"
edit_profile: "Editar Perfil"
cancel: "Cancelar"
@@ -63,17 +63,18 @@ class DeleteMyAccount extends React.Component {
<p className="talk-plugin-auth--delete-my-account-description">
{t('delete_request.delete_my_account_description')}
</p>
<p className="talk-plugin-auth--delete-my-account-description">
{scheduledDeletionDate &&
t(
'delete_request.already_submitted_request_description',
moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
)}
</p>
{scheduledDeletionDate ? (
<Button onClick={this.cancelAccountDeletion}>
{t('delete_request.cancel_account_deletion_request')}
</Button>
<div>
<p className="talk-plugin-auth--delete-my-account-description">
{t(
'delete_request.already_submitted_request_description',
moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
)}
</p>
<Button onClick={this.cancelAccountDeletion}>
{t('delete_request.cancel_account_deletion_request')}
</Button>
</div>
) : (
<Button icon="delete" onClick={this.showDialog}>
{t('delete_request.delete_my_account')}
@@ -7,21 +7,17 @@ class Button extends React.Component {
render() {
const {
className,
title,
onClick,
children,
active,
activeClassName,
disabled,
...rest
} = this.props;
return (
<button
className={cn(className, styles.button, {
[cn(styles.active, activeClassName)]: active,
})}
title={title}
onClick={onClick}
disabled={disabled}
{...rest}
>
{children}
</button>
@@ -32,11 +28,8 @@ class Button extends React.Component {
Button.propTypes = {
className: PropTypes.string,
activeClassName: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.node,
active: PropTypes.bool,
disabled: PropTypes.bool,
};
export default Button;
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../components/Button';
import bowser from 'bowser';
/**
* createToggle creates a button that can be active, inactive or disabled
@@ -32,7 +33,17 @@ const createToggle = (
this.execCommand();
};
// Detect whether there was focus on the RTE before the click.
hadFocusBeforeClick = false;
handleMouseDown = () => (this.hadFocusBeforeClick = this.props.api.focused);
handleClick = () => {
// Skip IOS when the focus was not there before.
// IOS fails to focus to the RTE correctly and scrolls to nirvana.
// See https://www.pivotaltracker.com/story/show/157607216
if (!this.hadFocusBeforeClick && bowser.ios) {
return;
}
this.props.api.focus();
this.formatToggle();
this.props.api.focus();
@@ -62,6 +73,7 @@ const createToggle = (
<Button
className={className}
title={title}
onMouseDown={this.handleMouseDown}
onClick={this.handleClick}
active={this.state.active}
disabled={disabled || this.state.disabled}
@@ -381,9 +381,13 @@ export function isBogusBR(node) {
}
/**
* Returns an array of all nodes after `node`.
* Returns an array of all nodes after `node` in a _line_.
*/
export function getRightOfNode(node) {
if (node.tagName === 'BR') {
return [];
}
let result = [];
let cur = node;
while (
@@ -515,10 +519,10 @@ export function outdentBlock(node, changeSelection) {
// A new lines to substitute the missing block element.
const needLineAfter =
node.nextSibling &&
!isBlockElement(node.nextSibling) &&
node.lastChild &&
!isBlockElement(node.lastChild);
!node.lastChild ||
(node.nextSibling &&
!isBlockElement(node.nextSibling) &&
!isBlockElement(node.lastChild));
const needLineBefore =
node.previousSibling &&
!isBlockElement(node.previousSibling) &&
+22 -27
View File
@@ -2,7 +2,7 @@ const ActionModel = require('../models/action');
const CommentModel = require('../models/comment');
const UserModel = require('../models/user');
const _ = require('lodash');
const errors = require('../errors');
const { ErrAlreadyExists } = require('../errors');
const incrActionCounts = async (action, value) => {
const ACTION_TYPE = action.action_type.toLowerCase();
@@ -41,35 +41,30 @@ const incrActionCounts = async (action, value) => {
* @param {object} update the update operation for the mongo findOneAndUpdate op
* @param {object} options the options operation for the mongo findOneAndUpdate op
*/
const findOnlyOneAndUpdate = async (query, update, options = {}) =>
new Promise((resolve, reject) => {
ActionModel.findOneAndUpdate(
query,
update,
Object.assign({}, options, {
// Use raw result to get `updatedExisting`.
passRawResult: true,
const findOnlyOneAndUpdate = async (query, update, options = {}) => {
const raw = await ActionModel.findOneAndUpdate(
query,
update,
Object.assign({}, options, {
// Use raw result to get `updatedExisting`.
rawResult: true,
// Ensure that if it's new, we return the new object created.
new: true,
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true,
}),
(err, doc, raw) => {
if (err) {
return reject(err);
}
if (raw.lastErrorObject.updatedExisting) {
return reject(new errors.ErrAlreadyExists(raw.value));
}
return resolve(raw.value);
}
);
});
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true,
})
);
if (raw.lastErrorObject.updatedExisting) {
throw new ErrAlreadyExists(raw.value);
}
return raw.value;
};
module.exports = class ActionsService {
/**
+2
View File
@@ -6,6 +6,7 @@ const {
wordlist,
commentLength,
assetClosed,
commentingDisabled,
karma,
staff,
links,
@@ -36,6 +37,7 @@ const applyStatus = status => () => ({ status });
const phases = [
commentLength,
assetClosed,
commentingDisabled,
wordlist,
staff,
links,
@@ -0,0 +1,9 @@
const { ErrCommentingDisabled } = require('../../../errors');
// This phase checks to see if commenting is site-wide disabled.
module.exports = (ctx, comment, { asset }) => {
// Check to see if the asset has closed commenting...
if (asset.settings.disableCommenting) {
throw new ErrCommentingDisabled(asset.settings.disableCommentingMessage);
}
};
+1
View File
@@ -1,6 +1,7 @@
module.exports.wordlist = require('./wordlist');
module.exports.commentLength = require('./commentLength');
module.exports.assetClosed = require('./assetClosed');
module.exports.commentingDisabled = require('./commentingDisabled');
module.exports.karma = require('./karma');
module.exports.staff = require('./staff');
module.exports.links = require('./links');
-1
View File
@@ -45,7 +45,6 @@ if (WEBPACK) {
// Connect to the Mongo instance.
mongoose
.connect(MONGO_URL, {
useMongoClient: true,
config: {
autoIndex: CREATE_MONGO_INDEXES,
},
@@ -179,6 +179,54 @@ describe('graph.mutations.createComment', () => {
});
});
describe('assets while commenting is disabled', () => {
[
{
disabled: false,
error: null,
},
{
disabled: true,
error: 'COMMENTING_DISABLED',
},
].forEach(({ disabled, error }) => {
describe(`commentingDisabled=${disabled}`, () => {
beforeEach(() =>
AssetModel.create({
id: '123',
settings: { disableCommenting: disabled },
})
);
it(
error ? 'does not create the comment' : 'creates the comment',
() => {
const context = new Context({ user: new UserModel({}) });
return graphql(schema, query, {}, context).then(
({ data, errors }) => {
expect(errors).to.be.undefined;
if (error) {
expect(data.createComment).to.have.property('comment').null;
expect(data.createComment).to.have.property('errors').not
.null;
expect(data.createComment.errors[0]).to.have.property(
'translation_key',
error
);
} else {
expect(data.createComment).to.have.property('comment').not
.null;
expect(data.createComment).to.have.property('errors').null;
}
}
);
}
);
});
});
});
describe('comments made with different asset moderation settings', () => {
[
{ moderation: 'PRE', status: 'PREMOD' },
+59 -75
View File
@@ -1644,6 +1644,10 @@ bson@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c"
bson@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.6.tgz#444db59ddd4c24f0cb063aabdc5c8c7b0ceca912"
buffer-crc32@^0.2.1, buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -1652,10 +1656,6 @@ buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
buffer-shims@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
buffer-xor@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@@ -3353,10 +3353,6 @@ es6-map@^0.1.3:
es6-symbol "~3.1.1"
event-emitter "~0.3.5"
es6-promise@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4"
es6-promise@^4.0.5:
version "4.1.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a"
@@ -4890,10 +4886,6 @@ home-or-tmp@^2.0.0:
os-homedir "^1.0.0"
os-tmpdir "^1.0.1"
hooks-fixed@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.2.tgz#20076daa07e77d8a6106883ce3f1722e051140b0"
hosted-git-info@^2.1.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
@@ -6399,9 +6391,9 @@ jxLoader@*:
promised-io "*"
walker "1.x"
kareem@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448"
kareem@2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.0.7.tgz#8d260366a4df4236ceccec318fcf10c17c5beb22"
keymaster@^1.6.2:
version "1.6.2"
@@ -6528,9 +6520,9 @@ linkifyjs@^2.1.5:
react ">=0.14.0"
react-dom ">=0.14.0"
lint-staged@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.0.0.tgz#57926c63201e7bd38ca0576d74391efa699b4a9d"
lint-staged@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.1.0.tgz#1514a5b71b8d9492ca0c3d2a44769cbcbc8bcc79"
dependencies:
app-root-path "^2.0.1"
chalk "^2.3.1"
@@ -6541,6 +6533,7 @@ lint-staged@^7.0.0:
execa "^0.9.0"
find-parent-dir "^0.3.0"
is-glob "^4.0.0"
is-windows "^1.0.2"
jest-validate "^22.4.0"
listr "^0.13.0"
lodash "^4.17.5"
@@ -6550,8 +6543,9 @@ lint-staged@^7.0.0:
p-map "^1.1.1"
path-is-inside "^1.0.2"
pify "^3.0.0"
please-upgrade-node "^3.0.1"
staged-git-files "1.1.0"
please-upgrade-node "^3.0.2"
staged-git-files "1.1.1"
string-argv "^0.0.2"
stringify-object "^3.2.2"
listr-silent-renderer@^1.1.1:
@@ -7451,36 +7445,36 @@ moment@^2.15.2:
version "2.22.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
mongodb-core@2.1.17:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.17.tgz#a418b337a14a14990fb510b923dee6a813173df8"
mongodb-core@3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.0.8.tgz#8d401f4eab6056c0d874a3d5844a4844f761d4d7"
dependencies:
bson "~1.0.4"
require_optional "~1.0.0"
require_optional "^1.0.1"
mongodb@2.2.33:
version "2.2.33"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.33.tgz#b537c471d34a6651b48f36fdbf29750340e08b50"
mongodb@3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.0.8.tgz#2c1daecac9a0ec2de2f2aea4dc97d76ae70f8951"
dependencies:
es6-promise "3.2.1"
mongodb-core "2.1.17"
readable-stream "2.2.7"
mongodb-core "3.0.8"
mongoose@^4.12.3:
version "4.13.7"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.13.7.tgz#f760c770e6c8cdf34a6fe8b7443882b5fced1032"
mongoose-legacy-pluralize@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4"
mongoose@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.1.1.tgz#a7e925607e76032e5ef20b3035a357bc8581b45e"
dependencies:
async "2.1.4"
bson "~1.0.4"
hooks-fixed "2.0.2"
kareem "1.5.0"
bson "~1.0.5"
kareem "2.0.7"
lodash.get "4.4.2"
mongodb "2.2.33"
mpath "0.3.0"
mpromise "0.5.5"
mquery "2.3.3"
mongodb "3.0.8"
mongoose-legacy-pluralize "1.0.2"
mpath "0.4.1"
mquery "3.0.0"
ms "2.0.0"
muri "1.3.0"
regexp-clone "0.0.1"
sliced "1.0.1"
@@ -7509,17 +7503,13 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mpath@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.3.0.tgz#7a58f789e9b5fd3c94520634157960f26bd5ef44"
mpath@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.4.1.tgz#ed10388430380bf7bbb5be1391e5d6969cb08e89"
mpromise@0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6"
mquery@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.3.3.tgz#221412e5d4e7290ca5582dd16ea8f190a506b518"
mquery@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.0.0.tgz#e5f387dbabc0b9b69859e550e810faabe0ceabb0"
dependencies:
bluebird "3.5.0"
debug "2.6.9"
@@ -7538,10 +7528,6 @@ ms@^2.0.0, ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
muri@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/muri/-/muri-1.3.0.tgz#aeccf3db64c56aa7c5b34e00f95b7878527a4721"
murmurhash-js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
@@ -8556,9 +8542,11 @@ platform@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.4.tgz#6f0fb17edaaa48f21442b3a975c063130f1c3ebd"
please-upgrade-node@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.0.1.tgz#0a681f2c18915e5433a5ca2cd94e0b8206a782db"
please-upgrade-node@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.0.2.tgz#7b9eaeca35aa4a43d6ebdfd10616c042f9a83acc"
dependencies:
semver-compare "^1.0.0"
pluralize@^1.2.1:
version "1.2.1"
@@ -9717,18 +9705,6 @@ readable-stream@2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stre
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
readable-stream@2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1"
dependencies:
buffer-shims "~1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
string_decoder "~1.0.0"
util-deprecate "~1.0.1"
readdirp@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
@@ -10034,7 +10010,7 @@ require-uncached@^1.0.3:
caller-path "^0.1.0"
resolve-from "^1.0.0"
require_optional@~1.0.0:
require_optional@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e"
dependencies:
@@ -10285,6 +10261,10 @@ selenium-standalone@^6.11.0:
which "^1.2.12"
yauzl "^2.5.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -10762,9 +10742,9 @@ stack-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
staged-git-files@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.0.tgz#1a9bb131c1885601023c7aaddd3d54c22142c526"
staged-git-files@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b"
static-extend@^0.1.1:
version "0.1.2"
@@ -10841,6 +10821,10 @@ strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
string-argv@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736"
string-hash@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
@@ -10875,7 +10859,7 @@ string.prototype.padend@^3.0.0:
es-abstract "^1.4.3"
function-bind "^1.0.2"
string_decoder@^1.0.0, string_decoder@~1.0.0, string_decoder@~1.0.3:
string_decoder@^1.0.0, string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies: