[CORL-276] Sitewide Pre-Moderation (#2392)

* feat: initial implementation

* chore: docs update

* fix: lint

* fix: naming
This commit is contained in:
Wyatt Johnson
2019-07-05 23:18:58 +00:00
committed by GitHub
parent e7745a85aa
commit 9a191b44ba
10 changed files with 374 additions and 5 deletions
+1 -1
View File
@@ -299,7 +299,7 @@ To test out the email sending functionality, you can run [inbucket](https://www.
which provides a test SMTP server that can visualize emails in the browser:
```bash
docker run -d --name inbucket -p 2500:2500 -p 9000:9000 inbucket/inbucket
docker run -d --name inbucket --restart always -p 2500:2500 -p 9000:9000 inbucket/inbucket
```
You can then configure the email server on Coral by updating the Tenant with:
@@ -13,6 +13,8 @@ interface Props {
invert?: boolean;
onLabel?: React.ReactNode;
offLabel?: React.ReactNode;
format?: ((value: any, name: string) => any) | null;
parse?: ((value: any, name: string) => any) | null;
}
const OnOffField: FunctionComponent<Props> = ({
@@ -21,9 +23,17 @@ const OnOffField: FunctionComponent<Props> = ({
onLabel,
offLabel,
invert = false,
parse = parseStringBool,
format,
}) => (
<div>
<Field name={name} type="radio" parse={parseStringBool} value={!invert}>
<Field
name={name}
type="radio"
value={!invert}
parse={parse}
format={format}
>
{({ input }) => (
<RadioButton
id={`${input.name}-true`}
@@ -43,7 +53,13 @@ const OnOffField: FunctionComponent<Props> = ({
</RadioButton>
)}
</Field>
<Field name={name} type="radio" parse={parseStringBool} value={invert}>
<Field
name={name}
type="radio"
parse={parse}
format={format}
value={invert}
>
{({ input }) => (
<RadioButton
id={`${input.name}-false`}
@@ -5,11 +5,13 @@ import { HorizontalGutter } from "coral-ui/components";
import AkismetConfigContainer from "./AkismetConfigContainer";
import PerspectiveConfigContainer from "./PerspectiveConfigContainer";
import PreModerationConfigContainer from "./PreModerationConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof AkismetConfigContainer>["settings"] &
PropTypesOf<typeof PerspectiveConfigContainer>["settings"];
PropTypesOf<typeof PerspectiveConfigContainer>["settings"] &
PropTypesOf<typeof PreModerationConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
@@ -19,6 +21,11 @@ const ModerationConfig: FunctionComponent<Props> = ({
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-moderationContainer">
<PreModerationConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<PerspectiveConfigContainer
disabled={disabled}
settings={settings}
@@ -47,6 +47,7 @@ const enhanced = withFragmentContainer<Props>({
fragment ModerationConfigContainer_settings on Settings {
...AkismetConfigContainer_settings
...PerspectiveConfigContainer_settings
...PreModerationConfigContainer_settings
}
`,
})(ModerationConfigContainer);
@@ -0,0 +1,65 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { parseStringBool } from "coral-framework/lib/form";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
Typography,
} from "coral-ui/components";
import Header from "../../Header";
import OnOffField from "../../OnOffField";
interface Props {
disabled: boolean;
}
const parse = (v: string) => {
return parseStringBool(v) ? "PRE" : "POST";
};
const format = (v: "PRE" | "POST") => {
return v === "PRE";
};
const PreModerationConfig: FunctionComponent<Props> = ({ disabled }) => {
return (
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-moderation-preModeration-title">
<Header container="legend">Pre-moderation</Header>
</Localized>
<Localized id="configure-moderation-preModeration-explanation">
<Typography variant="detail">
When pre-moderation is turned on, comments will not be published
unless approved by a moderator.
</Typography>
</Localized>
<FormField container={<FieldSet />}>
<Localized id="configure-moderation-preModeration-moderation">
<InputLabel container="legend">
Pre-moderate all comments sitewide
</InputLabel>
</Localized>
<OnOffField
name="moderation"
disabled={disabled}
parse={parse}
format={format}
/>
</FormField>
<FormField container={<FieldSet />}>
<Localized id="configure-moderation-preModeration-premodLinksEnable">
<InputLabel container="legend">
Pre-moderate comments containing links sitewide
</InputLabel>
</Localized>
<OnOffField name="premodLinksEnable" disabled={disabled} />
</FormField>
</HorizontalGutter>
);
};
export default PreModerationConfig;
@@ -0,0 +1,36 @@
import React from "react";
import { graphql } from "react-relay";
import { PreModerationConfigContainer_settings as SettingsData } from "coral-admin/__generated__/PreModerationConfigContainer_settings.graphql";
import { withFragmentContainer } from "coral-framework/lib/relay";
import PreModerationConfig from "./PreModerationConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class PreModerationConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <PreModerationConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment PreModerationConfigContainer_settings on Settings {
moderation
premodLinksEnable
}
`,
})(PreModerationConfigContainer);
export default enhanced;
@@ -5,6 +5,11 @@ exports[`renders correctly 1`] = `
data-testid="configure-moderationContainer"
size="double"
>
<Relay(PreModerationConfigContainer)
disabled={false}
onInitValues={[Function]}
settings={Object {}}
/>
<Relay(PerspectiveConfigContainer)
disabled={false}
onInitValues={[Function]}
@@ -107,6 +107,139 @@ exports[`renders configure moderation 1`] = `
className="Box-root HorizontalGutter-root HorizontalGutter-double"
data-testid="configure-moderationContainer"
>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
>
Pre-moderation
</legend>
<p
className="Box-root Typography-root Typography-detail Typography-colorTextPrimary"
>
When pre-moderation is turned on, comments will not be published unless
approved by a moderator.
</p>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Pre-moderate all comments sitewide
</legend>
<div>
<div
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
>
<input
checked={false}
className="RadioButton-input"
disabled={false}
id="moderation-true"
name="moderation"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="radio"
value={true}
/>
<label
className="RadioButton-label"
htmlFor="moderation-true"
>
<span>
On
</span>
</label>
</div>
<div
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
>
<input
checked={true}
className="RadioButton-input"
disabled={false}
id="moderation-false"
name="moderation"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="radio"
value={false}
/>
<label
className="RadioButton-label"
htmlFor="moderation-false"
>
<span>
Off
</span>
</label>
</div>
</div>
</fieldset>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
>
Pre-moderate comments containing links sitewide
</legend>
<div>
<div
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
>
<input
checked={false}
className="RadioButton-input"
disabled={false}
id="premodLinksEnable-true"
name="premodLinksEnable"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="radio"
value={true}
/>
<label
className="RadioButton-label"
htmlFor="premodLinksEnable-true"
>
<span>
On
</span>
</label>
</div>
<div
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
>
<input
checked={true}
className="RadioButton-input"
disabled={false}
id="premodLinksEnable-false"
name="premodLinksEnable"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="radio"
value={false}
/>
<label
className="RadioButton-label"
htmlFor="premodLinksEnable-false"
>
<span>
Off
</span>
</label>
</div>
</div>
</fieldset>
</fieldset>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
@@ -1,5 +1,5 @@
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import { GQLMODERATION_MODE, GQLResolver } from "coral-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
@@ -60,6 +60,102 @@ it("renders configure moderation", async () => {
expect(within(configureContainer).toJSON()).toMatchSnapshot();
});
it("change site wide pre-moderation", async () => {
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.moderation).toEqual(
GQLMODERATION_MODE.PRE
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
moderationContainer,
saveChangesButton,
} = await createTestRenderer({
resolvers,
});
const preModerationContainer = within(moderationContainer).getAllByText(
"Pre-moderate all comments sitewide",
{ selector: "fieldset" }
)[0];
const onField = within(preModerationContainer).getByLabelText("On");
// Let's enable it.
onField.props.onChange(onField.props.value.toString());
// Send form
within(configureContainer)
.getByType("form")
.props.onSubmit();
// Submit button and text field should be disabled.
expect(saveChangesButton.props.disabled).toBe(true);
expect(onField.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(onField.props.disabled).toBe(false);
});
// Should have successfully sent with server.
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change site wide link pre-moderation", async () => {
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.premodLinksEnable).toEqual(true);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
moderationContainer,
saveChangesButton,
} = await createTestRenderer({
resolvers,
});
const preModerationContainer = within(moderationContainer).getAllByText(
"Pre-moderate comments containing links sitewide",
{ selector: "fieldset" }
)[0];
const onField = within(preModerationContainer).getByLabelText("On");
// Let's enable it.
onField.props.onChange(onField.props.value.toString());
// Send form
within(configureContainer)
.getByType("form")
.props.onSubmit();
// Submit button and text field should be disabled.
expect(saveChangesButton.props.disabled).toBe(true);
expect(onField.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(onField.props.disabled).toBe(false);
});
// Should have successfully sent with server.
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change akismet settings", async () => {
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
+10
View File
@@ -215,6 +215,16 @@ configure-auth-oidc-jwksURI = JWKS URI
configure-auth-oidc-useLoginOn = Use OpenID Connect login on
### Moderation
#### Pre-Moderation
configure-moderation-preModeration-title = Pre-moderation
configure-moderation-preModeration-explanation =
When pre-moderation is turned on, comments will not be published unless
approved by a moderator.
configure-moderation-preModeration-moderation =
Pre-moderate all comments sitewide
configure-moderation-preModeration-premodLinksEnable =
Pre-moderate comments containing links sitewide
configure-moderation-apiKey = API Key
configure-moderation-akismet-title = Akismet Spam Detection Filter