From 96dfa2fd657b89f2642359101534bcbd8eff695b Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 11 Dec 2018 16:01:44 +0100 Subject: [PATCH 1/2] add setting for new 'most active' algorithm --- config/locales/en/settings.yml | 2 ++ config/locales/es/settings.yml | 2 ++ db/dev_seeds/settings.rb | 1 + db/seeds.rb | 3 +++ lib/tasks/settings.rake | 5 +++++ 5 files changed, 13 insertions(+) diff --git a/config/locales/en/settings.yml b/config/locales/en/settings.yml index f8b9bf0d7..808fcaee4 100644 --- a/config/locales/en/settings.yml +++ b/config/locales/en/settings.yml @@ -52,6 +52,8 @@ en: place_name_description: "Name of your city" related_content_score_threshold: "Related content score threshold" related_content_score_threshold_description: "Hides content that users mark as unrelated" + hot_score_period_in_days: "Period (days) used by the filter 'most active'" + hot_score_period_in_days_description: "The filter 'most active' used in multiple sections will be based on the votes during the last X days" map_latitude: "Latitude" map_latitude_description: "Latitude to show the map position" map_longitude: "Longitude" diff --git a/config/locales/es/settings.yml b/config/locales/es/settings.yml index f0dff19f7..af5203c01 100644 --- a/config/locales/es/settings.yml +++ b/config/locales/es/settings.yml @@ -52,6 +52,8 @@ es: place_name_description: "Nombre de tu ciudad" related_content_score_threshold: "Umbral de puntuación de contenido relacionado" related_content_score_threshold_description: "Oculta el contenido que los usuarios marquen como no relacionado" + hot_score_period_in_days: "Periodo (días) usado para el filtro 'Más Activos'" + hot_score_period_in_days_description: "El filtro 'Más Activos' usado en diferentes secciones se basará en los votos de los últimos X días" map_latitude: "Latitud" map_latitude_description: "Latitud para mostrar la posición del mapa" map_longitude: "Longitud" diff --git a/db/dev_seeds/settings.rb b/db/dev_seeds/settings.rb index ce9bdd7c2..cc03a4315 100644 --- a/db/dev_seeds/settings.rb +++ b/db/dev_seeds/settings.rb @@ -69,6 +69,7 @@ section "Creating Settings" do Setting.create(key: 'featured_proposals_number', value: 3) Setting.create(key: 'related_content_score_threshold', value: -0.3) + Setting.create(key: 'hot_score_period_in_days', value: 31) Setting['feature.homepage.widgets.feeds.proposals'] = true Setting['feature.homepage.widgets.feeds.debates'] = true diff --git a/db/seeds.rb b/db/seeds.rb index 14f1ddaa6..7d424fe28 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -136,6 +136,9 @@ Setting['feature.homepage.widgets.feeds.proposals'] = true Setting['feature.homepage.widgets.feeds.debates'] = true Setting['feature.homepage.widgets.feeds.processes'] = true +# Votes hot_score configuration +Setting['hot_score_period_in_days'] = 31 + WebSection.create(name: 'homepage') WebSection.create(name: 'debates') WebSection.create(name: 'proposals') diff --git a/lib/tasks/settings.rake b/lib/tasks/settings.rake index 729615133..bf5ccdcc3 100644 --- a/lib/tasks/settings.rake +++ b/lib/tasks/settings.rake @@ -31,4 +31,9 @@ namespace :settings do Setting['featured_proposals_number'] = 3 end + desc "Create new period to calculate hot_score" + task create_hot_score_period_setting: :environment do + Setting['hot_score_period_in_days'] = 31 + end + end From ef835bef1c803170091e99cd19a448149ffe43ae Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 11 Dec 2018 16:02:08 +0100 Subject: [PATCH 2/2] new algorithm for filter 'most active' --- app/models/debate.rb | 7 +- app/models/legislation/proposal.rb | 7 +- app/models/proposal.rb | 7 +- config/locales/es/general.yml | 4 +- lib/score_calculator.rb | 24 +++--- lib/tasks/votes.rake | 11 +++ spec/models/debate_spec.rb | 96 ++++++++++++++++-------- spec/models/legislation/proposal_spec.rb | 78 +++++++++++++++++++ spec/models/proposal_spec.rb | 83 +++++++++++++------- 9 files changed, 230 insertions(+), 87 deletions(-) diff --git a/app/models/debate.rb b/app/models/debate.rb index 93ca5420f..a73a1ab6c 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -122,14 +122,11 @@ class Debate < ActiveRecord::Base end def after_commented - save # updates the hot_score because there is a before_save + save # update cache when it has a new comment end def calculate_hot_score - self.hot_score = ScoreCalculator.hot_score(created_at, - cached_votes_total, - cached_votes_up, - comments_count) + self.hot_score = ScoreCalculator.hot_score(self) end def calculate_confidence_score diff --git a/app/models/legislation/proposal.rb b/app/models/legislation/proposal.rb index 43a5cf1e8..d7c5e4807 100644 --- a/app/models/legislation/proposal.rb +++ b/app/models/legislation/proposal.rb @@ -121,14 +121,11 @@ class Legislation::Proposal < ActiveRecord::Base end def after_commented - save # updates the hot_score because there is a before_save + save # update cache when it has a new comment end def calculate_hot_score - self.hot_score = ScoreCalculator.hot_score(created_at, - total_votes, - total_votes, - comments_count) + self.hot_score = ScoreCalculator.hot_score(self) end def calculate_confidence_score diff --git a/app/models/proposal.rb b/app/models/proposal.rb index fff616626..e947f69e4 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -166,14 +166,11 @@ class Proposal < ActiveRecord::Base end def after_commented - save # updates the hot_score because there is a before_save + save # update cache when it has a new comment end def calculate_hot_score - self.hot_score = ScoreCalculator.hot_score(created_at, - total_votes, - total_votes, - comments_count) + self.hot_score = ScoreCalculator.hot_score(self) end def calculate_confidence_score diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 8c2212dae..573af438a 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -106,7 +106,7 @@ es: orders: confidence_score: Mejor valorados created_at: Nuevos - hot_score: Más activos hoy + hot_score: Más activos most_commented: Más comentados relevance: Más relevantes recommendations: Recomendaciones @@ -346,7 +346,7 @@ es: orders: confidence_score: Más apoyadas created_at: Nuevas - hot_score: Más activas hoy + hot_score: Más activas most_commented: Más comentadas relevance: Más relevantes archival_date: Archivadas diff --git a/lib/score_calculator.rb b/lib/score_calculator.rb index 4f4e21e85..df40428f8 100644 --- a/lib/score_calculator.rb +++ b/lib/score_calculator.rb @@ -1,19 +1,19 @@ module ScoreCalculator - EPOC = Time.new(2015, 6, 15).in_time_zone - COMMENT_WEIGHT = 1.0 / 5 # 1 positive vote / x comments - TIME_UNIT = 24.hours.to_f + def self.hot_score(resource) + return 0 unless resource.created_at - def self.hot_score(date, votes_total, votes_up, comments_count) - total = (votes_total + COMMENT_WEIGHT * comments_count).to_f - ups = (votes_up + COMMENT_WEIGHT * comments_count).to_f - downs = total - ups - score = ups - downs - offset = Math.log([score.abs, 1].max, 10) * (ups / [total, 1].max) - sign = score <=> 0 - seconds = ((date || Time.current) - EPOC).to_f + period = [ + Setting['hot_score_period_in_days'].to_i, + ((Time.current - resource.created_at) / 1.day).ceil + ].min - (((offset * sign) + (seconds / TIME_UNIT)) * 10000000).round + votes_total = resource.votes_for.where("created_at >= ?", period.days.ago).count + votes_up = resource.get_upvotes.where("created_at >= ?", period.days.ago).count + votes_down = votes_total - votes_up + votes_score = votes_up - votes_down + + (votes_score.to_f / period).round end def self.confidence_score(votes_total, votes_up) diff --git a/lib/tasks/votes.rake b/lib/tasks/votes.rake index 0cc595662..357f7578b 100644 --- a/lib/tasks/votes.rake +++ b/lib/tasks/votes.rake @@ -13,4 +13,15 @@ namespace :votes do end + desc "Resets hot_score to its new value" + task reset_hot_score: :environment do + models = [Debate, Proposal, Legislation::Proposal] + + models.each do |model| + model.find_each do |resource| + resource.save + print "." + end + end + end end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 6f9104fa8..8b6e2b052 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -236,57 +236,91 @@ describe Debate do describe '#hot_score' do let(:now) { Time.current } - it "increases for newer debates" do - old = create(:debate, :with_hot_score, created_at: now - 1.day) - new = create(:debate, :with_hot_score, created_at: now) - expect(new.hot_score).to be > old.hot_score + it "period is correctly calculated to get exact votes per day" do + new_debate = create(:debate, created_at: 23.hours.ago) + 2.times { new_debate.vote_by(voter: create(:user), vote: "yes") } + expect(new_debate.hot_score).to be 2 + + old_debate = create(:debate, created_at: 25.hours.ago) + 2.times { old_debate.vote_by(voter: create(:user), vote: "yes") } + expect(old_debate.hot_score).to be 1 + + older_debate = create(:debate, created_at: 49.hours.ago) + 3.times { older_debate.vote_by(voter: create(:user), vote: "yes") } + expect(old_debate.hot_score).to be 1 end - it "increases for debates with more comments" do - more_comments = create(:debate, :with_hot_score, created_at: now, comments_count: 25) - less_comments = create(:debate, :with_hot_score, created_at: now, comments_count: 1) - expect(more_comments.hot_score).to be > less_comments.hot_score + it "remains the same for not voted debates" do + new = create(:debate, created_at: now) + old = create(:debate, created_at: 1.day.ago) + older = create(:debate, created_at: 2.month.ago) + expect(new.hot_score).to be 0 + expect(old.hot_score).to be 0 + expect(older.hot_score).to be 0 end it "increases for debates with more positive votes" do - more_likes = create(:debate, :with_hot_score, created_at: now, cached_votes_total: 10, cached_votes_up: 5) - less_likes = create(:debate, :with_hot_score, created_at: now, cached_votes_total: 10, cached_votes_up: 1) - expect(more_likes.hot_score).to be > less_likes.hot_score + more_positive_votes = create(:debate) + 2.times { more_positive_votes.vote_by(voter: create(:user), vote: "yes") } + + less_positive_votes = create(:debate) + less_positive_votes.vote_by(voter: create(:user), vote: "yes") + + expect(more_positive_votes.hot_score).to be > less_positive_votes.hot_score end - it "increases for debates with more confidence" do - more_confidence = create(:debate, :with_hot_score, created_at: now, cached_votes_total: 1000, cached_votes_up: 700) - less_confidence = create(:debate, :with_hot_score, created_at: now, cached_votes_total: 10, cached_votes_up: 9) - expect(more_confidence.hot_score).to be > less_confidence.hot_score + it "increases for debates with the same amount of positive votes within less days" do + newer_debate = create(:debate, created_at: now) + 5.times { newer_debate.vote_by(voter: create(:user), vote: "yes") } + + older_debate = create(:debate, created_at: 1.day.ago) + 5.times { older_debate.vote_by(voter: create(:user), vote: "yes") } + + expect(newer_debate.hot_score).to be > older_debate.hot_score end - it "decays in older debates, even if they have more votes" do - older_more_voted = create(:debate, :with_hot_score, created_at: now - 5.days, cached_votes_total: 1000, cached_votes_up: 900) - new_less_voted = create(:debate, :with_hot_score, created_at: now, cached_votes_total: 10, cached_votes_up: 9) - expect(new_less_voted.hot_score).to be > older_more_voted.hot_score + it "decreases for debates with more negative votes" do + more_negative_votes = create(:debate) + 5.times { more_negative_votes.vote_by(voter: create(:user), vote: "yes") } + 3.times { more_negative_votes.vote_by(voter: create(:user), vote: "no") } + + less_negative_votes = create(:debate) + 5.times { less_negative_votes.vote_by(voter: create(:user), vote: "yes") } + 2.times { less_negative_votes.vote_by(voter: create(:user), vote: "no") } + + expect(more_negative_votes.hot_score).to be < less_negative_votes.hot_score + end + + it "increases for debates voted within the period (last month by default)" do + newer_debate = create(:debate, created_at: 2.months.ago) + 20.times { create(:vote, votable: newer_debate, created_at: 3.days.ago) } + + older_debate = create(:debate, created_at: 2.months.ago) + 20.times { create(:vote, votable: older_debate, created_at: 40.days.ago) } + + expect(newer_debate.hot_score).to be > older_debate.hot_score end describe 'actions which affect it' do - let(:debate) { create(:debate, :with_hot_score) } - it "increases with likes" do + let(:debate) { create(:debate) } + + before do + 5.times { debate.vote_by(voter: create(:user), vote: "yes") } + 2.times { debate.vote_by(voter: create(:user), vote: "no") } + end + + it "increases with positive votes" do previous = debate.hot_score - 5.times { debate.register_vote(create(:user), true) } + 3.times { debate.vote_by(voter: create(:user), vote: "yes") } expect(previous).to be < debate.hot_score end - it "decreases with dislikes" do - debate.register_vote(create(:user), true) + it "decreases with negative votes" do previous = debate.hot_score - 3.times { debate.register_vote(create(:user), false) } + 3.times { debate.vote_by(voter: create(:user), vote: "no") } expect(previous).to be > debate.hot_score end - - it "increases with comments" do - previous = debate.hot_score - 25.times{ Comment.create(user: create(:user), commentable: debate, body: 'foobarbaz') } - expect(previous).to be < debate.reload.hot_score - end end end diff --git a/spec/models/legislation/proposal_spec.rb b/spec/models/legislation/proposal_spec.rb index 4606acabc..2e23179a4 100644 --- a/spec/models/legislation/proposal_spec.rb +++ b/spec/models/legislation/proposal_spec.rb @@ -27,4 +27,82 @@ describe Legislation::Proposal do expect(proposal).not_to be_valid end + describe '#hot_score' do + let(:now) { Time.current } + + it "period is correctly calculated to get exact votes per day" do + new_proposal = create(:legislation_proposal, created_at: 23.hours.ago) + 2.times { new_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(new_proposal.hot_score).to be 2 + + old_proposal = create(:legislation_proposal, created_at: 25.hours.ago) + 2.times { old_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(old_proposal.hot_score).to be 1 + + older_proposal = create(:legislation_proposal, created_at: 49.hours.ago) + 3.times { older_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(old_proposal.hot_score).to be 1 + end + + it "remains the same for not voted proposals" do + new = create(:legislation_proposal, created_at: now) + old = create(:legislation_proposal, created_at: 1.day.ago) + older = create(:legislation_proposal, created_at: 2.month.ago) + expect(new.hot_score).to be 0 + expect(old.hot_score).to be 0 + expect(older.hot_score).to be 0 + end + + it "increases for proposals with more positive votes" do + more_positive_votes = create(:legislation_proposal) + 2.times { more_positive_votes.vote_by(voter: create(:user), vote: "yes") } + + less_positive_votes = create(:legislation_proposal) + less_positive_votes.vote_by(voter: create(:user), vote: "yes") + + expect(more_positive_votes.hot_score).to be > less_positive_votes.hot_score + end + + it "increases for proposals with the same amount of positive votes within less days" do + newer_proposal = create(:legislation_proposal, created_at: now) + 5.times { newer_proposal.vote_by(voter: create(:user), vote: "yes") } + + older_proposal = create(:legislation_proposal, created_at: 1.day.ago) + 5.times { older_proposal.vote_by(voter: create(:user), vote: "yes") } + + expect(newer_proposal.hot_score).to be > older_proposal.hot_score + end + + it "increases for proposals voted within the period (last month by default)" do + newer_proposal = create(:legislation_proposal, created_at: 2.months.ago) + 20.times { create(:vote, votable: newer_proposal, created_at: 3.days.ago) } + + older_proposal = create(:legislation_proposal, created_at: 2.months.ago) + 20.times { create(:vote, votable: older_proposal, created_at: 40.days.ago) } + + expect(newer_proposal.hot_score).to be > older_proposal.hot_score + end + + describe 'actions which affect it' do + + let(:proposal) { create(:legislation_proposal) } + + before do + 5.times { proposal.vote_by(voter: create(:user), vote: "yes") } + 2.times { proposal.vote_by(voter: create(:user), vote: "no") } + end + + it "increases with positive votes" do + previous = proposal.hot_score + 3.times { proposal.vote_by(voter: create(:user), vote: "yes") } + expect(previous).to be < proposal.hot_score + end + + it "decreases with negative votes" do + previous = proposal.hot_score + 3.times { proposal.vote_by(voter: create(:user), vote: "no") } + expect(previous).to be > proposal.hot_score + end + end + end end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index fa4226764..ff6cfb117 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -254,49 +254,78 @@ describe Proposal do describe '#hot_score' do let(:now) { Time.current } - it "increases for newer proposals" do - old = create(:proposal, :with_hot_score, created_at: now - 1.day) - new = create(:proposal, :with_hot_score, created_at: now) - expect(new.hot_score).to be > old.hot_score + it "period is correctly calculated to get exact votes per day" do + new_proposal = create(:proposal, created_at: 23.hours.ago) + 2.times { new_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(new_proposal.hot_score).to be 2 + + old_proposal = create(:proposal, created_at: 25.hours.ago) + 2.times { old_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(old_proposal.hot_score).to be 1 + + older_proposal = create(:proposal, created_at: 49.hours.ago) + 3.times { older_proposal.vote_by(voter: create(:user), vote: "yes") } + expect(old_proposal.hot_score).to be 1 end - it "increases for proposals with more comments" do - more_comments = create(:proposal, :with_hot_score, created_at: now, comments_count: 25) - less_comments = create(:proposal, :with_hot_score, created_at: now, comments_count: 1) - expect(more_comments.hot_score).to be > less_comments.hot_score + it "remains the same for not voted proposals" do + new = create(:proposal, created_at: now) + old = create(:proposal, created_at: 1.day.ago) + older = create(:proposal, created_at: 2.month.ago) + expect(new.hot_score).to be 0 + expect(old.hot_score).to be 0 + expect(older.hot_score).to be 0 end it "increases for proposals with more positive votes" do - more_likes = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 5) - less_likes = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 1) - expect(more_likes.hot_score).to be > less_likes.hot_score + more_positive_votes = create(:proposal) + 2.times { more_positive_votes.vote_by(voter: create(:user), vote: "yes") } + + less_positive_votes = create(:proposal) + less_positive_votes.vote_by(voter: create(:user), vote: "yes") + + expect(more_positive_votes.hot_score).to be > less_positive_votes.hot_score end - it "increases for proposals with more confidence" do - more_confidence = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 700) - less_confidence = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 9) - expect(more_confidence.hot_score).to be > less_confidence.hot_score + it "increases for proposals with the same amount of positive votes within less days" do + newer_proposal = create(:proposal, created_at: now) + 5.times { newer_proposal.vote_by(voter: create(:user), vote: "yes") } + + older_proposal = create(:proposal, created_at: 1.day.ago) + 5.times { older_proposal.vote_by(voter: create(:user), vote: "yes") } + + expect(newer_proposal.hot_score).to be > older_proposal.hot_score end - it "decays in older proposals, even if they have more votes" do - older_more_voted = create(:proposal, :with_hot_score, created_at: now - 5.days, cached_votes_up: 900) - new_less_voted = create(:proposal, :with_hot_score, created_at: now, cached_votes_up: 9) - expect(new_less_voted.hot_score).to be > older_more_voted.hot_score + it "increases for proposals voted within the period (last month by default)" do + newer_proposal = create(:proposal, created_at: 2.months.ago) + 20.times { create(:vote, votable: newer_proposal, created_at: 3.days.ago) } + + older_proposal = create(:proposal, created_at: 2.months.ago) + 20.times { create(:vote, votable: older_proposal, created_at: 40.days.ago) } + + expect(newer_proposal.hot_score).to be > older_proposal.hot_score end describe 'actions which affect it' do - let(:proposal) { create(:proposal, :with_hot_score) } - it "increases with votes" do - previous = proposal.hot_score - 5.times { proposal.register_vote(create(:user, verified_at: Time.current), true) } - expect(previous).to be < proposal.reload.hot_score + let(:proposal) { create(:proposal) } + + before do + 5.times { proposal.vote_by(voter: create(:user), vote: "yes") } + 2.times { proposal.vote_by(voter: create(:user), vote: "no") } end - it "increases with comments" do + it "increases with positive votes" do previous = proposal.hot_score - 25.times{ Comment.create(user: create(:user), commentable: proposal, body: 'foobarbaz') } - expect(previous).to be < proposal.reload.hot_score + 3.times { proposal.vote_by(voter: create(:user), vote: "yes") } + expect(previous).to be < proposal.hot_score + end + + it "decreases with negative votes" do + previous = proposal.hot_score + 3.times { proposal.vote_by(voter: create(:user), vote: "no") } + expect(previous).to be > proposal.hot_score end end end