FEATURE: Track how many user flags are agreed/disagreed/ignored

Display the percentage when reviewing flags.
This commit is contained in:
Robin Ward 2018-10-31 15:35:07 -04:00
parent ceafcbc898
commit ec91450aae
13 changed files with 183 additions and 20 deletions

View File

@ -0,0 +1,53 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
tagName: "",
@computed("percentage")
showPercentage(percentage) {
return percentage.total >= 3;
},
// We do a little logic to choose which icon to display and which text
@computed("user.flags_agreed", "user.flags_disagreed", "user.flags_ignored")
percentage(agreed, disagreed, ignored) {
let total = agreed + disagreed + ignored;
let result = { total };
if (total > 0) {
result.agreed = Math.round((agreed / total) * 100);
result.disagreed = Math.round((disagreed / total) * 100);
result.ignored = Math.round((ignored / total) * 100);
}
let highest = Math.max(agreed, disagreed, ignored);
if (highest === agreed) {
result.icon = "thumbs-up";
result.className = "agreed";
result.label = `${result.agreed}%`;
} else if (highest === disagreed) {
result.icon = "thumbs-down";
result.className = "disagreed";
result.label = `${result.disagreed}%`;
} else {
result.icon = "external-link";
result.className = "ignored";
result.label = `${result.ignored}%`;
}
result.title = I18n.t("admin.flags.user_percentage.summary", {
agreed: I18n.t("admin.flags.user_percentage.agreed", {
count: result.agreed
}),
disagreed: I18n.t("admin.flags.user_percentage.disagreed", {
count: result.disagreed
}),
ignored: I18n.t("admin.flags.user_percentage.disagreed", {
count: result.ignored
}),
count: total
});
return result;
}
});

View File

@ -8,6 +8,7 @@
<div class='flagger-flag-type'>
{{post-action-title postAction.post_action_type_id postAction.name_key}}
</div>
{{user-flag-percentage user=postAction.user}}
{{/flag-user}}
{{/each}}
</div>

View File

@ -0,0 +1,6 @@
{{#if showPercentage}}
<div class='user-flag-percentage' title={{percentage.title}}>
<span class="percentage-label {{percentage.className}}">{{percentage.label}}</span>
{{d-icon percentage.icon}}
</div>
{{/if}}

View File

@ -107,10 +107,32 @@
.flag-user-date {
color: $primary-medium;
}
.flag-user-avatar {
margin-right: 0.5em;
}
.flag-user-extra {
display: flex;
align-items: center;
.user-flag-percentage {
display: flex;
align-items: center;
margin-left: 0.5em;
.percentage-label {
margin-right: 0.25em;
&.agreed {
color: $success;
}
&.disagreed {
color: $danger;
}
&.ignored {
color: $primary-medium;
}
}
}
}
}
.flag-conversation {

View File

@ -164,6 +164,9 @@ class PostAction < ActiveRecord::Base
trigger_spam = true if action.post_action_type_id == PostActionType.types[:spam]
end
# Update the flags_agreed user stat
UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_agreed = flags_agreed + 1")
DiscourseEvent.trigger(:confirmed_spam_post, post) if trigger_spam
if actions.first.present?
@ -183,8 +186,7 @@ class PostAction < ActiveRecord::Base
PostActionType.notify_flag_type_ids
end
actions = PostAction.where(post_id: post.id)
.where(post_action_type_id: action_type_ids)
actions = PostAction.active.where(post_id: post.id).where(post_action_type_id: action_type_ids)
actions.each do |action|
action.disagreed_at = Time.zone.now
@ -194,6 +196,9 @@ class PostAction < ActiveRecord::Base
action.add_moderator_post_if_needed(moderator, :disagreed)
end
# Update the flags_disagreed user stat
UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_disagreed = flags_disagreed + 1")
# reset all cached counters
cached = {}
action_type_ids.each do |atid|

View File

@ -1207,18 +1207,16 @@ class Report
u.username,
u.uploaded_avatar_id as avatar_id,
CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced,
SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) as disagreed_flags,
SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) as agreed_flags,
ROUND(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric, 2) as ratio,
SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) spread,
ROUND((1-(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric)) *
(SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)), 2) as score
FROM post_actions AS pa
INNER JOIN users AS u ON u.id = pa.user_id
WHERE pa.post_action_type_id IN (#{PostActionType.flag_types.values.join(', ')})
AND pa.user_id <> -1
GROUP BY u.id, u.username, u.silenced_till
HAVING SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) > SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)
us.flags_disagreed AS disagreed_flags,
us.flags_agreed AS agreed_flags,
ROUND(us.flags_agreed::numeric / us.flags_disagreed::numeric, 2) as ratio,
us.flags_disagreed - us.flags_agreed AS spread,
ROUND((1-(us.flags_agreed::numeric / us.flags_disagreed::numeric)) *
(us.flags_disagreed - us.flags_agreed)) AS score
FROM users AS u
INNER JOIN user_stats AS us ON us.user_id = u.id
WHERE u.id <> -1
AND flags_disagreed > flags_agreed
ORDER BY score DESC
LIMIT 20
SQL

