mirror of
https://github.com/discourse/discourse.git
synced 2025-03-26 02:45:36 +08: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 computed from "ember-addons/ember-computed-decorators";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import optionalService from "discourse/lib/optional-service";
|
import optionalService from "discourse/lib/optional-service";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
let _components = {};
|
let _components = {};
|
||||||
|
|
||||||
@ -140,6 +141,13 @@ export default Ember.Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
explainReviewable(reviewable) {
|
||||||
|
showModal("explain-reviewable", {
|
||||||
|
title: "review.explain.title",
|
||||||
|
model: reviewable
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
this.set("editing", true);
|
this.set("editing", true);
|
||||||
this._updates = { payload: {} };
|
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'>
|
<span class='status'>
|
||||||
{{reviewable-status reviewable.status}}
|
{{reviewable-status reviewable.status}}
|
||||||
</span>
|
</span>
|
||||||
|
<a {{action "explainReviewable" reviewable}} class='explain' title={{i18n "review.explain.why"}}>
|
||||||
|
{{d-icon "question-circle"}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='reviewable-contents'>
|
<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 {
|
.nav-pills {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
require_dependency 'reviewable_explanation_serializer'
|
||||||
|
|
||||||
class ReviewablesController < ApplicationController
|
class ReviewablesController < ApplicationController
|
||||||
requires_login
|
requires_login
|
||||||
@ -102,6 +103,17 @@ class ReviewablesController < ApplicationController
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def explain
|
||||||
|
reviewable = find_reviewable
|
||||||
|
|
||||||
|
render_serialized(
|
||||||
|
{ reviewable: reviewable, scores: reviewable.explain_score },
|
||||||
|
ReviewableExplanationSerializer,
|
||||||
|
rest_serializer: true,
|
||||||
|
root: 'reviewable_explanation'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
reviewable = find_reviewable
|
reviewable = find_reviewable
|
||||||
|
|
||||||
|
@ -481,6 +481,25 @@ class Reviewable < ActiveRecord::Base
|
|||||||
.count
|
.count
|
||||||
end
|
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
|
protected
|
||||||
|
|
||||||
def recalculate_score
|
def recalculate_score
|
||||||
|
@ -59,10 +59,22 @@ class ReviewableScore < ActiveRecord::Base
|
|||||||
user_stat = user&.user_stat
|
user_stat = user&.user_stat
|
||||||
return 0.0 if user_stat.blank?
|
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
|
return 0.0 if total <= 5
|
||||||
|
|
||||||
(user_stat.flags_agreed / total) * 5.0
|
(agreed / total) * 5.0
|
||||||
end
|
end
|
||||||
|
|
||||||
def reviewable_conversation
|
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:
|
review:
|
||||||
order_by: "Order by"
|
order_by: "Order by"
|
||||||
in_reply_to: "in reply to"
|
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:
|
claim_help:
|
||||||
optional: "You can claim this item to prevent others from reviewing it."
|
optional: "You can claim this item to prevent others from reviewing it."
|
||||||
required: "You must claim items before you can review them."
|
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" => "reviewables#index" # For ember app
|
||||||
get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ }
|
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/topics" => "reviewables#topics"
|
||||||
get "review/settings" => "reviewables#settings"
|
get "review/settings" => "reviewables#settings"
|
||||||
put "review/settings" => "reviewables#settings"
|
put "review/settings" => "reviewables#settings"
|
||||||
|
@ -236,6 +236,30 @@ describe ReviewablesController do
|
|||||||
end
|
end
|
||||||
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
|
context "#perform" do
|
||||||
fab!(:reviewable) { Fabricate(:reviewable) }
|
fab!(:reviewable) { Fabricate(:reviewable) }
|
||||||
before do
|
before do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user