From 96a935ffecd7cfe36bfca00aedb2feeb606daae6 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Fri, 28 Jan 2022 12:28:18 -0500 Subject: [PATCH] Add new cases for reaction weights to lead to more effective rankings (#16138) * Base author reaction functionality * Get logic more in place * Finalize tests * Add admin clause for new user points * Add proper registered_at for seeded user * Fix regsitered_at in e2e * Add registered_at across the board in e2e tests * Update comments * Update spec/models/reaction_spec.rb Co-authored-by: Jamie Gaskins * Update spec/models/reaction_spec.rb Co-authored-by: Jamie Gaskins * Update spec/models/reaction_spec.rb Co-authored-by: Jamie Gaskins * Put points in constant and refactor points resave logic Co-authored-by: Jamie Gaskins --- app/models/reaction.rb | 27 +++++++++++++++++++ spec/models/reaction_spec.rb | 46 +++++++++++++++++++++++++++++++-- spec/support/seeds/seeds_e2e.rb | 8 ++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 66445d6b5..ee69393ab 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -16,6 +16,9 @@ class Reaction < ApplicationRecord REACTABLE_TYPES = %w[Comment Article User].freeze STATUSES = %w[valid invalid confirmed archived].freeze + # Days to ramp up new user points weight + NEW_USER_RAMPUP_DAYS_COUNT = 10 + belongs_to :reactable, polymorphic: true belongs_to :user @@ -158,9 +161,19 @@ class Reaction < ApplicationRecord def assign_points base_points = BASE_POINTS.fetch(category, 1.0) + + # Ajust for certain states base_points = 0 if status == "invalid" base_points /= 2 if reactable_type == "User" base_points *= 2 if status == "confirmed" + + unless persisted? # Actions we only want to apply upon initial creation + # Author's comment reaction counts for more weight on to their own posts. (5.0 vs 1.0) + base_points *= 5 if positive_reaction_to_comment_on_own_article? + + # New users will have their reaction weight gradually ramp by 0.1 from 0 to 1.0. + base_points *= new_user_adjusted_points if new_untrusted_user # New users get minimal reaction weight + end self.points = user ? (base_points * user.reputation_modifier) : -5 end @@ -179,4 +192,18 @@ class Reaction < ApplicationRecord def notify_slack_channel_about_vomit_reaction Slack::Messengers::ReactionVomit.call(reaction: self) end + + def positive_reaction_to_comment_on_own_article? + BASE_POINTS.fetch(category, 1.0).positive? && + reactable_type == "Comment" && + reactable&.commentable&.user_id == user_id + end + + def new_user_adjusted_points + ((Time.current - user.registered_at).seconds.in_days / NEW_USER_RAMPUP_DAYS_COUNT) + end + + def new_untrusted_user + user.registered_at > NEW_USER_RAMPUP_DAYS_COUNT.days.ago && !user.trusted? && !user.any_admin? + end end diff --git a/spec/models/reaction_spec.rb b/spec/models/reaction_spec.rb index 9d576e943..24dc145e4 100644 --- a/spec/models/reaction_spec.rb +++ b/spec/models/reaction_spec.rb @@ -1,9 +1,9 @@ require "rails_helper" RSpec.describe Reaction, type: :model do - let(:user) { create(:user) } + let(:user) { create(:user, registered_at: 20.days.ago) } let(:article) { create(:article, user: user) } - let(:reaction) { build(:reaction, reactable: article) } + let(:reaction) { build(:reaction, reactable: article, user: user) } describe "builtin validations" do subject { build(:reaction, reactable: article, user: user) } @@ -74,12 +74,54 @@ RSpec.describe Reaction, type: :model do expect(reaction.points).to eq(0) end + it "assigns extra 5 points if reaction is to comment on author's post" do + comment = create(:comment, commentable: article) + comment_reaction = create(:reaction, reactable: comment, user: user) + expect(comment_reaction.points).to eq(5.0) + end + + it "does not extra 5 points if reaction is to comment on author's post" do + second_user = create(:user) + second_article = create(:article, user: second_user) + comment = create(:comment, commentable: second_article) + comment_reaction = create(:reaction, reactable: comment, user: user) + expect(comment_reaction.points).to eq(1) + end + it "assigns the correct points if reaction is confirmed" do reaction_points = reaction.points reaction.update(status: "confirmed") expect(reaction.points).to eq(reaction_points * 2) end + it "assigns fractional points to new users on create" do + newish_user = create(:user, registered_at: 3.days.ago) + reaction = create(:reaction, reactable: article, user: newish_user) + expect(reaction.points).to be_within(0.1).of(0.3) + end + + it "Does not assign new fractional logic on re-save" do + reaction.save + original_points = reaction.points + reaction.user.update_column(:registered_at, 7.days.ago) + reaction.save + expect(reaction.points).to eq(original_points) + end + + it "assigns full points to new users who is also trusted" do + newish_user = create(:user, registered_at: 3.days.ago) + newish_user.add_role(:trusted) + create(:reaction, reactable: article, user: newish_user) + expect(reaction.points).to be_within(0.1).of(1.0) + end + + it "assigns full points to new users who is admin" do + newish_user = create(:user, registered_at: 3.days.ago) + newish_user.add_role(:admin) + create(:reaction, reactable: article, user: newish_user) + expect(reaction.points).to be_within(0.1).of(1.0) + end + context "when user is trusted" do before { reaction.user.add_role(:trusted) } diff --git a/spec/support/seeds/seeds_e2e.rb b/spec/support/seeds/seeds_e2e.rb index b8ab6a12c..baf2798ef 100644 --- a/spec/support/seeds/seeds_e2e.rb +++ b/spec/support/seeds/seeds_e2e.rb @@ -32,6 +32,7 @@ seeder.create_if_doesnt_exist(User, "email", "admin@forem.local") do username: "Admin_McAdmin", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -76,6 +77,7 @@ seeder.create_if_doesnt_exist(User, "email", "trusted-user-1@forem.local") do username: "trusted_user_1", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -120,6 +122,7 @@ seeder.create_if_doesnt_exist(User, "email", "change-password-user@forem.com") d username: "changepassworduser", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -146,6 +149,7 @@ seeder.create_if_doesnt_exist(User, "email", "article-editor-v1-user@forem.local username: "article_editor_v1_user", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -173,6 +177,7 @@ seeder.create_if_doesnt_exist(User, "email", "article-editor-v2-user@forem.local username: "article_editor_v2_user", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -199,6 +204,7 @@ seeder.create_if_doesnt_exist(User, "email", "notifications-user@forem.local") d username: "notifications_user", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -271,6 +277,7 @@ seeder.create_if_doesnt_exist(User, "email", "liquid-tags-user@forem.local") do username: "liquid_tags_user", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true, @@ -431,6 +438,7 @@ seeder.create_if_doesnt_exist(User, "email", "series-user@forem.local") do username: "series_user", profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")), confirmed_at: Time.current, + registered_at: Time.current, password: "password", password_confirmation: "password", saw_onboarding: true,