mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 01:16:38 -06:00
FEATURE: Adds a pop up that shows a more detailed score for reviewables (#8035)
If you click a (?) icon beside the reviewable status a pop up will appear with expanded informatio that explains how the reviewable got its score, and how it compares to system thresholds.
This commit is contained in:
parent
e90636eadc
commit
bde0ef865f
@ -0,0 +1,9 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
jsonMode: true,
|
||||
|
||||
pathFor(store, type, id) {
|
||||
return `/review/${id}/explain.json`;
|
||||
}
|
||||
});
|
@ -3,6 +3,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Category from "discourse/models/category";
|
||||
import optionalService from "discourse/lib/optional-service";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
let _components = {};
|
||||
|
||||
@ -140,6 +141,13 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
explainReviewable(reviewable) {
|
||||
showModal("explain-reviewable", {
|
||||
title: "review.explain.title",
|
||||
model: reviewable
|
||||
});
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.set("editing", true);
|
||||
this._updates = { payload: {} };
|
||||
|
@ -0,0 +1,15 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: null,
|
||||
reviewableExplanation: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({ loading: true, reviewableExplanation: null });
|
||||
|
||||
this.store
|
||||
.find("reviewable-explanation", this.model.id)
|
||||
.then(result => this.set("reviewableExplanation", result))
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
});
|
5
app/assets/javascripts/discourse/helpers/float.js.es6
Normal file
5
app/assets/javascripts/discourse/helpers/float.js.es6
Normal file
@ -0,0 +1,5 @@
|
||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
|
||||
registerUnbound("float", function(n) {
|
||||
return parseFloat(n).toFixed(1);
|
||||
});
|
@ -10,6 +10,9 @@
|
||||
<span class='status'>
|
||||
{{reviewable-status reviewable.status}}
|
||||
</span>
|
||||
<a {{action "explainReviewable" reviewable}} class='explain' title={{i18n "review.explain.why"}}>
|
||||
{{d-icon "question-circle"}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='reviewable-contents'>
|
||||
|
@ -0,0 +1,11 @@
|
||||
{{#if value}}
|
||||
<span class='score-value'>
|
||||
<span class='score-number'>{{float value}}</span>
|
||||
{{#if label}}
|
||||
<span class='score-value-type' title={{i18n (concat "review.explain." label ".title")}}>
|
||||
{{i18n (concat "review.explain." label ".name")}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='op'>+</span>
|
||||
{{/if}}
|
@ -0,0 +1,47 @@
|
||||
{{#d-modal-body class="explain-reviewable"}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{i18n "review.explain.formula"}}</th>
|
||||
<th>{{i18n "review.explain.subtotal"}}</th>
|
||||
</tr>
|
||||
{{#each reviewableExplanation.scores as |s|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{score-value value="1.0" tagName=""}}
|
||||
{{score-value value=s.type_bonus label="type_bonus" tagName=""}}
|
||||
{{score-value value=s.take_action_bonus label="take_action_bonus" tagName=""}}
|
||||
{{score-value value=s.trust_level_bonus label="trust_level_bonus" tagName=""}}
|
||||
{{score-value value=s.user_accuracy_bonus label="user_accuracy_bonus" tagName=""}}
|
||||
</td>
|
||||
<td class='sum'>{{float s.score}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr class="total">
|
||||
<td>{{i18n "review.explain.total"}}</td>
|
||||
<td class='sum'>{{float reviewableExplanation.total_score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class='thresholds'>
|
||||
<tr>
|
||||
<td>{{i18n "review.explain.min_score_visibility"}}</td>
|
||||
<td class='sum'>
|
||||
{{float reviewableExplanation.min_score_visibility}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{i18n "review.explain.score_to_hide"}}</td>
|
||||
<td class='sum'>
|
||||
{{float reviewableExplanation.hide_post_score}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action=(route-action "closeModal") label="close"}}
|
||||
</div>
|
37
app/assets/stylesheets/common/base/explain-reviewable.scss
Normal file
37
app/assets/stylesheets/common/base/explain-reviewable.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.explain-reviewable {
|
||||
min-width: 500px;
|
||||
|
||||
.thresholds {
|
||||
margin-top: 1em;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
table td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
td.sum {
|
||||
text-align: right;
|
||||
}
|
||||
td.sum.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
tr.total {
|
||||
td {
|
||||
background-color: $primary-low;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.op {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score-value-type {
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
.op:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -20,6 +20,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.explain {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
margin-bottom: 1em;
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
require_dependency 'reviewable_explanation_serializer'
|
||||
|
||||
class ReviewablesController < ApplicationController
|
||||
requires_login
|
||||
@ -102,6 +103,17 @@ class ReviewablesController < ApplicationController
|
||||
)
|
||||
end
|
||||
|
||||
def explain
|
||||
reviewable = find_reviewable
|
||||
|
||||
render_serialized(
|
||||
{ reviewable: reviewable, scores: reviewable.explain_score },
|
||||
ReviewableExplanationSerializer,
|
||||
rest_serializer: true,
|
||||
root: 'reviewable_explanation'
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
reviewable = find_reviewable
|
||||
|
||||
|
@ -481,6 +481,25 @@ class Reviewable < ActiveRecord::Base
|
||||
.count
|
||||
end
|
||||
|
||||
def explain_score
|
||||
DB.query(<<~SQL, reviewable_id: id)
|
||||
SELECT rs.reviewable_id,
|
||||
rs.user_id,
|
||||
CASE WHEN (u.admin OR u.moderator) THEN 5.0 ELSE u.trust_level END AS trust_level_bonus,
|
||||
us.flags_agreed,
|
||||
us.flags_disagreed,
|
||||
us.flags_ignored,
|
||||
rs.score,
|
||||
rs.take_action_bonus,
|
||||
COALESCE(pat.score_bonus, 0.0) AS type_bonus
|
||||
FROM reviewable_scores AS rs
|
||||
INNER JOIN users AS u ON u.id = rs.user_id
|
||||
LEFT OUTER JOIN user_stats AS us ON us.user_id = rs.user_id
|
||||
LEFT OUTER JOIN post_action_types AS pat ON pat.id = rs.reviewable_score_type
|
||||
WHERE rs.reviewable_id = :reviewable_id
|
||||
SQL
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def recalculate_score
|
||||
|
@ -59,10 +59,22 @@ class ReviewableScore < ActiveRecord::Base
|
||||
user_stat = user&.user_stat
|
||||
return 0.0 if user_stat.blank?
|
||||
|
||||
total = (user_stat.flags_agreed + user_stat.flags_disagreed + user_stat.flags_ignored).to_f
|
||||
calc_user_accuracy_bonus(
|
||||
user_stat.flags_agreed,
|
||||
user_stat.flags_disagreed,
|
||||
user_stat.flags_ignored
|
||||
)
|
||||
end
|
||||
|
||||
def self.calc_user_accuracy_bonus(agreed, disagreed, ignored)
|
||||
agreed ||= 0
|
||||
disagreed ||= 0
|
||||
ignored ||= 0
|
||||
|
||||
total = (agreed + disagreed + ignored).to_f
|
||||
return 0.0 if total <= 5
|
||||
|
||||
(user_stat.flags_agreed / total) * 5.0
|
||||
(agreed / total) * 5.0
|
||||
end
|
||||
|
||||
def reviewable_conversation
|
||||
|
38
app/serializers/reviewable_explanation_serializer.rb
Normal file
38
app/serializers/reviewable_explanation_serializer.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
require_dependency 'reviewable_score_explanation_serializer'
|
||||
|
||||
class ReviewableExplanationSerializer < ApplicationSerializer
|
||||
attributes(
|
||||
:id,
|
||||
:total_score,
|
||||
:scores,
|
||||
:min_score_visibility,
|
||||
:hide_post_score
|
||||
)
|
||||
|
||||
has_many :scores, serializer: ReviewableScoreExplanationSerializer, embed: :objects
|
||||
|
||||
def id
|
||||
object[:reviewable].id
|
||||
end
|
||||
|
||||
def hide_post_score
|
||||
Reviewable.score_required_to_hide_post
|
||||
end
|
||||
|
||||
def spam_silence_score
|
||||
Reviewable.spam_score_to_silence_new_user
|
||||
end
|
||||
|
||||
def min_score_visibility
|
||||
Reviewable.min_score_for_priority
|
||||
end
|
||||
|
||||
def total_score
|
||||
object[:reviewable].score
|
||||
end
|
||||
|
||||
def scores
|
||||
object[:scores]
|
||||
end
|
||||
end
|
24
app/serializers/reviewable_score_explanation_serializer.rb
Normal file
24
app/serializers/reviewable_score_explanation_serializer.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ReviewableScoreExplanationSerializer < ApplicationSerializer
|
||||
attributes(
|
||||
:user_id,
|
||||
:type_bonus,
|
||||
:trust_level_bonus,
|
||||
:take_action_bonus,
|
||||
:flags_agreed,
|
||||
:flags_disagreed,
|
||||
:flags_ignored,
|
||||
:user_accuracy_bonus,
|
||||
:score
|
||||
)
|
||||
|
||||
def user_accuracy_bonus
|
||||
ReviewableScore.calc_user_accuracy_bonus(
|
||||
object.flags_agreed,
|
||||
object.flags_disagreed,
|
||||
object.flags_ignored
|
||||
)
|
||||
end
|
||||
|
||||
end
|
@ -370,6 +370,23 @@ en:
|
||||
review:
|
||||
order_by: "Order by"
|
||||
in_reply_to: "in reply to"
|
||||
explain:
|
||||
why: "explain why this item ended up in the queue"
|
||||
title: "Reviewable Scoring"
|
||||
formula: "Formula"
|
||||
subtotal: "Subtotal"
|
||||
total: "Total"
|
||||
min_score_visibility: "Minimum Score for Visibility"
|
||||
score_to_hide: "Score to Hide Post"
|
||||
user_accuracy_bonus:
|
||||
name: "user accuracy"
|
||||
title: "Users whose flags have been historically agreed with are given a bonus."
|
||||
trust_level_bonus:
|
||||
name: "trust level"
|
||||
title: "Reviewable items created by higher trust level users have a higher score."
|
||||
type_bonus:
|
||||
name: "type bonus"
|
||||
title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority."
|
||||
claim_help:
|
||||
optional: "You can claim this item to prevent others from reviewing it."
|
||||
required: "You must claim items before you can review them."
|
||||
|
@ -324,6 +324,7 @@ Discourse::Application.routes.draw do
|
||||
|
||||
get "review" => "reviewables#index" # For ember app
|
||||
get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ }
|
||||
get "review/:reviewable_id/explain" => "reviewables#explain", constraints: { reviewable_id: /\d+/ }
|
||||
get "review/topics" => "reviewables#topics"
|
||||
get "review/settings" => "reviewables#settings"
|
||||
put "review/settings" => "reviewables#settings"
|
||||
|
@ -236,6 +236,30 @@ describe ReviewablesController do
|
||||
end
|
||||
end
|
||||
|
||||
context "#explain" do
|
||||
context "basics" do
|
||||
fab!(:reviewable) { Fabricate(:reviewable) }
|
||||
|
||||
before do
|
||||
sign_in(Fabricate(:moderator))
|
||||
end
|
||||
|
||||
it "returns the explanation as json" do
|
||||
get "/review/#{reviewable.id}/explain.json"
|
||||
expect(response.code).to eq("200")
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json['reviewable_explanation']['id']).to eq(reviewable.id)
|
||||
expect(json['reviewable_explanation']['total_score']).to eq(reviewable.score)
|
||||
end
|
||||
|
||||
it "returns 404 for a missing reviewable" do
|
||||
get "/review/123456789/explain.json"
|
||||
expect(response.code).to eq("404")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#perform" do
|
||||
fab!(:reviewable) { Fabricate(:reviewable) }
|
||||
before do
|
||||
|
Loading…
Reference in New Issue
Block a user