FEATURE: Allow users to customize bonuses for reviewable types

A new settings section in the review queue allows admins to specify that
certain types of flags should be weighted higher than others.
This commit is contained in:
Robin Ward 2019-04-02 17:00:15 -04:00
parent da2f659635
commit 62956003c3
20 changed files with 333 additions and 132 deletions

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/review/settings";
}
});

View File

@ -1,4 +1,5 @@
/* You might be looking for navigation-item. */
import { iconHTML } from "discourse-common/lib/icon-library";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
@ -6,9 +7,13 @@ export default Ember.Component.extend({
classNameBindings: ["active"],
router: Ember.inject.service(),
@computed("path")
fullPath(path) {
return Discourse.getURL(path);
@computed("label", "i18nLabel", "icon")
contents(label, i18nLabel, icon) {
let text = i18nLabel || I18n.t(label);
if (icon) {
return `${iconHTML(icon)} ${text}`.htmlSafe();
}
return text;
},
@computed("route", "router.currentRoute")

View File

@ -0,0 +1,24 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Ember.Controller.extend({
saving: false,
saved: false,
actions: {
save() {
let bonuses = {};
this.get("settings.reviewable_score_types").forEach(st => {
bonuses[st.id] = parseFloat(st.score_bonus);
});
this.set("saving", true);
ajax("/review/settings", { method: "PUT", data: { bonuses } })
.then(() => {
this.set("saved", true);
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
}
}
});

View File

@ -173,6 +173,7 @@ export default function() {
this.route("show", { path: "/:reviewable_id" });
this.route("index", { path: "/" });
this.route("topics", { path: "/topics" });
this.route("settings", { path: "/settings" });
});
this.route("signup", { path: "/signup" });
this.route("login", { path: "/login" });

View File

@ -0,0 +1,9 @@
export default Discourse.Route.extend({
model() {
return this.store.find("reviewable-settings");
},
setupController(controller, model) {
controller.set("settings", model);
}
});

View File

@ -1,17 +1,7 @@
{{#if routeParam}}
{{#if i18nLabel}}
{{#link-to route routeParam}}{{i18nLabel}}{{/link-to}}
{{else}}
{{#link-to route routeParam}}{{i18n label}}{{/link-to}}
{{/if}}
{{#link-to route routeParam}}{{contents}}{{/link-to}}
{{else if route}}
{{#link-to route}}{{contents}}{{/link-to}}
{{else}}
{{#if route}}
{{#link-to route}}{{i18n label}}{{/link-to}}
{{else}}
{{#if path}}
<a href="{{unbound fullPath}}" data-auto-route="true">{{i18n label}}</a>
{{else}}
<a href="{{unbound href}}" data-auto-route="true">{{i18n label}}</a>
{{/if}}
{{/if}}
<a href="{{get-url path}}" data-auto-route="true">{{contents}}</a>
{{/if}}

View File

@ -1,10 +1,4 @@
<div class="reviewable">
<ul class="nav nav-pills reviewable-title">
{{nav-item route='review.index' label='review.view_all'}}
{{nav-item route='review.topics' label='review.grouped_by_topic'}}
</ul>
<div class="reviewable-container">
<div class="reviewable-container">
<div class="reviewable-list">
{{#if reviewables}}
{{#load-more selector=".reviewable-item" action=(action "loadMore")}}
@ -79,5 +73,4 @@
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
<div class='reviewable-settings'>
<h4>{{i18n "review.settings.score_bonuses.title"}}</h4>
<p class='description'>{{i18n "review.settings.score_bonuses.description"}}</p>
{{#each settings.reviewable_score_types as |rst|}}
<div class='reviewable-score-type'>
<div class='title'>{{rst.title}}</div>
<div class='field'>
{{input value=rst.score_bonus}}
</div>
</div>
{{/each}}
<div class='reviewable-score-type'>
<div class='title'></div>
<div class='field'>
{{d-button
icon="check"
label="review.settings.save_changes"
class="btn-primary save-settings"
action=(action "save")
disabled=saving}}
{{#if saved}}
<span class='saved'>{{i18n "review.settings.saved"}}</span>
{{/if}}
</div>
</div>
</div>

View File

@ -1,10 +1,4 @@
<div class="reviewable">
<ul class="nav nav-pills reviewable-title">
{{nav-item route='review.index' routeParam=(query-params topic_id=null) label='review.view_all'}}
{{nav-item route='review.topics' label='review.grouped_by_topic'}}
</ul>
{{#if reviewableTopics}}
{{#if reviewableTopics}}
<table class='reviewable-topics'>
<thead>
<th>{{i18n "review.topics.topic"}} </th>
@ -37,9 +31,8 @@
{{/each}}
</tbody>
</table>
{{else}}
{{else}}
<div class="no-review">
{{i18n "review.none"}}
</div>
{{/if}}
</div>
{{/if}}

View File

@ -1 +1,11 @@
{{outlet}}
<div class="reviewable">
<ul class="nav nav-pills reviewable-title">
{{nav-item route='review.index' label='review.view_all'}}
{{nav-item route='review.topics' label='review.grouped_by_topic'}}
{{#if currentUser.admin}}
{{nav-item route='review.settings' label='review.settings.title' icon='wrench'}}
{{/if}}
</ul>
{{outlet}}
</div>

View File

@ -1,4 +1,8 @@
.reviewable {
.nav-pills {
margin-bottom: 1em;
}
.reviewable-container {
display: flex;
flex-direction: row;
@ -23,6 +27,23 @@
}
}
.reviewable-settings {
p.description {
margin-bottom: 2em;
}
.saved {
margin-left: 0.5em;
}
.reviewable-score-type {
display: flex;
.title {
width: 30%;
}
}
}
.reviewable-user-details {
margin: 0.5em 0;
}

View File

@ -143,6 +143,21 @@ class ReviewablesController < ApplicationController
end
end
def settings
raise Discourse::InvalidAccess.new unless current_user.admin?
post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order('id')
data = { reviewable_score_types: post_action_types }
if request.put?
params[:bonuses].each do |id, bonus|
PostActionType.where(id: id).update_all(score_bonus: bonus.to_f)
end
end
render_serialized(data, ReviewableSettingsSerializer, rest_serializer: true)
end
protected
def find_reviewable

View File

@ -0,0 +1,7 @@
class ReviewableScoreBonusSerializer < ApplicationSerializer
attributes :id, :name, :score_bonus
def name
I18n.t("post_action_types.#{object.name_key}.title")
end
end

View File

@ -1,5 +1,5 @@
class ReviewableScoreTypeSerializer < ApplicationSerializer
attributes :id, :title
attributes :id, :title, :score_bonus
# Allow us to share post action type translations for backwards compatibility
def title
@ -7,4 +7,12 @@ class ReviewableScoreTypeSerializer < ApplicationSerializer
I18n.t("reviewable_score_types.#{ReviewableScore.types[id]}.title")
end
def score_bonus
object.score_bonus.to_f
end
def include_score_bonus?
object.respond_to?(:score_bonus)
end
end

View File

@ -0,0 +1,13 @@
class ReviewableSettingsSerializer < ApplicationSerializer
attributes :id
has_many :reviewable_score_types, serializer: ReviewableScoreTypeSerializer
def id
scope.user.id
end
def reviewable_score_types
object[:reviewable_score_types]
end
end

View File

@ -359,9 +359,17 @@ en:
placeholder: "type the message title here"
review:
settings:
saved: "Saved"
save_changes: "Save Changes"
title: "Settings"
score_bonuses:
title: "Score Bonuses"
description: "Bonuses allow certain types to be scored higher than others so they can be prioritized. Note: changing these values will not apply to previously scored items."
moderation_history: "Moderation History"
view_all: "view all"
grouped_by_topic: "grouped by topic"
view_all: "View All"
grouped_by_topic: "Grouped by Topic"
none: "There are no items to review."
view_pending: "view pending"
topic_has_pending:

View File

@ -318,6 +318,8 @@ Discourse::Application.routes.draw do
get "review" => "reviewables#index" # For ember app
get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ }
get "review/topics" => "reviewables#topics"
get "review/settings" => "reviewables#settings"
put "review/settings" => "reviewables#settings"
put "review/:reviewable_id/perform/:action_id" => "reviewables#perform", constraints: {
reviewable_id: /\d+/,
action_id: /[a-z\_]+/

View File

@ -12,6 +12,22 @@ describe ReviewablesController do
put "/review/123/perform/approve.json"
expect(response.code).to eq("403")
end
it "denies settings" do
get "/review/settings.json"
expect(response.code).to eq("403")
end
end
context "regular user" do
before do
sign_in(Fabricate(:user))
end
it "does not allow settings" do
get "/review/settings.json"
expect(response.code).to eq("403")
end
end
context "when logged in" do
@ -307,6 +323,22 @@ describe ReviewablesController do
end
end
context "#settings" do
it "renders the settings as JSON" do
get "/review/settings.json"
expect(response.code).to eq("200")
json = ::JSON.parse(response.body)
expect(json['reviewable_settings']).to be_present
expect(json['reviewable_score_types']).to be_present
end
it "allows the settings to be updated" do
put "/review/settings.json", params: { bonuses: { 8 => 3.45 } }
expect(response.code).to eq("200")
expect(PostActionType.find_by(id: 8).score_bonus).to eq(3.45)
end
end
context "#update" do
let(:reviewable) { Fabricate(:reviewable) }
let(:reviewable_post) { Fabricate(:reviewable_queued_post) }

View File

@ -33,6 +33,16 @@ QUnit.test("Grouped by topic", async assert => {
);
});
QUnit.test("Settings", async assert => {
await visit("/review/settings");
assert.ok(find(".reviewable-score-type").length, "has a list of bonuses");
await fillIn(".reviewable-score-type:eq(0) .field input ", "0.5");
await click(".save-settings");
assert.ok(find(".reviewable-settings .saved").length, "it saved");
});
QUnit.test("Flag related", async assert => {
await visit("/review");

View File

@ -89,6 +89,30 @@ export default function(helpers) {
});
});
this.get("/review/settings", () => {
return response(200, {
reviewable_score_types: [
{
id: 3,
title: "Off-Topic",
score_bonus: 0.0
},
{
id: 4,
title: "Inappropriate",
score_bonus: 0.0
}
],
reviewable_settings: {
id: 13870,
reviewable_score_type_ids: [3, 4]
},
__rest_serializer: "1"
});
});
this.put("/review/settings", () => response(200, {}));
this.get("/review/:id", () => {
return response(200, {
reviewable: flag