View File

@ -4,7 +4,10 @@ class FlaggedUserSerializer < BasicUserSerializer
:post_count,
:topic_count,
:ip_address,
:custom_fields
:custom_fields,
:flags_agreed,
:flags_disagreed,
:flags_ignored
def can_delete_all_posts
scope.can_delete_all_posts?(object)
@ -18,6 +21,18 @@ class FlaggedUserSerializer < BasicUserSerializer
object.ip_address.try(:to_s)
end
def flags_agreed
object.user_stat.flags_agreed
end
def flags_disagreed
object.user_stat.flags_disagreed
end
def flags_ignored
object.user_stat.flags_ignored
end
def custom_fields
fields = User.whitelisted_user_custom_fields(scope)

View File

@ -2931,6 +2931,21 @@ en:
was_edited: "Post was edited after the first flag"
previous_flags_count: "This post has already been flagged {{count}} times."
show_details: "Show flag details"
user_percentage:
summary:
one: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} total flag)"
other: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} total flags)"
agreed:
one: "{{count}}% agree"
other: "{{count}}% agree"
disagreed:
one: "{{count}}% disagree"
other: "{{count}}% disagree"
ignored:
one: "{{count}}% ignore"
other: "{{count}}% ignore"
details: "details"
flagged_topics:

View File

@ -0,0 +1,34 @@
class AddFlagStatsToUser < ActiveRecord::Migration[5.2]
def up
add_column :user_stats, :flags_agreed, :integer, default: 0, null: false
add_column :user_stats, :flags_disagreed, :integer, default: 0, null: false
add_column :user_stats, :flags_ignored, :integer, default: 0, null: false
sql = <<~SQL
UPDATE user_stats
SET flags_agreed = x.flags_agreed,
flags_disagreed = x.flags_disagreed,
flags_ignored = x.flags_ignored
FROM (
SELECT u.id AS user_id,
SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) as flags_disagreed,
SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) as flags_agreed,
SUM(CASE WHEN pa.deferred_at IS NOT NULL THEN 1 ELSE 0 END) as flags_ignored
FROM post_actions AS pa
INNER JOIN users AS u ON u.id = pa.user_id
WHERE pa.post_action_type_id IN (#{PostActionType.notify_flag_types.values.join(', ')})
AND pa.user_id > 0
GROUP BY u.id
) AS x
WHERE x.user_id = user_stats.user_id
SQL
execute sql
end
def down
remove_column :user_stats, :flags_agreed
remove_column :user_stats, :flags_disagreed
remove_column :user_stats, :flags_ignored
end
end

View File

@ -206,7 +206,7 @@ module FlagQuery
results = PostAction
.flags
.active
.includes(post: [:user, :topic])
.includes(post: [{ user: :user_stat }, :topic])
.references(:post)
.where("posts.user_id > 0")
.order('post_actions.created_at DESC')

View File

@ -79,12 +79,17 @@ describe PostAction do
# Acting on the flag should not post an automated status message (since a moderator already replied)
expect(topic.posts.count).to eq(2)
PostAction.agree_flags!(post, admin)
expect(action.user.user_stat.flags_agreed).to eq(1)
expect(action.user.user_stat.flags_disagreed).to eq(0)
topic.reload
expect(topic.posts.count).to eq(2)
# Clearing the flags should not post an automated status message
PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message")
new_action = PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message")
PostAction.clear_flags!(post, admin)
expect(new_action.user.user_stat.flags_agreed).to eq(0)
expect(new_action.user.user_stat.flags_disagreed).to eq(1)
topic.reload
expect(topic.posts.count).to eq(2)
@ -95,6 +100,9 @@ describe PostAction do
expect(topic.posts.count).to eq(1)
PostAction.agree_flags!(another_post, admin)
expect(action.user.user_stat.flags_agreed).to eq(2)
expect(action.user.user_stat.flags_disagreed).to eq(0)
topic.reload
expect(topic.posts.count).to eq(2)
expect(topic.posts.last.post_type).to eq(Post.types[:moderator_action])
@ -361,7 +369,7 @@ describe PostAction do
# If a flag is dismissed
PostAction.clear_flags!(post, admin)
expect(PostAction.flag_counts_for(post.id)).to eq([8, 0])
expect(PostAction.flag_counts_for(post.id)).to eq([0, 8])
end
end
@ -689,6 +697,7 @@ describe PostAction do
SiteSetting.auto_respond_to_flag_actions = false
PostAction.agree_flags!(post, admin)
expect(action.user.user_stat.flags_agreed).to eq(1)
topic.reload
expect(topic.posts.count).to eq(1)
@ -704,6 +713,7 @@ describe PostAction do
SiteSetting.auto_respond_to_flag_actions = true
PostAction.agree_flags!(post, admin)
expect(action.user.user_stat.flags_agreed).to eq(1)
user_notifications = user.notifications
expect(user_notifications.count).to eq(1)
@ -715,11 +725,12 @@ describe PostAction do
post = Fabricate(:post)
user = Fabricate(:user)
action = PostAction.act(user, post, PostActionType.types[:notify_user], message: "WAT")
topic = action.reload.related_post.topic
action.reload.related_post.topic
expect(user.notifications.count).to eq(0)
SiteSetting.auto_respond_to_flag_actions = true
PostAction.agree_flags!(post, admin)
expect(action.user.user_stat.flags_agreed).to eq(0)
user_notifications = user.notifications
expect(user_notifications.count).to eq(0)

View File

@ -404,6 +404,7 @@ describe WebHook do
payload = JSON.parse(job_args["payload"])
expect(payload["id"]).to eq(post_action.id)
post_action = PostAction.act(Fabricate(:user), post, PostActionType.types[:spam])
PostAction.clear_flags!(post, moderator)
job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first

View File

@ -59,6 +59,7 @@ RSpec.describe Admin::FlagsController do
post_action.reload
expect(post_action.agreed_by_id).to eq(admin.id)
expect(user.user_stat.reload.flags_agreed).to eq(1)
post_1.reload
expect(post_1.deleted_at).to eq(nil)
@ -77,6 +78,7 @@ RSpec.describe Admin::FlagsController do
post_action.reload
expect(post_action.agreed_by_id).to eq(admin.id)
expect(user.user_stat.reload.flags_agreed).to eq(1)
agree_post = Topic.joins(:topic_allowed_users).where('topic_allowed_users.user_id = ?', user.id).order(:id).last.posts.last
expect(agree_post.raw).to eq(I18n.with_locale(:en) { I18n.t('flags_dispositions.agreed') })