From 62956003c37b3aa2dedc1b5c1d312420c9d1dcf4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 2 Apr 2019 17:00:15 -0400 Subject: [PATCH] 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. --- .../adapters/reviewable-settings.js.es6 | 7 + .../discourse/components/nav-item.js.es6 | 11 +- .../controllers/review-settings.js.es6 | 24 ++++ .../discourse/routes/app-route-map.js.es6 | 1 + .../discourse/routes/review-settings.js.es6 | 9 ++ .../templates/components/nav-item.hbs | 18 +-- .../discourse/templates/review-index.hbs | 125 +++++++++--------- .../discourse/templates/review-settings.hbs | 29 ++++ .../discourse/templates/review-topics.hbs | 83 ++++++------ .../discourse/templates/review.hbs | 12 +- .../stylesheets/common/base/reviewables.scss | 21 +++ app/controllers/reviewables_controller.rb | 15 +++ .../reviewable_score_bonus_serializer.rb | 7 + .../reviewable_score_type_serializer.rb | 10 +- .../reviewable_settings_serializer.rb | 13 ++ config/locales/client.en.yml | 12 +- config/routes.rb | 2 + spec/requests/reviewables_controller_spec.rb | 32 +++++ .../javascripts/acceptance/review-test.js.es6 | 10 ++ .../helpers/review-pretender.js.es6 | 24 ++++ 20 files changed, 333 insertions(+), 132 deletions(-) create mode 100644 app/assets/javascripts/discourse/adapters/reviewable-settings.js.es6 create mode 100644 app/assets/javascripts/discourse/controllers/review-settings.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/review-settings.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/review-settings.hbs create mode 100644 app/serializers/reviewable_score_bonus_serializer.rb create mode 100644 app/serializers/reviewable_settings_serializer.rb diff --git a/app/assets/javascripts/discourse/adapters/reviewable-settings.js.es6 b/app/assets/javascripts/discourse/adapters/reviewable-settings.js.es6 new file mode 100644 index 00000000000..8b299035d17 --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/reviewable-settings.js.es6 @@ -0,0 +1,7 @@ +import RestAdapter from "discourse/adapters/rest"; + +export default RestAdapter.extend({ + pathFor() { + return "/review/settings"; + } +}); diff --git a/app/assets/javascripts/discourse/components/nav-item.js.es6 b/app/assets/javascripts/discourse/components/nav-item.js.es6 index d83952cab6f..20d6d8049f4 100644 --- a/app/assets/javascripts/discourse/components/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/components/nav-item.js.es6 @@ -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") diff --git a/app/assets/javascripts/discourse/controllers/review-settings.js.es6 b/app/assets/javascripts/discourse/controllers/review-settings.js.es6 new file mode 100644 index 00000000000..339b0055949 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/review-settings.js.es6 @@ -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)); + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 05023b3f59b..fe84822b078 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -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" }); diff --git a/app/assets/javascripts/discourse/routes/review-settings.js.es6 b/app/assets/javascripts/discourse/routes/review-settings.js.es6 new file mode 100644 index 00000000000..46ed6f10fa5 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/review-settings.js.es6 @@ -0,0 +1,9 @@ +export default Discourse.Route.extend({ + model() { + return this.store.find("reviewable-settings"); + }, + + setupController(controller, model) { + controller.set("settings", model); + } +}); diff --git a/app/assets/javascripts/discourse/templates/components/nav-item.hbs b/app/assets/javascripts/discourse/templates/components/nav-item.hbs index 96c49a6fc3d..1205e1b36ab 100644 --- a/app/assets/javascripts/discourse/templates/components/nav-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/nav-item.hbs @@ -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}} - {{i18n label}} - {{else}} - {{i18n label}} - {{/if}} - {{/if}} + {{contents}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/review-index.hbs b/app/assets/javascripts/discourse/templates/review-index.hbs index b1110bef60b..fc7819750f2 100644 --- a/app/assets/javascripts/discourse/templates/review-index.hbs +++ b/app/assets/javascripts/discourse/templates/review-index.hbs @@ -1,83 +1,76 @@ -
- - -
-
- {{#if reviewables}} - {{#load-more selector=".reviewable-item" action=(action "loadMore")}} -
- {{#each reviewables as |r|}} - {{reviewable-item reviewable=r remove=(action "remove")}} - {{/each}} -
- {{/load-more}} - {{conditional-loading-spinner condition=reviewables.loadingMore}} - {{else}} -
- {{i18n "review.none"}} +
+
+ {{#if reviewables}} + {{#load-more selector=".reviewable-item" action=(action "loadMore")}} +
+ {{#each reviewables as |r|}} + {{reviewable-item reviewable=r remove=(action "remove")}} + {{/each}}
- {{/if}} + {{/load-more}} + {{conditional-loading-spinner condition=reviewables.loadingMore}} + {{else}} +
+ {{i18n "review.none"}} +
+ {{/if}} +
+ +
+
+ + {{combo-box value=filterStatus content=statuses}}
-
+ {{#if filtersExpanded}}
- - {{combo-box value=filterStatus content=statuses}} + + {{combo-box value=filterType content=allTypes none="review.filters.type.all"}}
- {{#if filtersExpanded}} -
- - {{combo-box value=filterType content=allTypes none="review.filters.type.all"}} -
+
+ + {{input value=filterScore class="score-filter"}} +
-
- - {{input value=filterScore class="score-filter"}} -
+
+ + {{category-chooser none="category.all" value=filterCategoryId}} +
-
- - {{category-chooser none="category.all" value=filterCategoryId}} -
+
+ {{i18n "review.filtered_user"}} + {{user-selector + excludeCurrentUser=false + usernames=filterUsername + fullWidthWrap="true" + class="user-selector" + single="true" + canReceiveUpdates="true"}} +
+ {{#if filterTopic}}
- {{i18n "review.filtered_user"}} - {{user-selector - excludeCurrentUser=false - usernames=filterUsername - fullWidthWrap="true" - class="user-selector" - single="true" - canReceiveUpdates="true"}} + {{i18n "review.filtered_topic"}} + {{d-button label="review.show_all_topics" icon="times" action=(action "resetTopic")}}
- - {{#if filterTopic}} -
- {{i18n "review.filtered_topic"}} - {{d-button label="review.show_all_topics" icon="times" action=(action "resetTopic")}} -
- {{/if}} {{/if}} + {{/if}} -
+
+ {{d-button + icon="sync" + label="review.filters.refresh" + + class="btn-primary refresh" action=(action "refresh")}} + + {{#if site.mobileView}} {{d-button - icon="sync" - label="review.filters.refresh" - - class="btn-primary refresh" action=(action "refresh")}} - - {{#if site.mobileView}} - {{d-button - label="show_help" - icon=toggleFiltersIcon - class="btn-default expand-secondary-filters" - action=(action "toggleFilters")}} - {{/if}} -
+ label="show_help" + icon=toggleFiltersIcon + class="btn-default expand-secondary-filters" + action=(action "toggleFilters")}} + {{/if}}
diff --git a/app/assets/javascripts/discourse/templates/review-settings.hbs b/app/assets/javascripts/discourse/templates/review-settings.hbs new file mode 100644 index 00000000000..142562cf2d6 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/review-settings.hbs @@ -0,0 +1,29 @@ +
+

{{i18n "review.settings.score_bonuses.title"}}

+

{{i18n "review.settings.score_bonuses.description"}}

+ + {{#each settings.reviewable_score_types as |rst|}} +
+
{{rst.title}}
+
+ {{input value=rst.score_bonus}} +
+
+ {{/each}} + +
+
+
+ {{d-button + icon="check" + label="review.settings.save_changes" + class="btn-primary save-settings" + action=(action "save") + disabled=saving}} + + {{#if saved}} + {{i18n "review.settings.saved"}} + {{/if}} +
+
+
diff --git a/app/assets/javascripts/discourse/templates/review-topics.hbs b/app/assets/javascripts/discourse/templates/review-topics.hbs index 7fc8f8be7d3..39a76f3fd8f 100644 --- a/app/assets/javascripts/discourse/templates/review-topics.hbs +++ b/app/assets/javascripts/discourse/templates/review-topics.hbs @@ -1,45 +1,38 @@ -
- - - {{#if reviewableTopics}} - - - - - - - - - {{#each reviewableTopics as |rt|}} - - - - - - - {{/each}} - -
{{i18n "review.topics.topic"}} {{i18n "review.topics.reviewable_count"}}{{i18n "review.topics.reported_by"}}
-
- {{topic-status topic=rt}} - {{replace-emoji rt.fancy_title}} -
-
- {{rt.stats.count}} - - {{i18n "review.topics.unique_users" count=rt.stats.unique_users}} - - {{#link-to "review.index" (query-params topic_id=rt.id) class="btn btn-primary btn-small"}} - {{d-icon "list"}} - {{i18n "review.topics.details"}} - {{/link-to}} -
- {{else}} -
- {{i18n "review.none"}} -
- {{/if}} -
+{{#if reviewableTopics}} + + + + + + + + + {{#each reviewableTopics as |rt|}} + + + + + + + {{/each}} + +
{{i18n "review.topics.topic"}} {{i18n "review.topics.reviewable_count"}}{{i18n "review.topics.reported_by"}}
+
+ {{topic-status topic=rt}} + {{replace-emoji rt.fancy_title}} +
+
+ {{rt.stats.count}} + + {{i18n "review.topics.unique_users" count=rt.stats.unique_users}} + + {{#link-to "review.index" (query-params topic_id=rt.id) class="btn btn-primary btn-small"}} + {{d-icon "list"}} + {{i18n "review.topics.details"}} + {{/link-to}} +
+{{else}} +
+ {{i18n "review.none"}} +
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/review.hbs b/app/assets/javascripts/discourse/templates/review.hbs index c24cd68950a..f5e5dcdd2ef 100644 --- a/app/assets/javascripts/discourse/templates/review.hbs +++ b/app/assets/javascripts/discourse/templates/review.hbs @@ -1 +1,11 @@ -{{outlet}} +
+ + + {{outlet}} +
diff --git a/app/assets/stylesheets/common/base/reviewables.scss b/app/assets/stylesheets/common/base/reviewables.scss index 07238854dfe..15ff987deba 100644 --- a/app/assets/stylesheets/common/base/reviewables.scss +++ b/app/assets/stylesheets/common/base/reviewables.scss @@ -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; } diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index 7bfc4bc0e88..04e76230e50 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -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 diff --git a/app/serializers/reviewable_score_bonus_serializer.rb b/app/serializers/reviewable_score_bonus_serializer.rb new file mode 100644 index 00000000000..9917bd15fd1 --- /dev/null +++ b/app/serializers/reviewable_score_bonus_serializer.rb @@ -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 diff --git a/app/serializers/reviewable_score_type_serializer.rb b/app/serializers/reviewable_score_type_serializer.rb index c4d8ecd2d53..675a1275d8a 100644 --- a/app/serializers/reviewable_score_type_serializer.rb +++ b/app/serializers/reviewable_score_type_serializer.rb @@ -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 diff --git a/app/serializers/reviewable_settings_serializer.rb b/app/serializers/reviewable_settings_serializer.rb new file mode 100644 index 00000000000..64b83ce23bd --- /dev/null +++ b/app/serializers/reviewable_settings_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index abd454e1b09..16d69af5b41 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index 08233e7b0c6..9cc0354d66a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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\_]+/ diff --git a/spec/requests/reviewables_controller_spec.rb b/spec/requests/reviewables_controller_spec.rb index 656b61e358a..ebec09fdc10 100644 --- a/spec/requests/reviewables_controller_spec.rb +++ b/spec/requests/reviewables_controller_spec.rb @@ -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) } diff --git a/test/javascripts/acceptance/review-test.js.es6 b/test/javascripts/acceptance/review-test.js.es6 index 190e370ee02..958f6fe2db8 100644 --- a/test/javascripts/acceptance/review-test.js.es6 +++ b/test/javascripts/acceptance/review-test.js.es6 @@ -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"); diff --git a/test/javascripts/helpers/review-pretender.js.es6 b/test/javascripts/helpers/review-pretender.js.es6 index 80858c3115d..4e6d1194526 100644 --- a/test/javascripts/helpers/review-pretender.js.es6 +++ b/test/javascripts/helpers/review-pretender.js.es6 @@ -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