mirror of
https://github.com/discourse/discourse.git
synced 2025-01-16 07:42:42 +08:00
9bf31add6a
Score types are dynamic because of custom flags. Therefore we cannot memorize them on class level as it is not multisite safe.
126 lines
3.7 KiB
Ruby
126 lines
3.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ReviewableScore < ActiveRecord::Base
|
|
belongs_to :reviewable
|
|
belongs_to :user
|
|
belongs_to :reviewed_by, class_name: "User"
|
|
belongs_to :meta_topic, class_name: "Topic"
|
|
|
|
enum :status, { pending: 0, agreed: 1, disagreed: 2, ignored: 3 }
|
|
|
|
# To keep things simple the types correspond to `PostActionType` for backwards
|
|
# compatibility, but we can add extra reasons for scores.
|
|
def self.types
|
|
PostActionType.flag_types.merge(PostActionType.score_types).merge(@api_types || {})
|
|
end
|
|
|
|
def self.type_title(type)
|
|
I18n.t("post_action_types.#{type}.title", default: nil) ||
|
|
I18n.t("reviewable_score_types.#{type}.title", default: nil) ||
|
|
PostActionType.names[types[type]]
|
|
end
|
|
|
|
# When extending post action flags, we need to call this method in order to
|
|
# get the latests flags.
|
|
def self.reload_types
|
|
@api_types = nil
|
|
types
|
|
end
|
|
|
|
def self.add_new_types(type_names)
|
|
@api_types ||= {}
|
|
next_id = types.values.max + 1
|
|
|
|
type_names.each_with_index { |name, idx| @api_types[name] = next_id + idx }
|
|
end
|
|
|
|
def self.score_transitions
|
|
{ approved: statuses[:agreed], rejected: statuses[:disagreed], ignored: statuses[:ignored] }
|
|
end
|
|
|
|
def score_type
|
|
Reviewable::Collection::Item.new(reviewable_score_type)
|
|
end
|
|
|
|
def took_action?
|
|
take_action_bonus > 0
|
|
end
|
|
|
|
def self.calculate_score(user, type_bonus, take_action_bonus)
|
|
score = user_flag_score(user) + type_bonus + take_action_bonus
|
|
score > 0 ? score : 0
|
|
end
|
|
|
|
# A user's flag score is:
|
|
# 1.0 + trust_level + user_accuracy_bonus
|
|
# (trust_level is 5 for staff)
|
|
def self.user_flag_score(user)
|
|
1.0 + (user.staff? ? 5.0 : user.trust_level.to_f) + user_accuracy_bonus(user)
|
|
end
|
|
|
|
# A user's accuracy bonus is:
|
|
# if 5 or less flags => 0.0
|
|
# if > 5 flags => (agreed flags / total flags) * 5.0
|
|
def self.user_accuracy_bonus(user)
|
|
user_stat = user&.user_stat
|
|
return 0.0 if user_stat.blank? || user.bot?
|
|
|
|
calc_user_accuracy_bonus(user_stat.flags_agreed, user_stat.flags_disagreed)
|
|
end
|
|
|
|
def self.calc_user_accuracy_bonus(agreed, disagreed)
|
|
agreed ||= 0
|
|
disagreed ||= 0
|
|
|
|
total = (agreed + disagreed).to_f
|
|
return 0.0 if total <= 5
|
|
accuracy_axis = 0.7
|
|
|
|
percent_correct = agreed / total
|
|
positive_accuracy = percent_correct >= accuracy_axis
|
|
|
|
bottom = positive_accuracy ? accuracy_axis : 0.0
|
|
top = positive_accuracy ? 1.0 : accuracy_axis
|
|
|
|
absolute_distance = positive_accuracy ? percent_correct - bottom : top - percent_correct
|
|
|
|
axis_distance_multiplier = 1.0 / (top - bottom)
|
|
positivity_multiplier = positive_accuracy ? 1.0 : -1.0
|
|
|
|
(
|
|
absolute_distance * axis_distance_multiplier * positivity_multiplier *
|
|
(Math.log(total, 4) * 5.0)
|
|
).round(2)
|
|
end
|
|
|
|
def reviewable_conversation
|
|
return if meta_topic.blank?
|
|
Reviewable::Conversation.new(meta_topic)
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: reviewable_scores
|
|
#
|
|
# id :bigint not null, primary key
|
|
# reviewable_id :integer not null
|
|
# user_id :integer not null
|
|
# reviewable_score_type :integer not null
|
|
# status :integer not null
|
|
# score :float default(0.0), not null
|
|
# take_action_bonus :float default(0.0), not null
|
|
# reviewed_by_id :integer
|
|
# reviewed_at :datetime
|
|
# meta_topic_id :integer
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# reason :string
|
|
# user_accuracy_bonus :float default(0.0), not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_reviewable_scores_on_reviewable_id (reviewable_id)
|
|
# index_reviewable_scores_on_user_id (user_id)
|
|
#
|