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:
parent
da2f659635
commit
62956003c3
|
@ -0,0 +1,7 @@
|
|||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/review/settings";
|
||||
}
|
||||
});
|
|
@ -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")
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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" });
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.store.find("reviewable-settings");
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("settings", model);
|
||||
}
|
||||
});
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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\_]+/
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue