FEATURE: add penalty options for take action (#10926)
* FEATURE: add penalty options for take action Add the ability to silence or suspend users from the "take action" button when moderators are flagging posts. This allows for a more streamlined active moderation workflow, when moderating against a topic directly.
This commit is contained in:
parent
e062b94e7f
commit
d68ad82a9e
|
@ -7,6 +7,9 @@ import ActionSummary from "discourse/models/action-summary";
|
||||||
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
|
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
|
||||||
import optionalService from "discourse/lib/optional-service";
|
import optionalService from "discourse/lib/optional-service";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import User from "discourse/models/user";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
export default Controller.extend(ModalFunctionality, {
|
||||||
adminTools: optionalService(),
|
adminTools: optionalService(),
|
||||||
|
@ -17,6 +20,58 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
isWarning: false,
|
isWarning: false,
|
||||||
topicActionByName: null,
|
topicActionByName: null,
|
||||||
spammerDetails: null,
|
spammerDetails: null,
|
||||||
|
flagActions: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.flagActions = {
|
||||||
|
icon: "gavel",
|
||||||
|
label: I18n.t("flagging.take_action"),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: "agree_and_keep",
|
||||||
|
icon: "thumbs-up",
|
||||||
|
label: I18n.t("flagging.take_action_options.default.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.default.details"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agree_and_suspend",
|
||||||
|
icon: "ban",
|
||||||
|
label: I18n.t("flagging.take_action_options.suspend.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.suspend.details"),
|
||||||
|
client_action: "suspend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agree_and_silence",
|
||||||
|
icon: "microphone-slash",
|
||||||
|
label: I18n.t("flagging.take_action_options.silence.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.silence.details"),
|
||||||
|
client_action: "silence",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clientSuspend(performAction) {
|
||||||
|
this._penalize("showSuspendModal", performAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
clientSilence(performAction) {
|
||||||
|
this._penalize("showSilenceModal", performAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _penalize(adminToolMethod, performAction) {
|
||||||
|
if (this.adminTools) {
|
||||||
|
let createdBy = await User.findByUsername(this.model.username);
|
||||||
|
let postId = this.model.id;
|
||||||
|
let postEdit = this.model.cooked;
|
||||||
|
return this.adminTools[adminToolMethod](createdBy, {
|
||||||
|
postId,
|
||||||
|
postEdit,
|
||||||
|
before: performAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
|
@ -24,9 +79,8 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
spammerDetails: null,
|
spammerDetails: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
let adminTools = this.adminTools;
|
if (this.adminTools) {
|
||||||
if (adminTools) {
|
this.adminTools.checkSpammer(this.get("model.user_id")).then((result) => {
|
||||||
adminTools.checkSpammer(this.get("model.user_id")).then((result) => {
|
|
||||||
this.set("spammerDetails", result);
|
this.set("spammerDetails", result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -133,9 +187,28 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
takeAction() {
|
takeAction(action) {
|
||||||
this.send("createFlag", { takeAction: true });
|
let performAction = (o = {}) => {
|
||||||
this.set("model.hidden", true);
|
o.takeAction = true;
|
||||||
|
this.send("createFlag", o);
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action.client_action) {
|
||||||
|
let actionMethod = this[`client${action.client_action.classify()}`];
|
||||||
|
if (actionMethod) {
|
||||||
|
return actionMethod.call(this, () =>
|
||||||
|
performAction({ skipClose: true })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`No handler for ${action.client_action} found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.set("model.hidden", true);
|
||||||
|
return performAction();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createFlag(opts) {
|
createFlag(opts) {
|
||||||
|
@ -171,7 +244,9 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
postAction
|
postAction
|
||||||
.act(this.model, params)
|
.act(this.model, params)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.send("closeModal");
|
if (!opts.skipClose) {
|
||||||
|
this.send("closeModal");
|
||||||
|
}
|
||||||
if (params.message) {
|
if (params.message) {
|
||||||
this.set("message", "");
|
this.set("message", "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,13 +35,10 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canTakeAction}}
|
{{#if canTakeAction}}
|
||||||
{{d-button
|
{{reviewable-bundled-action
|
||||||
class="btn-danger"
|
bundle=flagActions
|
||||||
action=(action "takeAction")
|
performAction=(action "takeAction")
|
||||||
disabled=submitDisabled
|
reviewableUpdating=submitDisabled
|
||||||
title="flagging.take_action_tooltip"
|
|
||||||
icon="gavel"
|
|
||||||
label="flagging.take_action"
|
|
||||||
}}
|
}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { test } from "qunit";
|
||||||
|
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||||
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import userFixtures from "discourse/tests/fixtures/user-fixtures";
|
||||||
|
|
||||||
|
acceptance("flagging", {
|
||||||
|
loggedIn: true,
|
||||||
|
afterEach() {
|
||||||
|
sandbox.restore();
|
||||||
|
},
|
||||||
|
pretend(pretenderServer, helper) {
|
||||||
|
const userResponse = Object.assign({}, userFixtures["/u/charlie.json"]);
|
||||||
|
pretenderServer.get("/u/uwe_keim.json", () => {
|
||||||
|
return helper.response(userResponse);
|
||||||
|
});
|
||||||
|
pretenderServer.get("/admin/users/255.json", () => {
|
||||||
|
return helper.response({
|
||||||
|
id: 255,
|
||||||
|
automatic: false,
|
||||||
|
name: "admin",
|
||||||
|
username: "admin",
|
||||||
|
user_count: 0,
|
||||||
|
alias_level: 99,
|
||||||
|
visible: true,
|
||||||
|
automatic_membership_email_domains: "",
|
||||||
|
primary_group: false,
|
||||||
|
title: null,
|
||||||
|
grant_trust_level: null,
|
||||||
|
has_messages: false,
|
||||||
|
flair_url: null,
|
||||||
|
flair_bg_color: null,
|
||||||
|
flair_color: null,
|
||||||
|
bio_raw: null,
|
||||||
|
bio_cooked: null,
|
||||||
|
public_admission: false,
|
||||||
|
allow_membership_requests: true,
|
||||||
|
membership_request_template: "Please add me",
|
||||||
|
full_name: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pretenderServer.get("/admin/users/5.json", () => {
|
||||||
|
return helper.response({
|
||||||
|
id: 5,
|
||||||
|
automatic: false,
|
||||||
|
name: "user",
|
||||||
|
username: "user",
|
||||||
|
user_count: 0,
|
||||||
|
alias_level: 99,
|
||||||
|
visible: true,
|
||||||
|
automatic_membership_email_domains: "",
|
||||||
|
primary_group: false,
|
||||||
|
title: null,
|
||||||
|
grant_trust_level: null,
|
||||||
|
has_messages: false,
|
||||||
|
flair_url: null,
|
||||||
|
flair_bg_color: null,
|
||||||
|
flair_color: null,
|
||||||
|
bio_raw: null,
|
||||||
|
bio_cooked: null,
|
||||||
|
public_admission: false,
|
||||||
|
allow_membership_requests: true,
|
||||||
|
membership_request_template: "Please add me",
|
||||||
|
full_name: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pretenderServer.put("admin/users/5/silence", () => {
|
||||||
|
return helper.response({
|
||||||
|
silenced: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pretenderServer.post("post_actions", () => {
|
||||||
|
return helper.response({
|
||||||
|
response: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openFlagModal() {
|
||||||
|
if (exists(".topic-post:first-child button.show-more-actions")) {
|
||||||
|
await click(".topic-post:first-child button.show-more-actions");
|
||||||
|
}
|
||||||
|
|
||||||
|
await click(".topic-post:first-child button.create-flag");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Flag modal opening", async (assert) => {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
await openFlagModal();
|
||||||
|
assert.ok(exists(".flag-modal-body"), "it shows the flag modal");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Flag take action dropdown exists", async (assert) => {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
await openFlagModal();
|
||||||
|
await click("#radio_inappropriate");
|
||||||
|
await selectKit(".reviewable-action-dropdown").expand();
|
||||||
|
assert.ok(
|
||||||
|
exists("[data-value='agree_and_silence']"),
|
||||||
|
"it shows the silence action option"
|
||||||
|
);
|
||||||
|
await click("[data-value='agree_and_silence']");
|
||||||
|
assert.ok(exists(".silence-user-modal"), "it shows the silence modal");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can silence from take action", async (assert) => {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
await openFlagModal();
|
||||||
|
await click("#radio_inappropriate");
|
||||||
|
await selectKit(".reviewable-action-dropdown").expand();
|
||||||
|
await click("[data-value='agree_and_silence']");
|
||||||
|
|
||||||
|
const silenceUntilCombobox = selectKit(".silence-until .combobox");
|
||||||
|
await silenceUntilCombobox.expand();
|
||||||
|
await silenceUntilCombobox.selectRowByValue("tomorrow");
|
||||||
|
await fillIn(".silence-reason", "for breaking the rules");
|
||||||
|
await click(".perform-silence");
|
||||||
|
assert.equal(find(".bootbox.modal:visible").length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Gets dismissable warning from canceling incomplete silence from take action", async (assert) => {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
await openFlagModal();
|
||||||
|
await click("#radio_inappropriate");
|
||||||
|
await selectKit(".reviewable-action-dropdown").expand();
|
||||||
|
await click("[data-value='agree_and_silence']");
|
||||||
|
|
||||||
|
const silenceUntilCombobox = selectKit(".silence-until .combobox");
|
||||||
|
await silenceUntilCombobox.expand();
|
||||||
|
await silenceUntilCombobox.selectRowByValue("tomorrow");
|
||||||
|
await fillIn(".silence-reason", "for breaking the rules");
|
||||||
|
await click(".d-modal-cancel");
|
||||||
|
assert.equal(find(".bootbox.modal:visible").length, 1);
|
||||||
|
|
||||||
|
await click(".modal-footer .btn-default");
|
||||||
|
assert.equal(find(".bootbox.modal:visible").length, 0);
|
||||||
|
assert.ok(exists(".silence-user-modal"), "it shows the silence modal");
|
||||||
|
|
||||||
|
await click(".d-modal-cancel");
|
||||||
|
assert.equal(find(".bootbox.modal:visible").length, 1);
|
||||||
|
|
||||||
|
await click(".modal-footer .btn-primary");
|
||||||
|
assert.equal(find(".bootbox.modal:visible").length, 0);
|
||||||
|
});
|
|
@ -490,6 +490,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flag-modal .modal-inner-container .select-kit.reviewable-action-dropdown {
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1000px) {
|
@media screen and (max-width: 1000px) {
|
||||||
table.reviewable-scores {
|
table.reviewable-scores {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -2997,7 +2997,17 @@ en:
|
||||||
flagging:
|
flagging:
|
||||||
title: "Thanks for helping to keep our community civil!"
|
title: "Thanks for helping to keep our community civil!"
|
||||||
action: "Flag Post"
|
action: "Flag Post"
|
||||||
take_action: "Take Action"
|
take_action: "Take Action..."
|
||||||
|
take_action_options:
|
||||||
|
default:
|
||||||
|
title: "Take Action"
|
||||||
|
details: "Reach the flag threshold immediately, rather than waiting for more community flags"
|
||||||
|
suspend:
|
||||||
|
title: "Suspend User"
|
||||||
|
details: "Reach the flag threshold, and suspend the user"
|
||||||
|
silence:
|
||||||
|
title: "Silence User"
|
||||||
|
details: "Reach the flag threshold, and silence the user"
|
||||||
notify_action: "Message"
|
notify_action: "Message"
|
||||||
official_warning: "Official Warning"
|
official_warning: "Official Warning"
|
||||||
delete_spammer: "Delete Spammer"
|
delete_spammer: "Delete Spammer"
|
||||||
|
|
Loading…
Reference in New Issue