mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 15:25:35 +08:00
FEATURE: Add Revise... option for queued post reviewable (#23454)
This commit adds a new Revise... action that can be taken for queued post reviewables. This will open a modal where the user can select a Reason from a preconfigured list (or by choosing Other..., a custom reason) and provide feedback to the user about their post. The post will be rejected still, but a PM will also be sent to the user so they have an opportunity to improve their post when they resubmit it.
This commit is contained in:
parent
5fe4e0ed48
commit
9762e65758
|
@ -0,0 +1,67 @@
|
|||
<DModal
|
||||
class="revise-and-reject-reviewable"
|
||||
@closeModal={{@closeModal}}
|
||||
@title={{i18n "review.revise_and_reject_post.title"}}
|
||||
>
|
||||
<:body>
|
||||
<div class="revise-and-reject-reviewable__queued-post">
|
||||
<ReviewableQueuedPost @reviewable={{@model.reviewable}} @tagName="" />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="reason">{{i18n
|
||||
"review.revise_and_reject_post.reason"
|
||||
}}</label>
|
||||
<ComboBox
|
||||
@name="reason"
|
||||
@content={{this.configuredReasons}}
|
||||
@value={{this.reason}}
|
||||
@onChange={{action (mut this.reason)}}
|
||||
@class="revise-and-reject-reviewable__reason"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showCustomReason}}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="custom_reason">{{i18n
|
||||
"review.revise_and_reject_post.custom_reason"
|
||||
}}</label>
|
||||
<Input
|
||||
name="custom_reason"
|
||||
class="revise-and-reject-reviewable__custom-reason"
|
||||
@type="text"
|
||||
@value={{this.customReason}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="feedback">{{i18n
|
||||
"review.revise_and_reject_post.feedback"
|
||||
}}
|
||||
<span class="revise-and-reject-reviewable__optional">({{i18n
|
||||
"review.revise_and_reject_post.optional"
|
||||
}})</span>
|
||||
</label>
|
||||
<DTextarea
|
||||
@name="feedback"
|
||||
@value={{this.feedback}}
|
||||
@onChange={{action (mut this.feedback)}}
|
||||
@class="revise-and-reject-reviewable__feedback"
|
||||
/>
|
||||
</div>
|
||||
</:body>
|
||||
<:footer>
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@action={{this.rejectAndSendPM}}
|
||||
@disabled={{this.sendPMDisabled}}
|
||||
@label="review.revise_and_reject_post.send_pm"
|
||||
/>
|
||||
<DButton
|
||||
class="btn-flat d-modal-cancel"
|
||||
@action={{@closeModal}}
|
||||
@label="cancel"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
|
@ -0,0 +1,62 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "I18n";
|
||||
|
||||
const OTHER_REASON = "other_reason";
|
||||
|
||||
export default class ReviseAndRejectPostReviewable extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked reason;
|
||||
@tracked customReason;
|
||||
@tracked feedback;
|
||||
@tracked submitting = false;
|
||||
|
||||
get configuredReasons() {
|
||||
const reasons = this.siteSettings.reviewable_revision_reasons
|
||||
.split("|")
|
||||
.filter(Boolean)
|
||||
.map((reason) => ({ id: reason, name: reason }))
|
||||
.concat([
|
||||
{
|
||||
id: OTHER_REASON,
|
||||
name: I18n.t("review.revise_and_reject_post.other_reason"),
|
||||
},
|
||||
]);
|
||||
return reasons;
|
||||
}
|
||||
|
||||
get showCustomReason() {
|
||||
return this.reason === OTHER_REASON;
|
||||
}
|
||||
|
||||
get sendPMDisabled() {
|
||||
return (
|
||||
isEmpty(this.reason) ||
|
||||
(this.reason === OTHER_REASON && isEmpty(this.customReason)) ||
|
||||
this.submitting
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async rejectAndSendPM() {
|
||||
this.submitting = true;
|
||||
|
||||
try {
|
||||
await this.args.model.performConfirmed(this.args.model.action, {
|
||||
revise_reason: this.reason,
|
||||
revise_custom_reason: this.customReason,
|
||||
revise_feedback: this.feedback,
|
||||
});
|
||||
this.args.closeModal();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { action, set } from "@ember/object";
|
|||
import { inject as service } from "@ember/service";
|
||||
import { classify, dasherize } from "@ember/string";
|
||||
import ExplainReviewableModal from "discourse/components/modal/explain-reviewable";
|
||||
import ReviseAndRejectPostReviewable from "discourse/components/modal/revise-and-reject-post-reviewable";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import optionalService from "discourse/lib/optional-service";
|
||||
|
@ -15,7 +16,13 @@ import I18n from "I18n";
|
|||
let _components = {};
|
||||
|
||||
const pluginReviewableParams = {};
|
||||
const actionModalClassMap = {};
|
||||
|
||||
// The mappings defined here are default core mappings, and cannot be overridden
|
||||
// by plugins.
|
||||
const defaultActionModalClassMap = {
|
||||
revise_and_reject_post: ReviseAndRejectPostReviewable,
|
||||
};
|
||||
const actionModalClassMap = { ...defaultActionModalClassMap };
|
||||
|
||||
export function addPluginReviewableParam(reviewableType, param) {
|
||||
pluginReviewableParams[reviewableType]
|
||||
|
@ -24,6 +31,11 @@ export function addPluginReviewableParam(reviewableType, param) {
|
|||
}
|
||||
|
||||
export function registerReviewableActionModal(actionName, modalClass) {
|
||||
if (Object.keys(defaultActionModalClassMap).includes(actionName)) {
|
||||
throw new Error(
|
||||
`Cannot override default action modal class for ${actionName} (mapped to ${defaultActionModalClassMap[actionName].name})!`
|
||||
);
|
||||
}
|
||||
actionModalClassMap[actionName] = modalClass;
|
||||
}
|
||||
|
||||
|
@ -135,7 +147,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
@bind
|
||||
_performConfirmed(performableAction) {
|
||||
_performConfirmed(performableAction, additionalData = {}) {
|
||||
let reviewable = this.reviewable;
|
||||
|
||||
let performAction = () => {
|
||||
|
@ -145,6 +157,7 @@ export default Component.extend({
|
|||
const data = {
|
||||
send_email: reviewable.sendEmail,
|
||||
reject_reason: reviewable.rejectReason,
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
(pluginReviewableParams[reviewable.type] || []).forEach((param) => {
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
@import "request_access";
|
||||
@import "request-group-membership-form";
|
||||
@import "reviewables";
|
||||
@import "revise-and-reject-post-reviewable";
|
||||
@import "rtl";
|
||||
@import "search-menu";
|
||||
@import "search";
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
.modal.revise-and-reject-reviewable {
|
||||
.modal-inner-container {
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.control-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.select-kit {
|
||||
width: 100%;
|
||||
summary {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.revise-and-reject-reviewable__optional {
|
||||
margin-left: 0.5em;
|
||||
color: var(--primary-low-mid);
|
||||
}
|
||||
|
||||
.revise-and-reject-reviewable__custom-reason {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.revise-and-reject-reviewable__queued-post {
|
||||
@extend .reviewable-item;
|
||||
|
||||
padding: 1em;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
.post-topic .title-text {
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
|
||||
.post-body {
|
||||
margin: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -222,11 +222,8 @@ class ReviewablesController < ApplicationController
|
|||
return render_json_error(error)
|
||||
end
|
||||
|
||||
if reviewable.type == "ReviewableUser"
|
||||
args.merge!(
|
||||
reject_reason: params[:reject_reason],
|
||||
send_email: params[:send_email] != "false",
|
||||
)
|
||||
if reviewable.type_class.respond_to?(:additional_args)
|
||||
args.merge!(reviewable.type_class.additional_args(params) || {})
|
||||
end
|
||||
|
||||
plugin_params =
|
||||
|
|
|
@ -547,6 +547,10 @@ class Reviewable < ActiveRecord::Base
|
|||
TYPE_TO_BASIC_SERIALIZER[self.type.to_sym] || BasicReviewableSerializer
|
||||
end
|
||||
|
||||
def type_class
|
||||
Reviewable.sti_class_for(self.type)
|
||||
end
|
||||
|
||||
def self.lookup_serializer_for(type)
|
||||
"#{type}Serializer".constantize
|
||||
rescue NameError
|
||||
|
|
|
@ -16,6 +16,16 @@ class ReviewableQueuedPost < Reviewable
|
|||
|
||||
after_commit :compute_user_stats, only: %i[create update]
|
||||
|
||||
def self.additional_args(params)
|
||||
return {} if params[:revise_reason].blank?
|
||||
|
||||
{
|
||||
revise_reason: params[:revise_reason],
|
||||
revise_feedback: params[:revise_feedback],
|
||||
revise_custom_reason: params[:revise_custom_reason],
|
||||
}
|
||||
end
|
||||
|
||||
def updatable_reviewable_scores
|
||||
# Approvals are possible for already rejected queued posts. We need the
|
||||
# scores to be updated when this happens.
|
||||
|
@ -57,6 +67,10 @@ class ReviewableQueuedPost < Reviewable
|
|||
a.label = "reviewables.actions.reject_post.title"
|
||||
end
|
||||
end
|
||||
|
||||
actions.add(:revise_and_reject_post) do |a|
|
||||
a.label = "reviewables.actions.revise_and_reject_post.title"
|
||||
end
|
||||
end
|
||||
|
||||
actions.add(:delete) if guardian.can_delete?(self)
|
||||
|
@ -147,6 +161,24 @@ class ReviewableQueuedPost < Reviewable
|
|||
create_result(:success, :rejected)
|
||||
end
|
||||
|
||||
def perform_revise_and_reject_post(performed_by, args)
|
||||
pm_translation_args = {
|
||||
topic_title: self.topic.title,
|
||||
topic_url: self.topic.url,
|
||||
reason: args[:revise_custom_reason].presence || args[:revise_reason],
|
||||
feedback: args[:revise_feedback],
|
||||
original_post: self.payload["raw"],
|
||||
site_name: SiteSetting.title,
|
||||
}
|
||||
SystemMessage.create_from_system_user(
|
||||
self.target_created_by,
|
||||
:reviewable_queued_post_revise_and_reject,
|
||||
pm_translation_args,
|
||||
)
|
||||
StaffActionLogger.new(performed_by).log_post_rejected(self, DateTime.now) if performed_by.staff?
|
||||
create_result(:success, :rejected)
|
||||
end
|
||||
|
||||
def perform_delete(performed_by, args)
|
||||
create_result(:success, :deleted)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,10 @@ class ReviewableUser < Reviewable
|
|||
create(created_by_id: Discourse.system_user.id, target: user)
|
||||
end
|
||||
|
||||
def self.additional_args(params)
|
||||
{ reject_reason: params[:reject_reason], send_email: params[:send_email] != "false" }
|
||||
end
|
||||
|
||||
def build_actions(actions, guardian, args)
|
||||
return unless pending?
|
||||
|
||||
|
|
|
@ -484,6 +484,14 @@ en:
|
|||
type_bonus:
|
||||
name: "type bonus"
|
||||
title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority."
|
||||
revise_and_reject_post:
|
||||
title: "Revise"
|
||||
reason: "Reason"
|
||||
send_pm: "Send PM"
|
||||
feedback: "Feedback"
|
||||
custom_reason: "Give a clear description of the reason"
|
||||
other_reason: "Other..."
|
||||
optional: "optional"
|
||||
stale_help: "This reviewable has been resolved by <b>%{username}</b>."
|
||||
claim_help:
|
||||
optional: "You can claim this item to prevent others from reviewing it."
|
||||
|
|
|
@ -2302,6 +2302,7 @@ en:
|
|||
approve_new_topics_unless_trust_level: "New topics for users below this trust level must be approved"
|
||||
approve_unless_staged: "New topics and posts for staged users must be approved"
|
||||
notify_about_queued_posts_after: "If there are posts that have been waiting to be reviewed for more than this many hours, send a notification to all moderators. Set to 0 to disable these notifications."
|
||||
reviewable_revision_reasons: "List of reasons that can be selected when rejecting a reviewable queued post with a revision. Other is always available as well, which allows for a custom reason to be entered."
|
||||
auto_close_messages_post_count: "Maximum number of posts allowed in a message before it is automatically closed (0 to disable)"
|
||||
auto_close_topics_post_count: "Maximum number of posts allowed in a topic before it is automatically closed (0 to disable)"
|
||||
auto_close_topics_create_linked_topic: "Create a new linked topic when a topic is auto-closed based on 'auto close topics post count' setting"
|
||||
|
@ -2985,6 +2986,29 @@ en:
|
|||
|
||||
For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines).
|
||||
|
||||
reviewable_queued_post_revise_and_reject:
|
||||
title: "Feedback on your post"
|
||||
subject_template: "Feedback on your post in %{topic_title}"
|
||||
text_body_template: |
|
||||
Hi %{username},
|
||||
|
||||
We've reviewed your post in [%{topic_title}](%{topic_url}) and have some feedback for you.
|
||||
|
||||
Reason: %{reason}
|
||||
|
||||
Feedback: %{feedback}
|
||||
|
||||
You can edit your original post below and re-submit to make the suggested changes, or reply to this message if you have any questions.
|
||||
|
||||
--------
|
||||
|
||||
%{original_post}
|
||||
|
||||
--------
|
||||
|
||||
Thanks,
|
||||
%{site_name} Moderators
|
||||
|
||||
post_hidden_again:
|
||||
title: "Post Hidden again"
|
||||
subject_template: "Post hidden by community flags, staff notified"
|
||||
|
@ -5227,6 +5251,8 @@ en:
|
|||
title: "No"
|
||||
discard_post:
|
||||
title: "Discard Post"
|
||||
revise_and_reject_post:
|
||||
title: "Revise Post..."
|
||||
ignore:
|
||||
title: "Ignore"
|
||||
ignore_and_do_nothing:
|
||||
|
|
|
@ -1968,6 +1968,10 @@ spam:
|
|||
reviewable_low_priority_threshold:
|
||||
default: 0
|
||||
min: 0
|
||||
reviewable_revision_reasons:
|
||||
default: "Duplicate|Does not meet posting guidelines"
|
||||
type: list
|
||||
client: true
|
||||
|
||||
rate_limits:
|
||||
unique_posts_mins: 5
|
||||
|
|
|
@ -125,6 +125,61 @@ RSpec.describe ReviewableQueuedPost, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
context "with revise_and_reject_post" do
|
||||
it "doesn't create the post the user intended" do
|
||||
post_count = Post.public_posts.count
|
||||
result = reviewable.perform(moderator, :revise_and_reject_post)
|
||||
expect(result.success?).to eq(true)
|
||||
expect(result.created_post).to be_nil
|
||||
expect(Post.public_posts.count).to eq(post_count)
|
||||
end
|
||||
|
||||
it "creates a private message to the creator of the post" do
|
||||
args = { revise_reason: "Duplicate", revise_feedback: "This is old news" }
|
||||
expect { reviewable.perform(moderator, :revise_and_reject_post, args) }.to change {
|
||||
Topic.where(archetype: Archetype.private_message).count
|
||||
}
|
||||
|
||||
topic = Topic.where(archetype: Archetype.private_message).last
|
||||
expect(topic.title).to eq(
|
||||
I18n.t(
|
||||
"system_messages.reviewable_queued_post_revise_and_reject.subject_template",
|
||||
topic_title: reviewable.topic.title,
|
||||
),
|
||||
)
|
||||
translation_params = {
|
||||
username: reviewable.target_created_by.username,
|
||||
topic_title: reviewable.topic.title,
|
||||
topic_url: reviewable.topic.url,
|
||||
reason: args[:revise_reason],
|
||||
feedback: args[:revise_feedback],
|
||||
original_post: reviewable.payload["raw"],
|
||||
site_name: SiteSetting.title,
|
||||
}
|
||||
expect(topic.first_post.raw.chomp).to eq(
|
||||
I18n.t(
|
||||
"system_messages.reviewable_queued_post_revise_and_reject.text_body_template",
|
||||
translation_params,
|
||||
).chomp,
|
||||
)
|
||||
end
|
||||
|
||||
it "supports sending a custom revise reason" do
|
||||
args = {
|
||||
revise_reason: "Other...",
|
||||
revise_feedback: "This is old news",
|
||||
revise_custom_reason: "Boring",
|
||||
}
|
||||
expect { reviewable.perform(moderator, :revise_and_reject_post, args) }.to change {
|
||||
Topic.where(archetype: Archetype.private_message).count
|
||||
}
|
||||
topic = Topic.where(archetype: Archetype.private_message).last
|
||||
|
||||
expect(topic.first_post.raw).not_to include("Other...")
|
||||
expect(topic.first_post.raw).to include("Boring")
|
||||
end
|
||||
end
|
||||
|
||||
context "with delete_user" do
|
||||
it "deletes the user and rejects the post" do
|
||||
other_reviewable =
|
||||
|
|
|
@ -22,6 +22,10 @@ module PageObjects
|
|||
find(".modal-footer .btn-primary").click
|
||||
end
|
||||
|
||||
def has_content?(content)
|
||||
find(".modal-body").has_content?(content)
|
||||
end
|
||||
|
||||
def open?
|
||||
has_css?(".modal.d-modal")
|
||||
end
|
||||
|
|
|
@ -18,6 +18,12 @@ module PageObjects
|
|||
end
|
||||
end
|
||||
|
||||
def select_action(reviewable, value)
|
||||
within(reviewable_by_id(reviewable.id)) do
|
||||
find(".reviewable-action.#{value.dasherize}").click
|
||||
end
|
||||
end
|
||||
|
||||
def reviewable_by_id(id)
|
||||
find(".reviewable-item[data-reviewable-id=\"#{id}\"]")
|
||||
end
|
||||
|
|
|
@ -76,6 +76,55 @@ describe "Reviewables", type: :system do
|
|||
expect(queued_post_reviewable.reload).to be_rejected
|
||||
expect(queued_post_reviewable.target_created_by).to be_nil
|
||||
end
|
||||
|
||||
it "allows revising and rejecting to send a PM to the user" do
|
||||
revise_modal = PageObjects::Modals::Base.new
|
||||
|
||||
review_page.visit_reviewable(queued_post_reviewable)
|
||||
|
||||
expect(queued_post_reviewable).to be_pending
|
||||
expect(queued_post_reviewable.target_created_by).to be_present
|
||||
|
||||
review_page.select_action(queued_post_reviewable, "revise_and_reject_post")
|
||||
|
||||
expect(revise_modal).to be_open
|
||||
|
||||
reason_dropdown =
|
||||
PageObjects::Components::SelectKit.new(".revise-and-reject-reviewable__reason")
|
||||
reason_dropdown.select_row_by_value(SiteSetting.reviewable_revision_reasons_map.first)
|
||||
find(".revise-and-reject-reviewable__feedback").fill_in(with: "This is a test")
|
||||
revise_modal.click_primary_button
|
||||
|
||||
expect(review_page).to have_reviewable_with_rejected_status(queued_post_reviewable)
|
||||
expect(queued_post_reviewable.reload).to be_rejected
|
||||
expect(Topic.where(archetype: Archetype.private_message).last.title).to eq(
|
||||
I18n.t(
|
||||
"system_messages.reviewable_queued_post_revise_and_reject.subject_template",
|
||||
topic_title: queued_post_reviewable.topic.title,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
it "allows selecting a custom reason for revise and reject" do
|
||||
revise_modal = PageObjects::Modals::Base.new
|
||||
|
||||
review_page.visit_reviewable(queued_post_reviewable)
|
||||
|
||||
expect(queued_post_reviewable).to be_pending
|
||||
expect(queued_post_reviewable.target_created_by).to be_present
|
||||
|
||||
review_page.select_action(queued_post_reviewable, "revise_and_reject_post")
|
||||
expect(revise_modal).to be_open
|
||||
|
||||
reason_dropdown =
|
||||
PageObjects::Components::SelectKit.new(".revise-and-reject-reviewable__reason")
|
||||
reason_dropdown.select_row_by_value("other_reason")
|
||||
find(".revise-and-reject-reviewable__custom-reason").fill_in(with: "I felt like it")
|
||||
find(".revise-and-reject-reviewable__feedback").fill_in(with: "This is a test")
|
||||
revise_modal.click_primary_button
|
||||
|
||||
expect(review_page).to have_reviewable_with_rejected_status(queued_post_reviewable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user